diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de626c8..9af8c37 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,15 +17,6 @@ jobs: strategy: fail-fast: false matrix: - # Pin macOS to macos-14 (Sonoma): the rolling `macos-latest` image - # The "ld: library 'clang_rt.osx' not found" failure is NOT a flaky - # image. It is the linker resolving a clang compiler-rt search path - # (Xcode .../clang//lib/darwin) that no longer exists on the runner, - # because a STALE `target/` was restored from the cache after the - # runner image's default Xcode moved. The real fix is the cache change - # below (we no longer cache `target/`, and the key is bumped to v2 to - # discard the poisoned caches), so the runner choice is no longer - # load-bearing. macos-latest is fine again with a clean target dir. os: [macos-latest, ubuntu-latest] steps: - uses: actions/checkout@v4 @@ -35,20 +26,17 @@ jobs: with: components: clippy - - name: Cache cargo (registry only) + - name: Cache cargo uses: actions/cache@v4 with: - # Deliberately NOT caching `target/`: a `target/` built against a - # different Xcode carries a stale clang compiler-rt search path that - # breaks the linker ("clang_rt.osx not found"). Cache only the - # download-heavy registry/git dirs; recompiling is cheap and correct. path: | ~/.cargo/bin/ ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ - key: ${{ runner.os }}-cargo-v2-${{ hashFiles('**/Cargo.lock') }} - restore-keys: ${{ runner.os }}-cargo-v2- + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo- - name: Check run: cargo check --workspace @@ -73,13 +61,13 @@ jobs: fail-fast: false matrix: include: - - os: macos-14 + - os: macos-latest target: aarch64-apple-darwin cargo_flags: "" # 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-14 + - os: macos-latest target: x86_64-apple-darwin cargo_flags: "--no-default-features --features ort-dynamic,vector-search" - os: ubuntu-latest diff --git a/.github/workflows/guard-no-private-cloud.yml b/.github/workflows/guard-no-private-cloud.yml deleted file mode 100644 index 515e959..0000000 --- a/.github/workflows/guard-no-private-cloud.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Guard — No Private Cloud Code - -# Fails if private Vestige Cloud *service* code (billing, sync-key/namespace -# mapping, Lemon Squeezy webhooks, transactional email) ever lands in this -# public repo. The public cloud *client* is allowed and does not trip this. -on: - push: - branches: [main, feat/cloud-sync-mvp] - pull_request: - workflow_dispatch: - -permissions: - contents: read - -jobs: - guard: - name: No private cloud service code - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - - name: Scan for private cloud service markers - run: ./scripts/check-no-private-cloud.sh diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml deleted file mode 100644 index 2b9f8a5..0000000 --- a/.github/workflows/pages.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: Deploy GitHub Pages - -on: - push: - branches: [main, feat/cloud-sync-mvp] - paths: - - 'apps/dashboard/**' - - '.github/workflows/pages.yml' - workflow_dispatch: - -permissions: - contents: read - pages: write - id-token: write - -# Allow one concurrent deployment; let an in-progress run finish. -concurrency: - group: pages - cancel-in-progress: false - -jobs: - deploy: - runs-on: ubuntu-latest - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - env: - # GitHub Pages serves this project repo from the /vestige/ subpath. - # The dashboard must be built with a matching base so every _app/ asset - # resolves instead of 404ing. - VESTIGE_BASE_PATH: /vestige - steps: - - uses: actions/checkout@v5 - - - uses: pnpm/action-setup@v4 - with: - version: 10 - - - uses: actions/setup-node@v5 - with: - node-version: 24 - cache: pnpm - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Build dashboard - run: pnpm --filter @vestige/dashboard build - - - name: Assemble site root - run: | - # The Pages project site is already served from /vestige/, and the - # dashboard is built with base path /vestige (VESTIGE_BASE_PATH), so - # its files go directly at the artifact root — the base path already - # accounts for the subpath. No extra subdirectory or redirect needed. - rm -rf _site - mkdir -p _site - cp -r apps/dashboard/build/* _site/ - - - uses: actions/configure-pages@v5 - - - uses: actions/upload-pages-artifact@v3 - with: - path: _site - - - id: deployment - uses: actions/deploy-pages@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c9ea405..6315588 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,14 +31,7 @@ jobs: - target: x86_64-pc-windows-msvc os: windows-latest archive: zip - # vector-search (usearch HNSW index) is REQUIRED. Without it the - # storage layer's #[cfg(feature = "vector-search")] paths compile - # out, so embeddings are never persisted or queried: the binary - # reports "Embedding Service: Not Ready", 0% coverage, consolidate - # generates 0 embeddings, and no model download is ever attempted. - # usearch builds cleanly on MSVC because vestige-core pins it with - # features=["fp16lib"] (see crates/vestige-core/Cargo.toml). - cargo_flags: "--no-default-features --features embeddings,ort-download,vector-search" + cargo_flags: "" needs_onnxruntime: false # Intel Mac uses the ort-dynamic feature to runtime-link against a # system libonnxruntime (Homebrew), sidestepping the missing @@ -57,93 +50,14 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - with: - ref: ${{ github.event_name == 'workflow_dispatch' && github.sha || github.event.inputs.tag || github.ref }} - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 10 - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: 22 - name: Install Rust uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} - - name: Validate release version - shell: bash - env: - RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }} - run: | - node <<'NODE' - const { execFileSync } = require('node:child_process'); - const tag = process.env.RELEASE_TAG || ''; - const expected = tag.replace(/^refs\/tags\//, '').replace(/^v/, ''); - if (!expected) { - throw new Error('Release tag is empty'); - } - - const packageFiles = [ - 'package.json', - 'apps/dashboard/package.json', - 'packages/vestige-init/package.json', - 'packages/vestige-mcp-npm/package.json' - ]; - for (const file of packageFiles) { - const actual = require(`./${file}`).version; - if (actual !== expected) { - throw new Error(`${file} version ${actual} does not match ${tag}`); - } - } - - const metadata = JSON.parse(execFileSync('cargo', [ - 'metadata', - '--format-version', - '1', - '--locked', - '--no-deps' - ], { encoding: 'utf8' })); - for (const name of ['vestige-core', 'vestige-mcp']) { - const pkg = metadata.packages.find((candidate) => candidate.name === name); - if (!pkg) throw new Error(`Missing Cargo package ${name}`); - if (pkg.version !== expected) { - throw new Error(`${name} version ${pkg.version} does not match ${tag}`); - } - } - NODE - - - name: Build embedded dashboard - shell: bash - env: - RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }} - run: | - pnpm install --frozen-lockfile - pnpm --filter @vestige/dashboard check - pnpm --filter @vestige/dashboard test - pnpm --filter @vestige/dashboard build - node <<'NODE' - const fs = require('node:fs'); - const tag = process.env.RELEASE_TAG || ''; - const expected = tag.replace(/^refs\/tags\//, '').replace(/^v/, ''); - const versionFile = 'apps/dashboard/build/_app/version.json'; - const version = JSON.parse(fs.readFileSync(versionFile, 'utf8')).version; - if (version !== expected) { - throw new Error(`${versionFile} version ${version} does not match ${tag}`); - } - for (const file of ['apps/dashboard/build/index.html', versionFile]) { - if (!fs.existsSync(file)) { - throw new Error(`Dashboard build did not produce ${file}`); - } - } - NODE - - name: Build - run: cargo build --locked --package vestige-mcp --release --target ${{ matrix.target }} ${{ matrix.cargo_flags }} + run: cargo build --package vestige-mcp --release --target ${{ matrix.target }} ${{ matrix.cargo_flags }} - name: Package (Unix) if: matrix.os != 'windows-latest' @@ -163,21 +77,10 @@ jobs: cd target/${{ matrix.target }}/release Compress-Archive -Path vestige-mcp.exe,vestige.exe,vestige-restore.exe -DestinationPath ../../../vestige-mcp-${{ matrix.target }}.zip - - name: Generate checksum - shell: bash - run: | - if command -v shasum >/dev/null 2>&1; then - shasum -a 256 vestige-mcp-${{ matrix.target }}.${{ matrix.archive }} > vestige-mcp-${{ matrix.target }}.${{ matrix.archive }}.sha256 - else - sha256sum vestige-mcp-${{ matrix.target }}.${{ matrix.archive }} > vestige-mcp-${{ matrix.target }}.${{ matrix.archive }}.sha256 - fi - - name: Upload to Release uses: softprops/action-gh-release@v2 with: tag_name: ${{ github.event.inputs.tag || github.ref_name }} - files: | - vestige-mcp-${{ matrix.target }}.${{ matrix.archive }} - vestige-mcp-${{ matrix.target }}.${{ matrix.archive }}.sha256 + files: vestige-mcp-${{ matrix.target }}.${{ matrix.archive }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 244fe52..7f169eb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,19 +12,6 @@ env: VESTIGE_TEST_MOCK_EMBEDDINGS: "1" jobs: - hook-tests: - name: Hook Tests - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: "3.9" - - run: python3 -m unittest discover -s tests/hooks -p 'test_*.py' - - run: python3 -m py_compile hooks/sanhedrin-local.py tests/hooks/test_sanhedrin_claim_mode.py - - run: bash -n hooks/sanhedrin.sh scripts/install-sandwich.sh scripts/check-sandwich-prereqs.sh - unit-tests: name: Unit Tests runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 80763c9..97c3949 100644 --- a/.gitignore +++ b/.gitignore @@ -139,6 +139,3 @@ apps/dashboard/node_modules/ # ============================================================================= fastembed-rs/ .mcp.json - -.claude/ -.codebase-memory/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 908940f..f03462d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,469 +5,17 @@ All notable changes to Vestige will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] - -### Fixed — Auto-consolidation merge: opt-out lever + protected pins honored (#142) - -The background consolidation cycle's auto-dedup pass silently concat-merges -near-duplicate memories (cosine ≥ 0.85): it keeps the strongest node, folds the -weaker ones in as `[MERGED]` blocks, and **hard-deletes** the originals — with no -reflog and no way to turn it off. Two fixes. First, it is now disableable: set -`VESTIGE_AUTO_CONSOLIDATE_MERGE=0` (or `false`/`off`/`no`) to suppress it. It -remains **on by default** (behavior unchanged), and the `dedup` MCP tool stays -available for on-demand, previewable, reversible merges regardless. Second, -**protected (pinned) memories are now excluded from this pass** — previously -`dedup protect` did nothing here, so a pinned memory could be absorbed or deleted -unattended, contradicting the interactive contract that a protected node may only -survive a merge, never be absorbed. A protected node is now never an anchor, never -a cluster member, and thus never merged into or deleted, whether the lever is on -or off. - -## [2.2.1] - 2026-07-02 — "Windows embeddings + backfill safety" - -A focused patch release. Two fixes plus a first-run guide. - -### Fixed — Windows embeddings never initialized (#101) - -The `x86_64-pc-windows-msvc` v2.2.0 binary was built without the `vector-search` -feature, so the storage layer's `#[cfg(feature = "vector-search")]` paths compiled -out. On Windows this meant new memories got no embedding, semantic search returned -nothing, `vestige health` reported "Embedding Service: Not Ready" (0% coverage), -and no model download was ever attempted — while v2.1.23 worked on the same machine. -The release build now includes `vector-search` on Windows (it compiles cleanly on -MSVC because `usearch` is pinned with `features = ["fp16lib"]`). npm and direct -downloads are fixed by the same rebuilt release asset. Thanks @Vrakoss for the -precise report. - -### Fixed — Retroactive Salience Backfill: bounded promote + opt-out lever (#103) - -The consolidation-pass backfill promoted root-cause memories with an **uncapped** -`stability * 1.5` FSRS multiply, and a code comment wrongly claimed it was capped. -On a chronically-recurring failure this could inflate a cause's stability without -bound, distorting its review schedule. Backfill promotion is now bounded to -`MIN(stability * 1.5, stability + 365.0)` (the additive +365-day ceiling the -backfill module already computed but never applied), on both the auto-fire and the -manual `backfill` tool paths. Auto-fire remains **on by default** (it shipped and -was documented in v2.2.0) but is now disableable: set `VESTIGE_BACKFILL_AUTOFIRE=0` -(or `false`/`off`/`no`) to turn it off; the manual `backfill` tool + CLI remain -available regardless. Thanks @randomnimbus for the report and the initial patch. - -### Added — First-run guide (#83) - -A single `docs/GETTING-STARTED.md` that consolidates install, "what gets saved", -how to inspect your memory, and project scoping into one 30-minute first-run path, -linked from the README. - -### Credits - -This release was driven by the community: - -- **@Vrakoss** reported the Windows embeddings regression (#101) with a clean, - precise repro that pinned the failure immediately. -- **@randomnimbus** (Peter Lauzon) reported the backfill safety issue (#103) and - contributed the fix — the bounded `promote_memory_backfill` and the - `VESTIGE_BACKFILL_AUTOFIRE` lever shipped as they proposed them - (co-authored in `f7530af`). - -Thank you both. - -## [2.2.0] - 2026-06-29 — "Retroactive Salience + Tool Consolidation" - -Three independent value streams land together as a coherent release. - -### Added — Retroactive Salience Backfill ("Memory with hindsight") - -A faithful port of Cai 2024 (*Nature*). When a **failure** (bug/crash/regression) -is recorded, Vestige reaches **backward in time** and promotes the quiet earlier -memory that *caused* it — the root cause a vector search structurally cannot -surface, because it is not *similar* to the failure, only *causally upstream* -(it shares an entity: the same file, env var, or service). Backward-only by -construction. Auto-fires inside the consolidation pass; also exposed as the -`backfill` MCP tool and the `vestige backfill` CLI command (`--manual`, -`--contrast`, `--no-promote` dry-run). - -### Changed — MCP Tool Consolidation (34 → 13 advertised tools) - -The MCP surface is consolidated from 34 tools to **13**: `recall` (folds -search + deep_reference + contradictions), `maintain` (consolidate/dream/gc/ -importance_score/backup/export/restore), `dedup` (8 merge tools → 1), `graph` -(explore/predict/memory_graph/composed_graph), `memory_status` (system_status/ -memory_health/timeline/changelog), plus `memory`, `codebase`, `intention`, -`smart_ingest`, `source_sync`, `session_start`, `suppress`, and the flagship -`backfill`. Old tool names remain dispatchable as hidden back-compat aliases. - -### Improved — `deep_reference` retrieval engine - -- **F32 embeddings** (was I8 quantization) — lifts the 0.4–0.6 paraphrase cosine - band so close-but-reworded queries actually retrieve. -- **Reciprocal Rank Fusion** replaces linear score combination in hybrid search. -- **Claim-vs-memory contradiction** — `recall`/`cross_reference` now test *your - claim* against stored memory, surfacing `claim_contradicts_memory` instead of - the old "confident silence." -- **Never-composed semantic-band gate** — admits no-shared-word memory pairs in - the 0.45–0.85 cosine band for `vestige compose`. -- New `vestige recall ` and `vestige compose` CLI commands expose the - engine outside the MCP path. - -### Fixed — security & correctness (multi-model audit swarm) - -SSRF/token-exfil hardening, panic/DoS/overflow fixes, deadlock and -lock-contention fixes, dedup and decay correctness. `usearch` keeps -`features = ["fp16lib"]` to avoid the Windows MSVC C1021 build break (#71/#94). - -## [2.1.27] - 2026-06-19 — "External-Source Connectors" - -Roadmap [#57](https://github.com/samvallad33/vestige/issues/57), **Phases 1–4 -(complete)**: Vestige can now act as a durable, local, semantically-searchable -retrieval layer over an external system of record — GitHub Issues and Redmine — -without replacing it. The external system stays canonical; Vestige **indexes, -connects, retrieves, and cites back** to the source record. - -Unlike a live ticket-system MCP proxy (which holds no state and is rate-limited -per query), Vestige keeps a durable embedded index: searchable **offline**, -**semantically**, joinable with the rest of your memory, temporally versioned, -and re-syncable **idempotently** with no duplication. To our knowledge no other -local-first memory layer combines native connectors, external-URL provenance, -content-hash idempotent sync, and tombstoning of vanished records. - -### Added - -- **`source_sync` MCP tool** — index an external system into Vestige. - - GitHub: `{"source": "github", "repo": "owner/name"}` indexes every issue + - its comments. Auth via `GITHUB_TOKEN` (public repos work tokenless at a - lower rate limit). - - Redmine: `{"source": "redmine", "project": ""}` indexes a project's - issues + journals (comments and status/assignment history). Host from - `REDMINE_URL`, auth from `REDMINE_API_KEY`. - - Re-running updates changed issues in place (no duplicates); `reconcile: - true` tombstones issues no longer visible upstream. -- **Source-aware investigation filters on `search`** (Phase 4) — filter results - by `source_system`, `source_project`, `source_id`, `source_type`, - `source_author`, a `source_updated_after`/`source_updated_before` date range, - and `source_status` (`valid` / `tombstoned` / `any`). Status, tracker, and - priority remain filterable via the existing `tag_prefix` (the connectors emit - `status:`/`tracker:`/`priority:`/`label:` tags). Applied as post-filters; - non-connector memories are excluded from a source-scoped query. -- **Source envelope** on every memory — structured, machine-readable provenance - (`source_system`, `source_id`, `source_url`, `source_updated_at`, - `content_hash`, `synced_at`, `source_project`, `source_type`, `source_author`) - distinct from the legacy free-form `source` label. Search results gain a - `sourceRecord` object (with the canonical `url`) **only** for - connector-ingested memories, so an agent can cite and follow the source. -- **Idempotent sync primitives** (`vestige-core`): `upsert_by_source` (keyed on - `(source_system, source_id)`, content-hash change detection), per-connector - cursor checkpoints (`connector_cursors`), and `reconcile_source_tombstones` - (invalidate-don't-delete via the bitemporal `valid_until`, so a vanished - record is retained for audit but drops out of current retrieval). -- **Connector contract** (`vestige_core::connectors`) — a small source-agnostic - `Connector` trait + `run_sync` driver (cursor overlap window, incremental - paging, optional deletion reconcile) with two reference connectors behind the - optional `connectors` cargo feature (on by default in the MCP server, off in - the core library's default features so non-connector consumers link no HTTP - client): - - **GitHub Issues** — `state=all`, `since` cursor, Link-header pagination, - drops PRs, host-pinned next-url. - - **Redmine** — `status_id=*` (open + closed), hex-encoded `updated_on>=` - cursor, `offset` pagination, per-issue detail fetch for journals (the list - endpoint omits them), `X-Redmine-API-Key` header auth. - -### Database - -- **Migration V17** — nine nullable source-envelope columns on `knowledge_nodes` - (additive; every existing memory is untouched), a partial UNIQUE index on - `(source_system, source_id)` enforcing one memory per external record while - costing nothing for envelope-less legacy rows, and the `connector_cursors` - checkpoint table. Idempotent on replay, following the established - `add_column_if_missing` pattern. - -### Notes - -- Local-first and optional: with no `source_sync` call, behavior is unchanged. - The default core-library build does not link an HTTP client. - -## [2.1.26] - 2026-06-15 — "Configurable Output" - -Roadmap **Phase 2: Configurable Output**. Users can now control the default -shape and size of high-traffic MCP responses with an optional, local-first -config file — without recompiling and without a cloud service. The default -behavior is unchanged: a fresh install with no `vestige.toml` behaves exactly -as before. - -### Added - -- **`vestige.toml` config file**, loaded from the active Vestige data directory - (`/vestige.toml`, alongside `vestige.db`). A missing or malformed - file falls back to built-in defaults, so existing installs are unaffected. -- **`[defaults]` table** with three keys: `detail_level` - (`brief` | `summary` | `full`), `limit` (default result count for - high-traffic tools), and `profile`. -- **Output profiles** — `lean`, `default`, `audit`, `research` — each presetting - a coherent bundle of detail level, result limit, and whether scores and - timestamps are included: - - `lean`: `brief` detail, limit 5, scores and timestamps dropped (smallest - context cost). - - `default`: historical behavior — `summary` detail, tool's own default - limit, scores and timestamps present. **Unchanged.** - - `audit`: `full` detail with every field, score, and timestamp. - - `research`: `full` detail with a larger default limit (25). -- **Three-layer precedence**, applied per call: an explicit MCP parameter wins - over the config file, which wins over the built-in default. -- **`profile` field** echoed in `search`, `memory_timeline`, `codebase` - (`get_context`), and `session_context` responses so the active profile is - observable. - -### Changed - -- `search`, `memory_timeline`, `codebase` (`get_context`), and - `session_context` now resolve their default detail level and result limit - through the config file when no explicit parameter is supplied. With no - `vestige.toml` present, their output is byte-for-byte identical to v2.1.25. - -### Documentation - -- `docs/CONFIGURATION.md` gains a **Output Configuration (`vestige.toml`)** - section documenting the file location, `[defaults]` keys, profile presets, - and precedence rules. - -## [2.1.25] - 2026-06-12 — "Merge / Supersede Controls" - -v2.1.25 ships Phase 3: diff-previewed, confidence-gated, reversible, -self-explaining combine/dedupe/supersede on a never-delete (bitemporal) store. -The default is always preview/review — these tools never silently mutate memory. -The differentiator is the reversible operation log: every merge/supersede/undo is -an auditable, reversible event with provenance ("why did these combine?") — a git -reflog for your agent's memory. - -### Added - -- **Seven new MCP tools** for merge/supersede control: - - `merge_candidates` — surface likely duplicate/overlapping clusters with - confidence scores and the signals behind each (Fellegi-Sunter - match/possible/non-match). Read-only. - - `plan_merge` — produce a previewable merge PLAN (a diff of combined - content/tags/provenance) without applying it. - - `plan_supersede` — preview superseding A with B (bitemporal invalidation, - audit-preserving) without applying. - - `apply_plan` — execute a previously-generated plan id; recorded as a - reversible operation. - - `merge_undo` — reverse a prior merge/supersede operation, or list the - reversible operation log (the "memory reflog"). - - `protect` — pin a memory so it can never be auto-merged, superseded, or - garbage-collected. - - `merge_policy` — get/set the per-project Fellegi-Sunter two thresholds - (`match_threshold`, `possible_threshold`) and `auto_apply`. -- **Bitemporal "invalidate, don't delete" supersede** (Graphiti-style): a - superseded memory is kept and stays queryable for audit. It is stamped with - `valid_until = now` and a new `superseded_by` lineage pointer, instead of being - deleted or merely demoted. -- **Reversible operation log** (`merge_operations` table) — every applied - merge/supersede records an undo payload and provenance signals so any operation - can be reversed, including restoring survivor content/tags and clearing the - bitemporal invalidation. -- **Fellegi-Sunter two-threshold scoring** for dedup/merge candidates, combining - embedding cosine similarity with tag and content-token overlap. Borderline - "possible" matches are surfaced for review instead of force-merged. -- **Memory protection / pinning** — `protected` column on `knowledge_nodes`; - protected memories are excluded from auto-merge/supersede/GC paths. -- **Migration V14** adding the `merge_plans` and `merge_operations` tables, the - `protected` and `superseded_by` columns on `knowledge_nodes`, and their - indexes. Idempotent on replay. -- **Docs**: `docs/MERGE_SUPERSEDE.md` describing the design, the bitemporal - model, the two-threshold policy, the reversible operation log, and the tool - surface. - -### Notes - -- All merge/supersede operations are **opt-in and preview-first**. `apply_plan` - requires `confirm=true` for `possible`/`non_match` plans, and only applies - `match` plans without confirmation when `merge_policy.auto_apply` is enabled - (default off). This deliberately avoids the silent-merge / auto-delete / - audit-trail-loss anti-patterns reported against other memory systems. -- The merge policy persists per-project and is also overridable via - `VESTIGE_MERGE_MATCH_THRESHOLD`, `VESTIGE_MERGE_POSSIBLE_THRESHOLD`, and - `VESTIGE_MERGE_AUTO_APPLY` environment variables. - -## [2.1.23] - 2026-05-27 — "Receipt Lock Hardening" - -v2.1.23 hardens the Sanhedrin launch path so Receipt Lock is portable, -observable, and precise enough for broader use. - -### Added - -- **Model-agnostic Sanhedrin backend presets** for custom OpenAI-compatible - servers, small laptops, Ollama, MLX, vLLM, llama.cpp, hosted APIs, and - Anthropic via LiteLLM. Sanhedrin no longer guesses a large default verifier. -- **Fail-open telemetry** in `fail-open.jsonl`, plus a dashboard telemetry API - and 7-day ambient dashboard counters for vetoes, appeals, and fail-open runs. -- **Receipt schema documentation** covering receipt artifacts, appeals, command - ledgers, fail-open logs, compatibility rules, and staged-evidence trust - boundaries. -- **Opt-in CUDA feature flags** for Qwen3 embedding builds on NVIDIA hardware. - -### Changed - -- Receipt Lock strips code fences, inline code, blockquotes, quoted regions, and - scoped epistemic hedges before matching verification claims. -- Structured transcript tool-use receipts are the default evidence path; loose - JSON command scanning now requires `VESTIGE_SANHEDRIN_ALLOW_LOOSE_LEDGER=1`. -- Claim-mode sampling now prioritizes higher-severity claims instead of taking - the first eight source-order claims. -- Hosted Sanhedrin credentials now require `VESTIGE_SANHEDRIN_API_KEY` and are - only sent to the configured Sanhedrin endpoint, never to Vestige retrieval. -- `smart_ingest` batch mode now defaults to `batchMergePolicy: "force_create"` - so caller-separated items stay separate unless callers opt into smart merging. -- CUDA-enabled Qwen3 builds now try `Device::new_cuda(0)` before falling back to - Metal or CPU. - -### Fixed - -- Standalone dashboard mode now hydrates the cognitive engine for Dream and - Deep Reference instead of returning 503s. -- `--data-dir` now rejects existing non-directory paths with a clear error. -- `vestige-restore` now handles `--help` and `--version` instead of treating - them as backup file paths. -- Smart ingest merge/update responses now include `previousContent`, - `mergedFrom`, and `mergePreview` so callers can inspect mutated memories. -- Daily Sanhedrin telemetry now preserves NOTE and CAUTION buckets instead of - folding both into PASS. - -## [2.1.22] - 2026-05-25 — "Sanhedrin Receipts" - -v2.1.22 makes the optional Sanhedrin hook quieter and more accountable by -turning draft judgment into local, appealable receipts instead of opaque vetoes. - -### Added - -- **Receipt Lock** blocks unsupported verification claims such as "tests passed" - unless the current transcript contains a matching successful test, build, - lint, or typecheck command receipt. -- **Veto receipts** are written to `~/.vestige/sanhedrin/latest.json` and - `latest.html` with Claim -> Verdict -> Precedent -> Fix -> Appeal fields. -- **Dashboard Verdict Bar** surfaces the latest PASS, NOTE, CAUTION, VETO, or - APPEALED state and lets users appeal stale, wrong, or too-strict vetoes. -- **Appeal training** records feedback in `appeals.jsonl` and suppresses future - vetoes for the same claim fingerprint. - -### Changed - -- Sanhedrin claim-mode output now feeds a per-claim receipt ledger while keeping - the existing one-line Stop-hook contract for Claude Code. - -## [2.1.21] - 2026-05-24 — "Agent-Neutral Hardening" - -v2.1.21 is a release-hardening pass for normal MCP usage across agents. It keeps -Claude Code Cognitive Sandwich companion files optional while making the MCP -server, package installer, release workflow, and portable sync path safer. - -### Added - -- **Agent-neutral memory protocol** — new `docs/AGENT-MEMORY-PROTOCOL.md` gives - any MCP-compatible client the same practical memory loop: initialize context, - search/deep-reference when needed, save durable facts with `smart_ingest`, and - promote/demote/purge with `memory`. -- **HTTP transport opt-in** — `vestige-mcp` now requires `--http`, - `--http-port`, or `VESTIGE_HTTP_ENABLED=1` before starting MCP-over-HTTP. -- **Release checksums** — release assets now publish `.sha256` files beside each - archive. - -### Changed - -- **`vestige update` is binary-only by default** — Claude Code Cognitive - Sandwich companion files refresh only with `vestige update --sandwich-companion` - or `vestige sandwich install`. -- **MCP tool results include structured content** while keeping text content for - clients that only consume the classic MCP response shape. -- **NPM install messaging is agent-neutral** and unsupported release targets - fail fast instead of trying to download assets that do not exist. -- **Portable merge uses UPSERT instead of `INSERT OR REPLACE`** for keyed tables, - preserving related rows instead of causing delete-and-insert side effects. - -### Fixed - -- **Destructive delete confirmation** — `memory(action="delete")` now requires - `confirm=true`, matching `purge`; the deprecated `delete_knowledge` shim no - longer bypasses confirmation. -- **Portable purge tombstone sync** — merge imports now carry - `deletion_tombstones` and apply purges without retaining deleted memory text. - Hard purge tombstones win over newer local edits during portable sync, while - tombstone merges keep the newest deletion timestamp. -- **Vector index reload staleness** — loading persisted embeddings rebuilds the - in-memory index from an empty index before adding current embeddings. -- **HTTP transport hardening** — origin, Accept, session, and protocol-version - validation now reject incompatible or cross-origin browser requests earlier. -- **Init config safety** — `@vestige/init` backs up existing config files, writes - atomically, accepts JSONC-style comments/trailing commas, and no longer writes - Xcode trust-accepted flags. -- **Release tag checkout** — manual release builds now checkout the requested tag - or ref before packaging. - -### Verified - -- `cargo test -p vestige-mcp --lib --no-fail-fast` -- `cargo test -p vestige-mcp --bin vestige-mcp --no-fail-fast` -- `cargo test -p vestige-core portable_merge_import --no-fail-fast` -- `cargo test -p vestige-mcp --bin vestige --no-fail-fast` -- `cargo test -p vestige-e2e-tests --test mcp_protocol --no-fail-fast` -- `cargo check --workspace` -- `cargo metadata --format-version 1 --locked --no-deps` -- `pnpm --filter @vestige/dashboard check` -- `pnpm --filter @vestige/dashboard test` -- `pnpm --filter @vestige/dashboard build` -- `node --check packages/vestige-init/bin/init.js` -- `node --check packages/vestige-mcp-npm/scripts/postinstall.js` -- `node --check packages/vestige-mcp-npm/bin/vestige-restore.js` - -## [2.1.2] - 2026-05-01 — "Honest Memory" - -v2.1.2 focuses on operational trust: exact search stays exact, purge really removes content, contradictions are directly inspectable, and the update flow no longer depends on copied curl commands. - -### Added - -- **Concrete search mode** — `search` now auto-detects literal queries such as quoted strings, env vars, UUIDs, paths, and code identifiers. Those queries take a keyword/literal path that skips HyDE, semantic fusion, FSRS reweighting, retrieval competition, and spreading activation so exact matches land first. -- **Irreversible purge** — `memory(action="purge", confirm=true)` permanently removes memory content and embeddings, scrubs `insights.source_memories`, detaches temporal-summary children, prunes graph edges, and writes only a non-content `deletion_tombstones` row for sync/audit. -- **First-class contradictions tool** — new `contradictions` MCP tool scans a topic or recent memories for trust-weighted disagreements using the same local contradiction logic as `deep_reference`. -- **Simple update flow** — `vestige update` refreshes the installed binary and companion Sandwich files without requiring users to paste a curl installer. -- **Pro waitlist preview** — `/dashboard/waitlist` adds a local-only marketing surface for Solo Pro and Team Pro early-access signups. `VITE_WAITLIST_ENDPOINT` and `VITE_SUPPORT_BOT_ENDPOINT` are opt-in dashboard env vars, so no signup data is captured unless endpoints are configured. - -### Fixed - -- **Dream connection persistence cap** — dense single-domain dreams now persist every connection discovered in that run instead of losing everything beyond the old 1,000-entry live buffer. The live dreamer buffer now keeps up to 200,000 high-scoring recent connections, and the MCP `dream` tool exposes `min_similarity` for corpus-specific tuning. -- **Embedding-model upgrade repair** — `vestige consolidate` now re-embeds every missing or active-model-mismatched memory in one pass, so v1/v2 mixed stores are no longer left partially unreachable after only the first 100 legacy embeddings are regenerated. - -## [2.1.1] - 2026-05-01 — "Portable Sync" - -v2.1.1 focuses on user-controlled portability: exact storage archives, merge-safe file sync, pluggable sync backends, and explicit hook opt-ins. - -### Added - -- **Exact portable archives** — `vestige portable-export` / `vestige portable-import` preserve raw Vestige storage rows: memory IDs, FSRS state, graph edges, suppression state, audit rows, sessions, intentions, and embedding blobs. -- **Merge-safe imports** — `vestige portable-import --merge` can merge into non-empty databases. It applies `sync_tombstones`, keeps newer local memory rows on timestamp conflicts, preserves stable IDs, and rebuilds FTS after import. -- **File-backed two-way sync** — `vestige sync ` performs pull-merge-push through a shared portable archive. This works today with Dropbox, iCloud Drive, Syncthing, Git, network shares, and shared folders. -- **Pluggable portable-sync backend trait** — core now exposes `PortableSyncBackend`, `FilePortableSyncBackend`, and `PortableSyncReport`, so non-file backends can reuse the same merge semantics without reimplementing conflict handling. -- **Portable restore merge mode** — the MCP `restore` tool accepts `merge: true` for portable archives and returns inserted/updated/deleted/skipped/conflict counts. -- **Qwen3 embedding opt-in** — build-time and runtime support for Qwen3 embeddings, with model-aware retrieval safeguards so mixed embedding models are not compared in the same vector path. - -### Fixed - -- **Sanhedrin, preflight, and all Vestige Claude Code hooks are optional again.** The Cognitive Sandwich installer now activates no hooks by default and leaves every preflight hook, every Stop hook, the MLX launchd service, and the 19 GB Qwen model path behind explicit `--enable-preflight`, `--enable-sanhedrin`, or `--with-launchd` flags. -- **x86-friendly Sanhedrin path.** The verifier bridge now accepts any OpenAI-compatible chat endpoint via `VESTIGE_SANHEDRIN_ENDPOINT` and `VESTIGE_SANHEDRIN_MODEL`, so Linux and Intel Mac users can opt in without MLX or Apple Silicon. - -### Verified - -- `cargo test -p vestige-core portable --no-fail-fast` -- `cargo test -p vestige-mcp portable --no-fail-fast` -- `cargo test --workspace --no-fail-fast` -- Installer shell/Python/JSON validation and default/preflight/Sanhedrin migration dry-runs. - ## [2.1.0] - 2026-04-27 — "Cognitive Sandwich Goes Local" -v2.1.0 ships the Cognitive Sandwich hook harness for Claude Code, with a hotfix that makes every hook layer opt-in. The default installer stages files, removes old Vestige hook wiring, starts no MLX service, downloads no model, and makes no automatic model calls. Users can opt into preflight context, Sanhedrin verification, or the Apple Silicon MLX backend separately. +The Sanhedrin Executioner — Vestige's veto layer for Claude Code responses — can run against a local MLX model (`mlx-community/Qwen3.6-35B-A3B-4bit`) when explicitly enabled. Combined with four pre-cognitive UserPromptSubmit hooks (synthesis-preflight, cwd-state-injector, vestige-pulse-daemon, preflight-swarm), Vestige now ships a complete "Cognitive Sandwich" — Vestige memories injected before the model thinks, optional Sanhedrin veto after the model speaks. + +> 2026-05-01 hotfix: Sanhedrin, preflight, and all Vestige Claude Code hooks are optional by default. The default installer activates no hooks, makes no automatic model calls, removes old Vestige hook wiring from previous v2.1.0 installs, no longer starts MLX, and removes the old v2.1.0 MLX launchd job on reinstall. Users who want preflight can opt in with `--enable-preflight`; users who want Sanhedrin can opt in with `--enable-sanhedrin`; Apple Silicon local MLX autostart is a separate `--with-launchd` flag, and x86 users can point `--sanhedrin-endpoint` at any OpenAI-compatible `/v1/chat/completions` endpoint. ### Added - **`hooks/`** — first-class harness-side companion to the Vestige MCP server. 9 production hooks designed for `~/.claude/hooks/`: - - `sanhedrin.sh` — Stop hook that invokes the local Qwen Executioner via the Python bridge. - - `sanhedrin-local.py` — local backend that POSTs to `mlx_lm.server` (`localhost:8080`) with Vestige evidence injected via the dashboard `/api/deep_reference` HTTP endpoint. TRUST_FLOOR=0.55 evidence filter + topical-relevance gate + inference-verb ban + 8 worked few-shots covering true positives AND false-positive guards. + - `sanhedrin.sh` — optional Stop hook that invokes the Sanhedrin Executioner via the Python bridge only when `VESTIGE_SANHEDRIN_ENABLED=1`. + - `sanhedrin-local.py` — OpenAI-compatible backend that POSTs to the configured Sanhedrin endpoint with Vestige evidence injected via the dashboard `/api/deep_reference` HTTP endpoint. TRUST_FLOOR=0.55 evidence filter + topical-relevance gate + inference-verb ban + 8 worked few-shots covering true positives AND false-positive guards. - `synthesis-preflight.sh` — UserPromptSubmit hook that POSTs the user prompt to `/api/deep_reference` and injects the trust-scored reasoning chain into context. - `cwd-state-injector.sh` — captures git status, branch, modified files, open PRs/issues. - `vestige-pulse-daemon.sh` — surfaces fresh Vestige dream insights from the past 20 min. @@ -475,18 +23,21 @@ v2.1.0 ships the Cognitive Sandwich hook harness for Claude Code, with a hotfix - `synthesis-stop-validator.sh` — Stop hook regex against forbidden hedging patterns. - `veto-detector.sh` — fast 50ms regex pre-screen against `veto`-tagged Vestige memories. - `synthesis-gate.sh` — legacy v1 trigger (kept for backward compat). - - `settings.fragment.json` — empty default JSON snippet; the installer only wires hooks from the opt-in preflight/Sanhedrin fragments. + - `settings.fragment.json` — empty default fragment used to remove old Vestige hook wiring without enabling new hooks. + - `settings.preflight.fragment.json` — opt-in UserPromptSubmit hooks used only with `--enable-preflight`. + - `settings.sanhedrin.fragment.json` — opt-in JSON snippet used only with `--enable-sanhedrin`. - **Dashboard `/api/changelog` endpoint** — bounded REST event feed for recent `DreamCompleted` and `ConnectionDiscovered` events, used by the Pulse hook to inject fresh synthesis into Claude Code context. - **`agents/`** — `executioner.md` (legacy/fallback Haiku 4.5 path), `lateral-thinker.md`, `synthesis-composer.md`. -- **`launchd/com.vestige.mlx-server.plist.template`** — auto-start `mlx_lm.server` with the Qwen3.6-35B-A3B-4bit model on login. Templated with `__HOME__` and `__MODEL__` placeholders. -- **`scripts/install-sandwich.sh`** — one-command installer that stages hooks and agents, removes old Vestige hook wiring by default, and only wires optional layers with `--enable-preflight`, `--enable-sanhedrin`, or `--with-launchd`. Backs up `settings.json` to `.bak.pre-sandwich`. Supports `--force`, `--include-memory-loader`, and `--src=PATH`. -- **`scripts/check-sandwich-prereqs.sh`** — default verifier confirms no Vestige Claude Code hooks are wired. Optional `--preflight` and `--sanhedrin` modes check the corresponding enabled layer. +- **`launchd/com.vestige.mlx-server.plist.template`** — optional Apple Silicon helper that auto-starts `mlx_lm.server` with the Qwen3.6-35B-A3B-4bit model on login. Templated with `__HOME__` and `__MODEL__` placeholders. +- **`scripts/install-sandwich.sh`** — one-command installer that stages hooks, agents, jq-merges the settings fragment, and backs up `settings.json` to `.bak.pre-sandwich`. Supports `--force`, `--enable-sanhedrin`, `--with-launchd`, `--sanhedrin-endpoint`, `--sanhedrin-model`, `--include-memory-loader`, `--src=PATH`. +- **`scripts/check-sandwich-prereqs.sh`** — verifier that default installs have no Vestige hooks wired, with `--preflight` and `--sanhedrin` checks for optional layers. - **`docs/COGNITIVE_SANDWICH.md`** — architecture diagram, install guide, performance notes (82 tok/s on M3 Max), uninstall, configuration env vars. - **PR #48** — `VESTIGE_DATA_DIR` env-var support + tilde expansion + secure unix perms (thanks @Jelloeater) — directly addresses the ghost env-vars exposed by v2.0.9 cleanup. ### Changed -- **Sanhedrin Executioner backend can run locally or remotely when explicitly enabled.** The bridge targets an OpenAI-compatible chat endpoint, with local `mlx_lm.server` + Qwen3.6-35B-A3B-4bit available behind `--with-launchd` on Apple Silicon. Anthropic API key no longer required for the post-cognitive layer. The `executioner.md` agent definition is retained as manual/fallback only when invoked explicitly via `Task(subagent_type='executioner')`. +- **Sanhedrin, preflight, and all Vestige Claude Code hooks are optional by default.** Default installs run on x86 and low-memory machines without wiring any Vestige hook, calling Claude, downloading the 19 GB MLX model, or starting MLX. Reinstalling the default v2.1.0 hotfix removes the old Vestige hook wiring and the old mandatory `com.vestige.mlx-server` launchd job if they exist. +- **Sanhedrin Executioner backend swapped from Anthropic Haiku 4.5 → OpenAI-compatible endpoint, with local `mlx_lm.server` + Qwen3.6-35B-A3B-4bit as the Apple Silicon opt-in path.** Anthropic API key no longer required for the post-cognitive layer. The `executioner.md` agent definition is retained as manual/fallback only when invoked explicitly via `Task(subagent_type='executioner')`. - **All hooks sanitized for public release** — replaced hardcoded personal absolute paths with `$HOME` / `$VESTIGE_*` env vars; removed personal regex tokens. - **NPM binary installer now follows package version** — `vestige-mcp-server@2.1.0` downloads release assets from `v2.1.0` instead of a stale hardcoded binary tag, while local workspace installs skip the release-asset download before the tag exists. @@ -494,23 +45,29 @@ v2.1.0 ships the Cognitive Sandwich hook harness for Claude Code, with a hotfix - `cargo test --workspace --release --no-fail-fast`: **1,229 passing, 0 failed** (366 vestige-core + 358 vestige-mcp lib + 4 vestige-mcp bin + 497 e2e + 4 doctests). - Sanhedrin bridge smoke checks: Python bytecode compilation passes, fail-open bridge invocation returns `yes`, and public hook settings validate as JSON. -- v2.1.0 hotfix installer matrix: default, preflight-only, Sanhedrin-only, full sandwich, legacy-all-hooks migration, and unrelated custom hooks preservation. - 8-day Sandwich dogfood: **84% pass rate, 16% legitimate vetoes** caught real hallucinations. +- 2026-05-01 hotfix checks: `cargo test --workspace --no-fail-fast`, `cargo build --release --workspace`, shell/Python/JSON validation, and default/preflight/Sanhedrin installer dry-runs all pass. ### Closes - #36 (Agent Hooks for Low-Effort Automatic Memory Capture) — Cognitive Sandwich is the answer. -### Prerequisites for optional local MLX Sanhedrin +### Prerequisites for the Cognitive Sandwich -- macOS Apple Silicon (M1+) — required only for `--with-launchd` MLX autostart - Python 3.10+ +- `jq` +- `vestige-mcp` +- Claude Code + +Optional local MLX Sanhedrin backend: + +- macOS Apple Silicon (M1+) — required for the launchd MLX helper only - ~22 GB free RAM (Qwen3.6-35B-A3B-4bit at runtime) - First-run model download: ~19 GB from Hugging Face (cached locally thereafter) ### Migration -None required for existing Vestige users. The Cognitive Sandwich is opt-in via `scripts/install-sandwich.sh`; running the default installer removes old Vestige hook wiring and leaves preflight, Sanhedrin, launchd, and the 19 GB model path disabled. The MCP server, schema, and tool surface are bit-identical to v2.0.9. +None required for existing Vestige users. The default installer now removes old v2.1.0 Claude Code hook wiring. The Cognitive Sandwich hook layers are opt-in via `--enable-preflight`, `--enable-sanhedrin`, or `--enable-sandwich`. The MCP server, schema, and tool surface are bit-identical to v2.0.9. --- diff --git a/CLAUDE.md b/CLAUDE.md index 7388599..0684836 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,89 +1,362 @@ -# 🚀 MANDATE #0 — MAXIMUM AMBITION. NO HEDGING. (read this first, every time) +# 🚫 READ FIRST EVERY PROMPT — NO HEDGING -This is the standing default for ALL work on Vestige, before anything else below. +Sam's April 20 2026 correction: *"you NEVER LISTEN TO YOUR RULES, WHY ARE YOU ALWAYS BREAKING THE HEDGING/PESSIMISTIC RULE."* -- **Assume maximum ambition.** Build the most exhaustive, jaw-dropping, correct - version — not the safe "good enough" one. The ceiling is "nobody else has - this," not "this works." Default to the winning move, not the cautious one. -- **Scour before you settle.** When unsure or stuck, research the real frontier - (web, source, the actual running app) before acting. Never accept the first - adequate answer when a deeper one exists. Verify, don't assume. -- **No hedging.** Lead with the best path and a clear recommendation. Forbidden: - "this is probably too risky," "temper expectations," "good enough," "maybe try - the easier one." Risks get their own honest section — never used to shrink the - target. -- **Show proof.** Verify changes in the real running app and share the evidence - (screenshots, test output, gate results) — don't claim done without it. -- **Protect what's flawless, detonate what isn't.** Treat finished, loved work as - load-bearing (don't break it); push everything else past where any other dev - would stop. +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. -Origin: Sam, Jun 22 2026 — the overnight session that turned the dashboard + -Memory Cinema from "alive" into a category-of-one particle journey. The depth -only happened because the bar was set to maximum. Make that the default, not the -exception. +**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 Agent Guidance +# Vestige v2.0.4 — Cognitive Memory & Reasoning System -This file is intentionally safe for the public repository. It gives coding -agents project-specific context without relying on private local files, -personal operating notes, or mandatory background hooks. +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.** -## Project Shape +**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. -Vestige is a local-first MCP memory server written in Rust, with a SvelteKit -dashboard embedded into the release binary. The core product promise is: +--- -- user-owned memory stored locally by default -- MCP-native integration with coding agents -- retrieval and memory lifecycle behavior informed by cognitive science -- explicit tools for search, review, suppression, purge, graph exploration, - contradiction inspection, and maintenance +## Session Start Protocol -## Working Rules +Every conversation, before responding to the user: -- Prefer source evidence over memory. Use `rg`, tests, and nearby code before - making claims about behavior. -- Keep release changes scoped. Do not rewrite unrelated modules during a - version/tag cleanup unless the release gate requires it. -- Preserve local-first behavior. Heavy models, Sanhedrin-style verifier hooks, - and preflight automation must remain optional. -- Treat deletion semantics carefully. `purge` must remove content and - embeddings, while retaining only content-free audit tombstones. -- Treat exact lookup semantics carefully. Env vars, paths, UUIDs, quoted - strings, and code identifiers should not be distorted by semantic expansion. - -## Common Checks - -Run the narrowest check that covers the change, then run the release gates -before tagging: - -```sh -cargo test --workspace --no-fail-fast -cargo clippy --workspace -- -D warnings -pnpm --filter @vestige/dashboard check -pnpm --filter @vestige/dashboard build +``` +session_context({ + queries: ["user preferences", "[current project] context"], + context: { codebase: "[project]", topics: ["[current topics]"] }, + token_budget: 2000 +}) ``` -For documentation-only changes, at minimum run: +Then check `automationTriggers` from response: +- `needsDream` → call `dream` (consolidates memories, discovers hidden connections) +- `needsBackup` → call `backup` +- `needsGc` → call `gc(dry_run: true)` then review +- totalMemories > 700 → call `find_duplicates` -```sh -git diff --check +Say "Remembering..." then retrieve context before answering. + +> **Fallback:** If `session_context` unavailable: `search` × 2 → `intention` check → `system_status` → `predict`. + +--- + +## Complete Tool Reference (23 Tools) + +### session_context — One-Call Initialization +``` +session_context({ + queries: ["user preferences", "project context"], // search queries + context: { codebase: "project-name", topics: ["svelte", "rust"], file: "src/main.rs" }, + token_budget: 2000, // 100-100000, controls response size + include_status: true, // system health + include_intentions: true, // triggered reminders + include_predictions: true // proactive memory predictions +}) +``` +Returns: markdown context + `automationTriggers` + `expandable` IDs for on-demand retrieval. + +### smart_ingest — Save Anything +**Single mode** — auto-decides CREATE/UPDATE/SUPERSEDE via Prediction Error Gating: +``` +smart_ingest({ + content: "What to remember", + tags: ["tag1", "tag2"], + node_type: "fact", // fact|concept|event|person|place|note|pattern|decision + source: "optional reference", + forceCreate: false // bypass dedup when needed +}) +``` +**Batch mode** — save up to 20 items in one call (session end, pre-compaction): +``` +smart_ingest({ + items: [ + { content: "Item 1", tags: ["session-end"], node_type: "fact" }, + { content: "Item 2", tags: ["bug-fix"], node_type: "fact" } + ] +}) +``` +Each item runs the full cognitive pipeline: importance scoring → intent detection → synaptic tagging → hippocampal indexing → PE gating → cross-project recording. + +### search — 7-Stage Cognitive Search +``` +search({ + query: "search query", + limit: 10, // 1-100 + min_retention: 0.0, // filter by retention strength + min_similarity: 0.5, // minimum cosine similarity + detail_level: "summary", // brief|summary|full + context_topics: ["rust", "debugging"], // boost topic-matching memories + token_budget: 3000, // 100-100000, truncate to fit + retrieval_mode: "balanced" // precise|balanced|exhaustive (v2.1) +}) +``` +Retrieval modes: `precise` (fast, no activation/competition), `balanced` (default 7-stage pipeline), `exhaustive` (5x overfetch, deep graph traversal, no competition suppression). + +Pipeline: Overfetch → Rerank (cross-encoder) → Temporal boost → Accessibility filter (FSRS-6) → Context match (Tulving 1973) → Competition (Anderson 1994) → Spreading activation. **Every search strengthens the memories it finds (Testing Effect).** + +### memory — Read, Edit, Delete, Promote, Demote +``` +memory({ action: "get", id: "uuid" }) // full node with all FSRS state +memory({ action: "edit", id: "uuid", content: "updated text" }) // preserves FSRS state, regenerates embedding +memory({ action: "delete", id: "uuid" }) +memory({ action: "promote", id: "uuid", reason: "was helpful" }) // +0.20 retrieval, +0.10 retention, 1.5x stability +memory({ action: "demote", id: "uuid", reason: "was wrong" }) // -0.30 retrieval, -0.15 retention, 0.5x stability +memory({ action: "state", id: "uuid" }) // Active/Dormant/Silent/Unavailable + accessibility score +memory({ action: "get_batch", ids: ["uuid1", "uuid2", "uuid3"] }) // retrieve up to 20 full memories at once (v2.1) +``` +Promote/demote does NOT delete — it adjusts ranking. Demoted memories rank lower; alternatives surface instead. +`get_batch` is designed for batch retrieval of expandable overflow IDs from search/session_context. + +### codebase — Code Patterns & Architectural Decisions +``` +codebase({ action: "remember_pattern", name: "Pattern Name", + description: "How it works and when to use it", + files: ["src/file.rs"], codebase: "project-name" }) + +codebase({ action: "remember_decision", decision: "What was decided", + rationale: "Why", alternatives: ["Option A", "Option B"], + files: ["src/file.rs"], codebase: "project-name" }) + +codebase({ action: "get_context", codebase: "project-name", limit: 10 }) +// Returns: patterns, decisions, cross-project insights ``` -## Documentation +### intention — Prospective Memory (Reminders) +``` +intention({ action: "set", description: "What to do", + trigger: { type: "context", topic: "authentication" }, // fires when discussing auth + priority: "high" }) -- User setup: `README.md` -- Claude-specific templates: `docs/CLAUDE-SETUP.md` -- Storage and sync behavior: `docs/STORAGE.md` -- Cognitive Sandwich and optional verifier hooks: `docs/COGNITIVE_SANDWICH.md` -- Release history: `CHANGELOG.md` +intention({ action: "set", description: "Deploy by Friday", + trigger: { type: "time", at: "2026-03-07T17:00:00Z" }, + deadline: "2026-03-07T17:00:00Z" }) -## Public-Repo Hygiene +intention({ action: "set", description: "Check test coverage", + trigger: { type: "context", codebase: "vestige", file_pattern: "*.test.*" } }) -Do not commit private absolute paths, local agent memory paths, unpublished -planning files, real credentials, personal operating notes, or private repo -locations. Example environment variables in docs must be empty placeholders or -obviously fake examples. +intention({ action: "check", context: { codebase: "vestige", topics: ["testing"] } }) +intention({ action: "update", id: "uuid", status: "complete" }) +intention({ action: "list", filter_status: "active" }) +``` + +### dream — Memory Consolidation +``` +dream({ memory_count: 50 }) +``` +5-stage cycle: Replay → Cross-reference → Strengthen → Prune → Transfer. Uses Waking SWR tagging (70% tagged + 30% random for diversity). Discovers hidden connections, generates insights, persists new edges to the activation network. + +### explore_connections — Graph Traversal +``` +explore_connections({ action: "associations", from: "uuid", limit: 10 }) +// Spreading activation from a memory — find related memories via graph traversal + +explore_connections({ action: "chain", from: "uuid-A", to: "uuid-B" }) +// Build reasoning path between two memories (A*-like pathfinding) + +explore_connections({ action: "bridges", from: "uuid-A", to: "uuid-B" }) +// Find connecting memories that bridge two concepts +``` + +### predict — Proactive Retrieval +``` +predict({ context: { codebase: "vestige", current_file: "src/main.rs", + current_topics: ["error handling", "rust"] } }) +``` +Returns: predictions with confidence, suggestions, speculative retrievals, top interests. Uses SpeculativeRetriever's learned patterns from access history. + +### importance_score — Should I Save This? +``` +importance_score({ content: "Content to evaluate", + context_topics: ["debugging"], project: "vestige" }) +``` +4-channel model: novelty (0.25), arousal (0.30), reward (0.25), attention (0.20). Composite > 0.6 = save it. + +### find_duplicates — Dedup Memory +``` +find_duplicates({ similarity_threshold: 0.80, limit: 20, tags: ["bug-fix"] }) +``` +Cosine similarity clustering. Returns merge/review suggestions. + +### memory_timeline — Chronological Browse +``` +memory_timeline({ start: "2026-02-01", end: "2026-03-01", + node_type: "decision", tags: ["vestige"], limit: 50, detail_level: "summary" }) +``` + +### memory_changelog — Audit Trail +``` +memory_changelog({ memory_id: "uuid", limit: 20 }) // per-memory history +memory_changelog({ start: "2026-03-01", limit: 20 }) // system-wide +``` + +### memory_health — Retention Dashboard +``` +memory_health() +``` +Returns: avg retention, distribution buckets (0-20%, 20-40%, etc.), trend (improving/declining/stable), recommendation. + +### memory_graph — Visualization Export +``` +memory_graph({ query: "search term", depth: 2, max_nodes: 50 }) +memory_graph({ center_id: "uuid", depth: 3, max_nodes: 100 }) +``` +Returns nodes with force-directed positions + edges with weights. + +### deep_reference — Cognitive Reasoning Engine (v2.0.4) ★ USE THIS FOR ALL FACTUAL QUESTIONS +``` +deep_reference({ query: "What port does the dev server use?" }) +deep_reference({ query: "Should I use prefix caching with vLLM?", depth: 30 }) +``` +**THE killer tool.** 8-stage cognitive reasoning pipeline: +1. Broad retrieval + cross-encoder reranking +2. Spreading activation expansion (finds connected memories search misses) +3. FSRS-6 trust scoring (retention × stability × reps ÷ lapses) +4. Intent classification (FactCheck / Timeline / RootCause / Comparison / Synthesis) +5. Temporal supersession (newer high-trust replaces older) +6. Trust-weighted contradiction analysis (only flags conflicts between strong memories) +7. Relation assessment (Supports / Contradicts / Supersedes / Irrelevant per pair) +8. **Template reasoning chain** — pre-built natural language reasoning the AI validates + +Parameters: `query` (required), `depth` (5-50, default 20). + +Returns: `intent`, `reasoning` (THE KEY FIELD — read this first), `recommended` (highest-trust answer), `evidence` (trust-sorted), `contradictions`, `superseded`, `evolution`, `related_insights`, `confidence`. + +`cross_reference` is a backward-compatible alias that calls `deep_reference`. + +### Maintenance Tools +``` +system_status() // health + stats + warnings + recommendations +consolidate() // FSRS-6 decay cycle + embedding generation +backup() // SQLite backup → ~/.vestige/backups/ +export({ format: "json", tags: ["bug-fix"], since: "2026-01-01" }) +gc({ min_retention: 0.1, dry_run: true }) // garbage collect (dry_run first!) +restore({ path: "/path/to/backup.json" }) +``` + +--- + +## Mandatory Save Gates + +**You MUST NOT proceed past a save gate without executing the save.** + +| Gate | Trigger | Action | +|------|---------|--------| +| **BUG_FIX** | After any error is resolved | `smart_ingest({ content: "BUG FIX: [error]\nRoot cause: [why]\nSolution: [fix]\nFiles: [paths]", tags: ["bug-fix", "project"], node_type: "fact" })` | +| **DECISION** | After any architectural/design choice | `codebase({ action: "remember_decision", decision, rationale, alternatives, files, codebase })` | +| **CODE_CHANGE** | After >20 lines or new pattern | `codebase({ action: "remember_pattern", name, description, files, codebase })` | +| **SESSION_END** | Before stopping or compaction | `smart_ingest({ items: [{ content: "SESSION: [summary]", tags: ["session-end"] }] })` | + +--- + +## Trigger Words — Auto-Save + +| User Says | Action | +|-----------|--------| +| "Remember this" / "Don't forget" | `smart_ingest` immediately | +| "I always..." / "I never..." / "I prefer..." | Save as preference | +| "This is important" | `smart_ingest` + `memory(action="promote")` | +| "Remind me..." / "Next time..." | `intention({ action: "set" })` | + +--- + +## Cognitive Architecture + +### Search Pipeline (7 stages) +1. **Overfetch** — 3x results from hybrid search (0.3 BM25 + 0.7 semantic, nomic-embed-text-v1.5 768D) +2. **Rerank** — Cross-encoder rescoring (Jina Reranker v1 Turbo, 38M params) +3. **Temporal** — Recency + validity window boosting (85% relevance + 15% temporal) +4. **Accessibility** — FSRS-6 retention filter (Active ≥0.7, Dormant ≥0.4, Silent ≥0.1) +5. **Context** — Tulving 1973 encoding specificity (topic overlap → +30% boost) +6. **Competition** — Anderson 1994 retrieval-induced forgetting (winners strengthen, competitors weaken) +7. **Activation** — Spreading activation side effects + predictive model + reconsolidation marking + +### Ingest Pipeline +**Pre:** 4-channel importance scoring (novelty/arousal/reward/attention) + intent detection → auto-tag +**Store:** Prediction Error Gating: similarity >0.92 → UPDATE, 0.75-0.92 → UPDATE/SUPERSEDE, <0.75 → CREATE +**Post:** Synaptic tagging (Frey & Morris 1997, 9h backward + 2h forward) + hippocampal indexing + cross-project recording + +### FSRS-6 (State-of-the-Art Spaced Repetition) +- Retrievability: `R = (1 + factor × t / S)^(-w20)` — 21 trained parameters +- Dual-strength model (Bjork & Bjork 1992): storage strength (grows) + retrieval strength (decays) +- Accessibility = retention×0.5 + retrieval×0.3 + storage×0.2 +- 20-30% more efficient than SM-2 (Anki) + +### 29 Cognitive Modules (stateful, persist across calls) + +**Neuroscience (16):** +ActivationNetwork (Collins & Loftus 1975), SynapticTaggingSystem (Frey & Morris 1997), HippocampalIndex (Teyler & Rudy 2007), ContextMatcher (Tulving 1973), AccessibilityCalculator, CompetitionManager (Anderson 1994), StateUpdateService, ImportanceSignals, NoveltySignal, ArousalSignal, RewardSignal, AttentionSignal, EmotionalMemory (Brown & Kulik 1977), PredictiveMemory, ProspectiveMemory, IntentionParser + +**Advanced (11):** +ImportanceTracker, ReconsolidationManager (Nader — 5min labile window), IntentDetector (9 intent types), ActivityTracker, MemoryDreamer (5-stage consolidation), MemoryChainBuilder (A*-like), MemoryCompressor (30-day min age), CrossProjectLearner (6 pattern types), AdaptiveEmbedder, SpeculativeRetriever (6 trigger types), ConsolidationScheduler + +**Search (2):** Reranker, TemporalSearcher + +### Memory States +- **Active** (retention ≥ 0.7) — easily retrievable +- **Dormant** (≥ 0.4) — retrievable with effort +- **Silent** (≥ 0.1) — difficult, needs cues +- **Unavailable** (< 0.1) — needs reinforcement + +### Connection Types +semantic, temporal, causal, spatial, part_of, user_defined — each with strength (0-1), activation_count, timestamps + +--- + +## Advanced Techniques + +### Cross-Project Intelligence +The CrossProjectLearner tracks patterns across ALL projects (ErrorHandling, AsyncConcurrency, Testing, Architecture, Performance, Security). When you learn a pattern in one project that works, it becomes available in all projects. Use `codebase({ action: "get_context" })` without a codebase filter to get universal patterns. + +### Reconsolidation Window +After any memory is accessed (via search, get, or promote), it enters a 5-minute "labile" state where modifications are enhanced. This is the optimal time to edit memories with new context. The system handles this automatically. + +### Synaptic Tagging (Retroactive Importance) +Memories encoded in the last 9 hours can be retroactively promoted when something important happens. If you fix a critical bug, not only does the fix get saved — related memories from the past 9 hours also get importance boosts. The SynapticTaggingSystem handles this automatically. + +### Dream Insights +Dreams don't just consolidate — they generate new insights by cross-referencing recent memories with older knowledge. The insights can reveal: contradictions between memories, previously unseen patterns, connections across different projects. Always check dream results for `insights_generated`. + +### Token Budget Strategy +Use `token_budget` on search and session_context to control response size. For quick lookups: 500. For deep context: 3000-5000. Results that don't fit go to `expandable` — retrieve them with `memory({ action: "get", id: "..." })`. + +### Detail Levels +- `brief` — id/type/tags/score only (1-2 tokens per result, good for scanning) +- `summary` — 8 fields including content preview (default, balanced) +- `full` — all FSRS state, timestamps, embedding info (for debugging/analysis) + +--- + +## Memory Hygiene + +**Promote** when user confirms helpful, solution worked, info was accurate. +**Demote** when user corrects mistake, info was wrong, led to bad outcome. +**Never save:** secrets, API keys, passwords, temporary debugging state, trivial info. + +--- + +## The One Rule + +**When in doubt, save. The cost of a duplicate is near zero (Prediction Error Gating handles dedup). The cost of lost knowledge is permanent.** + +Memory is retrieval. Searching strengthens memory. Search liberally, save aggressively. + +--- + +## Development + +- **Crate:** `vestige-mcp` v2.0.4, Rust 2024 edition, MSRV 1.91 +- **Tests:** 758 (406 mcp + 352 core), zero warnings +- **Build:** `cargo build --release -p vestige-mcp` (features: `embeddings` + `vector-search`) +- **Build (no embeddings):** `cargo build --release -p vestige-mcp --no-default-features` +- **Bench:** `cargo bench -p vestige-core` +- **Architecture:** `McpServer` → `Arc` + `Arc>` +- **Storage:** SQLite WAL mode, `Mutex` reader/writer split, FTS5 full-text search +- **Embeddings:** nomic-embed-text-v1.5 (768D, 8K context) via fastembed (local ONNX, no API) +- **Vector index:** USearch HNSW (20x faster than FAISS) +- **Binaries:** `vestige-mcp` (MCP server), `vestige` (CLI), `vestige-restore` +- **Dashboard:** SvelteKit 2 + Svelte 5 + Three.js + Tailwind 4, embedded at `/dashboard` +- **Env vars:** `VESTIGE_DASHBOARD_PORT` (default 3927), `VESTIGE_HTTP_PORT` (default 3928), `VESTIGE_HTTP_BIND` (default 127.0.0.1), `VESTIGE_AUTH_TOKEN` (auto-generated), `VESTIGE_CONSOLIDATION_INTERVAL_HOURS` (default 6), `RUST_LOG` diff --git a/CLAUDE.md.template b/CLAUDE.md.template index e007ea6..fefd005 100644 --- a/CLAUDE.md.template +++ b/CLAUDE.md.template @@ -88,7 +88,7 @@ Tags: ["decision", "topic-name"] | "Don't forget" | `smart_ingest` with tags: ["important"] | | "I always..." / "I never..." | Save as preference | | "I prefer..." / "I like..." | Save as preference | -| "This is important" | `smart_ingest` + `memory(action="promote")` | +| "This is important" | `smart_ingest` + `promote_memory` | | "Remind me..." | Create `intention` with trigger | | "Next time we..." | Create `intention` with context trigger | | "When I'm working on X..." | Create `intention` with codebase trigger | @@ -115,7 +115,7 @@ Act on feedback immediately — don't ask permission to promote/demote. ### Proactive Health Checks If you notice degraded recall or a user mentions memory issues: -1. Run `system_status` — check overall system status +1. Run `health_check` — check overall system status 2. If `averageRetention < 0.5` → suggest running `consolidate` 3. If `dueForReview > 50` → mention that some memories need review diff --git a/Cargo.lock b/Cargo.lock index 90d70a0..2e15454 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,16 +8,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common", - "generic-array", -] - [[package]] name = "ahash" version = "0.8.12" @@ -153,24 +143,6 @@ dependencies = [ "syn", ] -[[package]] -name = "argon2" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" -dependencies = [ - "base64ct", - "blake2", - "cpufeatures 0.2.17", - "password-hash", -] - -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - [[package]] name = "arrayvec" version = "0.7.6" @@ -339,29 +311,6 @@ dependencies = [ "core2", ] -[[package]] -name = "blake2" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" -dependencies = [ - "digest", -] - -[[package]] -name = "blake3" -version = "1.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d2d5991425dfd0785aed03aedcf0b321d61975c9b5b3689c774a2610ae0b51e" -dependencies = [ - "arrayref", - "arrayvec", - "cc", - "cfg-if", - "constant_time_eq", - "cpufeatures 0.3.0", -] - [[package]] name = "block" version = "0.1.6" @@ -443,10 +392,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bd9895436c1ba5dc1037a19935d084b838db066ff4e15ef7dded020b7c12a4a" dependencies = [ "byteorder", - "candle-kernels", "candle-metal-kernels", "candle-ug", - "cudarc 0.19.7", "float8", "gemm 0.19.0", "half", @@ -466,15 +413,6 @@ dependencies = [ "zip", ] -[[package]] -name = "candle-kernels" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "742e2ac226b777134436e9e692f44e77c278b8a7abb1554dc10e44dc911b349f" -dependencies = [ - "cudaforge", -] - [[package]] name = "candle-metal-kernels" version = "0.10.2" @@ -515,7 +453,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca0fc3167cbc99c8ec1be618cb620aa21dca95038f118c3579a79370e3dc5f77" dependencies = [ "ug", - "ug-cuda", "ug-metal", ] @@ -552,36 +489,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "chacha20" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures 0.2.17", -] - -[[package]] -name = "chacha20poly1305" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" -dependencies = [ - "aead", - "chacha20", - "cipher", - "poly1305", - "zeroize", -] - [[package]] name = "chrono" version = "0.4.44" @@ -623,17 +530,6 @@ dependencies = [ "half", ] -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", - "zeroize", -] - [[package]] name = "clap" version = "4.6.0" @@ -734,12 +630,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "constant_time_eq" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" - [[package]] name = "core-foundation" version = "0.9.4" @@ -795,15 +685,6 @@ dependencies = [ "libc", ] -[[package]] -name = "cpufeatures" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" -dependencies = [ - "libc", -] - [[package]] name = "crc32fast" version = "1.5.0" @@ -887,50 +768,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", - "rand_core 0.6.4", "typenum", ] -[[package]] -name = "cudaforge" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f7a0d45b139b5beeeb1c34188717e12241c44a0120afb498815ce7f5373c691" -dependencies = [ - "anyhow", - "fs2", - "glob", - "num_cpus", - "rayon", - "serde", - "serde_json", - "sha2", - "thiserror 2.0.18", - "walkdir", - "which", -] - -[[package]] -name = "cudarc" -version = "0.17.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf99ab37ee7072d64d906aa2dada9a3422f1d975cdf8c8055a573bc84897ed8" -dependencies = [ - "half", - "libloading 0.8.9", -] - -[[package]] -name = "cudarc" -version = "0.19.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cea5f10a99e025c1b44ae2354c2d8326b25ddbd0baf76bde8e55cfd4018a2cc" -dependencies = [ - "float8", - "half", - "libloading 0.9.0", -] - [[package]] name = "cxx" version = "1.0.194" @@ -1092,7 +932,6 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", - "subtle", ] [[package]] @@ -1195,12 +1034,6 @@ dependencies = [ "syn", ] -[[package]] -name = "env_home" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" - [[package]] name = "equator" version = "0.4.2" @@ -1421,16 +1254,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fs2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "fsevent-sys" version = "4.1.0" @@ -1447,7 +1270,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", - "futures-sink", ] [[package]] @@ -1756,10 +1578,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi", - "wasm-bindgen", ] [[package]] @@ -1769,11 +1589,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi 5.3.0", "wasip2", - "wasm-bindgen", ] [[package]] @@ -1814,12 +1632,6 @@ dependencies = [ "url", ] -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - [[package]] name = "h2" version = "0.4.13" @@ -2011,7 +1823,6 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.6", ] [[package]] @@ -2298,15 +2109,6 @@ dependencies = [ "libc", ] -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "generic-array", -] - [[package]] name = "interpolate_name" version = "0.2.4" @@ -2406,10 +2208,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -2608,12 +2412,6 @@ dependencies = [ "hashbrown 0.16.1", ] -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - [[package]] name = "lzma-rust2" version = "0.15.7" @@ -3090,12 +2888,6 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - [[package]] name = "open" version = "5.3.3" @@ -3221,17 +3013,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "password-hash" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" -dependencies = [ - "base64ct", - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "paste" version = "1.0.15" @@ -3318,17 +3099,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "poly1305" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" -dependencies = [ - "cpufeatures 0.2.17", - "opaque-debug", - "universal-hash", -] - [[package]] name = "portable-atomic" version = "1.13.1" @@ -3337,9 +3107,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.7" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" dependencies = [ "portable-atomic", ] @@ -3458,61 +3228,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror 2.0.18", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" -dependencies = [ - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.18", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.60.2", -] - [[package]] name = "quote" version = "1.0.45" @@ -3541,7 +3256,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ec095654a25171c2124e9e3393a930bddbffdc939556c914957a4c3e0a87166" dependencies = [ "rand_chacha", - "rand_core 0.9.5", + "rand_core", ] [[package]] @@ -3551,16 +3266,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", + "rand_core", ] [[package]] @@ -3742,7 +3448,6 @@ dependencies = [ "base64 0.22.1", "bytes", "encoding_rs", - "futures-channel", "futures-core", "futures-util", "h2", @@ -3759,8 +3464,6 @@ dependencies = [ "native-tls", "percent-encoding", "pin-project-lite", - "quinn", - "rustls", "rustls-pki-types", "serde", "serde_json", @@ -3768,7 +3471,6 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-native-tls", - "tokio-rustls", "tokio-util", "tower", "tower-http", @@ -3778,7 +3480,6 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 1.0.6", ] [[package]] @@ -3828,12 +3529,6 @@ dependencies = [ "sqlite-wasm-rs", ] -[[package]] -name = "rustc-hash" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" - [[package]] name = "rustix" version = "1.1.4" @@ -3868,7 +3563,6 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ - "web-time", "zeroize", ] @@ -4054,18 +3748,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "digest", ] @@ -4361,21 +4044,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "tinyvec" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tokenizers" version = "0.22.2" @@ -4603,17 +4271,6 @@ dependencies = [ "tracing-serde", ] -[[package]] -name = "trait-variant" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "try-lock" version = "0.2.5" @@ -4670,19 +4327,6 @@ dependencies = [ "yoke 0.7.5", ] -[[package]] -name = "ug-cuda" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f0a1fa748f26166778c33b8498255ebb7c6bffb472bcc0a72839e07ebb1d9b5" -dependencies = [ - "cudarc 0.17.8", - "half", - "serde", - "thiserror 1.0.69", - "ug", -] - [[package]] name = "ug-metal" version = "0.5.0" @@ -4742,16 +4386,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" -[[package]] -name = "universal-hash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" -dependencies = [ - "crypto-common", - "subtle", -] - [[package]] name = "untrusted" version = "0.9.0" @@ -4897,12 +4531,8 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vestige-core" -version = "2.2.1" +version = "2.1.0" dependencies = [ - "argon2", - "blake3", - "candle-core", - "chacha20poly1305", "chrono", "criterion", "directories", @@ -4910,7 +4540,6 @@ dependencies = [ "git2", "lru", "notify", - "reqwest", "rusqlite", "serde", "serde_json", @@ -4918,7 +4547,6 @@ dependencies = [ "thiserror 2.0.18", "tokio", "tracing", - "trait-variant", "usearch", "uuid", ] @@ -4938,7 +4566,7 @@ dependencies = [ [[package]] name = "vestige-mcp" -version = "2.2.1" +version = "2.1.0" dependencies = [ "anyhow", "axum", @@ -4965,19 +4593,6 @@ dependencies = [ "vestige-core", ] -[[package]] -name = "vestige-phase-1-tests" -version = "0.0.1" -dependencies = [ - "chrono", - "rusqlite", - "serde_json", - "tempfile", - "tokio", - "uuid", - "vestige-core", -] - [[package]] name = "walkdir" version = "2.5.0" @@ -5023,9 +4638,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", @@ -5036,23 +4651,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.64" +version = "0.4.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5060,9 +4671,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ "bumpalo", "proc-macro2", @@ -5073,9 +4684,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] @@ -5129,9 +4740,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.91" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" dependencies = [ "js-sys", "wasm-bindgen", @@ -5180,18 +4791,6 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" -[[package]] -name = "which" -version = "7.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762" -dependencies = [ - "either", - "env_home", - "rustix", - "winsafe", -] - [[package]] name = "winapi" version = "0.3.9" @@ -5458,12 +5057,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" -[[package]] -name = "winsafe" -version = "0.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" - [[package]] name = "wit-bindgen" version = "0.51.0" diff --git a/Cargo.toml b/Cargo.toml index 9dffd26..40cd33f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,14 +4,13 @@ members = [ "crates/vestige-core", "crates/vestige-mcp", "tests/e2e", - "tests/phase_1", ] exclude = [ "fastembed-rs", ] [workspace.package] -version = "2.2.1" +version = "2.1.0" edition = "2024" license = "AGPL-3.0-only" repository = "https://github.com/samvallad33/vestige" diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index e93e93c..0000000 --- a/Dockerfile +++ /dev/null @@ -1,28 +0,0 @@ -# Dockerfile for running the Vestige MCP server in an isolated sandbox. -# -# Used by registries such as Glama to start the server and run the standard -# MCP stdio introspection exchange (tools/list, resources/list, prompts/list). -# The server speaks MCP over stdio, which is exactly what these tools expect. -# -# Base must be glibc (Debian), not musl/Alpine: the npm postinstall downloads -# the prebuilt x86_64-unknown-linux-gnu Rust binary from the GitHub release, and -# a -gnu binary will not run on an Alpine/musl image. - -FROM node:20-slim - -# ca-certificates lets the postinstall fetch the release asset over HTTPS. -RUN apt-get update \ - && apt-get install -y --no-install-recommends ca-certificates \ - && rm -rf /var/lib/apt/lists/* - -# Install the published package globally. Its postinstall downloads the matching -# prebuilt vestige-mcp binary for linux/x64 from the GitHub release. -RUN npm install -g vestige-mcp-server@latest - -# Keep all memory data inside the container under a writable path. -ENV VESTIGE_DATA_DIR=/data -RUN mkdir -p /data - -# Start the MCP server on stdio. The `vestige-mcp` bin execs the native binary -# and inherits stdio, so the MCP client talks to it directly. -ENTRYPOINT ["vestige-mcp"] diff --git a/README.md b/README.md index bca1ffc..f3f1a83 100644 --- a/README.md +++ b/README.md @@ -1,307 +1,469 @@
-

Vestige

+# Vestige -### Your bug was born days before it crashed. You just can't remember where. +### The cognitive engine that gives AI agents a brain. -Vestige is a local-first memory for AI agents that reaches backward through time to find the quiet change that caused today's failure: the cause that looks nothing like the bug. One 23MB Rust binary. No cloud. Your data never leaves your machine. +[![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-1229%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) -[![GitHub stars](https://img.shields.io/github/stars/samvallad33/vestige?style=for-the-badge&logo=github&color=8b5cf6)](https://github.com/samvallad33/vestige/stargazers) -[![Release](https://img.shields.io/github/v/release/samvallad33/vestige?style=for-the-badge&color=06b6d4)](https://github.com/samvallad33/vestige/releases/latest) -[![Tests](https://img.shields.io/badge/tests-1550_passing-22c55e?style=for-the-badge)](https://github.com/samvallad33/vestige/actions) -[![License](https://img.shields.io/badge/license-AGPL--3.0-3b82f6?style=for-the-badge)](LICENSE) +**Your Agent forgets everything between sessions. Vestige fixes that.** -[**⚡ Quick Start**](#-get-it-running-in-60-seconds) · [**🧠 The Idea**](#-why-i-built-this) · [**🔬 The Science**](#-this-is-real-neuroscience-not-a-metaphor) · [**🛠 13 Tools**](#-13-tools-one-brain) · [**📊 Dashboard**](#-watch-your-ai-think-in-3d) +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](#-24-mcp-tools) | [Docs](docs/)
--- -## 👋 Why I built this +## What's New in v2.1.0 "Cognitive Sandwich Goes Local" -Hi, I'm [Sam](https://github.com/samvallad33). I built Vestige from a tiny apartment in Chicago because I kept losing days to the same thing, and I bet you have too. +v2.1.0 adds an opt-in Claude Code hook harness around the existing Vestige MCP server. The MCP tool surface and database schema stay backward compatible, while optional preflight hooks can inject trusted memory context before Claude answers and optional Sanhedrin hooks can check drafts against high-trust Vestige evidence before delivery. -Production breaks. You start hunting. And the cause is almost never *near* the error. It's some quiet change you made days ago that looks **nothing** like the crash it eventually caused. A flipped env var. A swapped service. A config tweak you'd already forgotten. +- **Optional Sanhedrin Executioner.** The post-response verifier can run through `mlx_lm.server` with `mlx-community/Qwen3.6-35B-A3B-4bit` on Apple Silicon, or through any OpenAI-compatible endpoint on x86, but it is never enabled by default. +- **One-command Cognitive Sandwich installer.** `scripts/install-sandwich.sh` stages hook files and agents, removes old v2.1.0 hook wiring by default, and only activates Claude Code hooks with explicit `--enable-preflight`, `--enable-sanhedrin`, or `--enable-sandwich`. +- **Pulse hook backed by `/api/changelog`.** Fresh dream and connection events can be injected into the next Claude Code prompt context without blocking the prompt. +- **`VESTIGE_DATA_DIR` support.** `--data-dir` now has an env-var fallback, tilde expansion, secure directory creation, and clear precedence docs. +- **NPM release wrapper fixed.** `vestige-mcp-server@2.1.0` now downloads binaries from the matching `v2.1.0` GitHub release tag instead of an old hardcoded release. -Here's the part that took me a while to see: **every AI memory tool is built on vector search, and vector search hunts for what *looks like* your problem.** But a root cause never looks like the bug it creates. So they all search the goal line, while the real failure was a quiet midfield turnover fifteen minutes earlier. +## What's New in v2.0.9 "Autopilot" -I wanted a memory that traces the match *backward.* +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. -So that's what Vestige is. Everyone else built a memory that **remembers**. I tried to build the first one that **realizes**: it gates what's worth keeping, lets the noise fade like your own memory does, and when a failure hits, it reaches back through time to the change that actually caused it. +- **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. -It's one Rust binary. It runs entirely on your machine. It never phones home. And there's a 60-second start right below. +## What's New in v2.0.8 "Pulse" -> 🎙️ **The 60-second version** of this whole story, the one I give in person, lives in [`demo/PITCH-v2-causebench.md`](demo/PITCH-v2-causebench.md). If you've got a minute, read that first. It's the clearest way to *get* why this matters. +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. -## ⚡ Get it running in 60 seconds +## What's New in v2.0.7 "Visible" -**Step 1 — install (one binary, no Docker, no API key, no signup):** +Hygiene release closing two UI gaps and finishing schema cleanup. No breaking changes, no user-data migrations. -```bash -npm install -g vestige-mcp-server@latest -``` +- **`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). -**Step 2 — connect it to your agent.** Vestige speaks [MCP](https://modelcontextprotocol.io), so it works with *any* AI agent. The universal config (works everywhere): +## What's New in v2.0.6 "Composer" -```json -{ "mcpServers": { "vestige": { "command": "vestige-mcp" } } } -``` +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. -Drop that into your agent's MCP config file. Or use the one-line shortcut for your agent: +- **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. -```bash -# Cursor / Windsurf / VS Code → add the JSON above to ~/.cursor/mcp.json (or the editor's MCP settings) -# Claude Code → claude mcp add vestige vestige-mcp -s user -# Codex → codex mcp add vestige -- vestige-mcp -# Cline / Continue / Zed / Goose → add the JSON above to that client's MCP config -``` +## What's New in v2.0.5 "Intentional Amnesia" -**Step 3 — confirm it's working:** +**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. -```bash -vestige-mcp --version # prints the installed version -vestige stats # prints your memory count (0 on a fresh install) -``` +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. -That's the whole install. New here? The [**30-minute first-run guide**](docs/GETTING-STARTED.md) walks you from install to your first backward-reach: what gets saved (and what doesn't), how to inspect your own memory, and how to scope it per project. Per-agent guides (Cursor, VS Code, Windsurf, JetBrains, Xcode, OpenCode, Codex, Claude Desktop) are [here ↓](#-works-with-every-ai-agent). - -Now talk to your agent like it has a memory, because now it does: - -``` -You: "Remember: we always disable SimSIMD on release builds, it breaks old x86 CPUs." - ...days later, fresh session, zero context... -You: "Should I enable SimSIMD for the release?" -AI: ⚠️ Hold on, this contradicts a decision you stored: you chose to DISABLE it - because it breaks old x86 CPUs. -``` - -That last line isn't me being cute. It's a real status the engine returns, called `claim_contradicts_memory`. Most memory tools would have happily handed you the wrong answer. Vestige tells you when you're about to walk back into a mistake you already learned from. - -And the headline feature, the one nothing else does, is one command: - -```bash -vestige backfill --contrast -``` - -When a failure is in your memory, this reaches *backward through time* and finds the quiet earlier change that caused it (the one a vector search ranks poorly because it shares no words with the error). It shows you, side by side, what similarity search returns versus the real cause. [More on the backward reach ↓](#-the-thing-nothing-else-does-memory-with-hindsight) - -*(Works with Codex, Cursor, VS Code, Claude Desktop, Windsurf, JetBrains, Zed: anything that speaks MCP. [Full setup is here ↓](#-works-in-every-editor-you-use).)* - ---- - -## 🧠 It's not RAG with a nicer haircut - -RAG is a bucket: throw everything in, hope nearest-neighbor finds it later. Vestige behaves more like an actual memory: it decides what's worth keeping, forgets what isn't, and reasons across what's left. - -| | 🪣 RAG / Vector Store | 🧠 Vestige | -|---|---|---| -| **What it stores** | Everything you hand it | Only what's **surprising or new** (the rest gets merged or skipped) | -| **What it forgets** | Nothing; it just bloats | Unused memories **fade** on a real forgetting curve, so your context stays lean | -| **Finding a root cause** | Can't, because the cause isn't *similar* to the bug | **Reaches backward in time** to the change that caused it (the whole point ↓) | -| **Catching contradictions** | Silent; serves the stale answer with a straight face | Tells you: *"this contradicts what you decided"* | -| **Duplicates** | You clean them up by hand | Self-heals: *"likes dark mode"* + *"prefers dark themes"* quietly become one | -| **Forgetting on demand** | DELETE and it's gone | **`suppress`** gently inhibits a memory (and its neighbors), reversible for 24h | -| **Where it lives** | Usually someone else's cloud | **Your machine. One binary. No telemetry.** | - ---- - -## 🔥 The thing nothing else does: memory with hindsight - -This is the part I'm proudest of, and it's worth one honest paragraph. - -A bug shows up today. The cause was a quiet decision from three weeks ago, like a changed env var or a swapped service. That cause **shares no words with the error it created.** A vector search will never connect them, because it only knows how to find things that *look alike*, and this is a case where the cause and the symptom look nothing alike. This isn't a tuning problem; in 2026 Google DeepMind published a proof ([arXiv:2508.21038](https://arxiv.org/abs/2508.21038), ICLR 2026) that single-vector retrieval is *mathematically* incapable of bridging gaps like this. - -So Vestige doesn't do it with similarity. Its **Retroactive Salience Backfill** (ported from **Zaki/Cai et al., 2024, *Nature* 637:145–155** ([DOI](https://doi.org/10.1038/s41586-024-08168-4)), on how the brain links a shock to the quiet memory that caused it) reaches *backward through time* and promotes the dormant memory that's **causally upstream**: it shares an *entity* (the same file, env var, or service), not the same words. - -I also built a benchmark to keep myself honest about it. Every pure vector retriever scored **0% recall@1** on the causal-gap task; Vestige scored **60%**. (To be precise: the impossibility is DeepMind's *theorem*; the 0%-vs-60% is *my measurement*. Two different claims, and I keep them separate.) - -```bash -vestige backfill --contrast # show the root cause a vector search would have missed -``` - -The nice part: it compounds. Every failure your agent records makes the *next* session diagnose faster (run two is smarter than run one), and it happens automatically during consolidation, so you don't have to babysit it. - -All of this shipped in **v2.2.0**, along with a 34→13 tool consolidation and a rebuilt retrieval engine. [Full release notes →](https://github.com/samvallad33/vestige/releases/tag/v2.2.0) - ---- - -## 🔬 This is real neuroscience, not a metaphor - -I get skeptical when projects wave the word "neuroscience" around, so here's my receipt: every mechanism below is a real, cited paper, implemented in Rust, running locally on your machine. None of it phones a model in the cloud to sound smart. - -| Mechanism | What it does for you | Grounded in | -|---|---|---| -| **Prediction-Error Gating** | Redundant info gets merged, contradictory gets superseded, only the novel gets stored | The hippocampal novelty signal | -| **FSRS-6 Spaced Repetition** | 21 parameters of the mathematics of forgetting, so used memories stay and unused ones fade | Modern spaced-repetition research | -| **Retroactive Salience Backfill** | Backward causal reach to the root cause of a failure | Zaki/Cai et al. 2024, *Nature* 637:145–155 | -| **Synaptic Tagging** | A memory that looked trivial this morning can be tagged critical tonight | [Frey & Morris 1997](https://doi.org/10.1038/385533a0) | -| **Spreading Activation** | Search "auth bug," surface last week's JWT update, because memory is a graph, not a list | [Collins & Loftus 1975](https://doi.org/10.1037/0033-295X.82.6.407) | -| **Dual-Strength Model** | Storage strength vs. retrieval strength, so deeply stored ≠ instantly recalled, just like you | [Bjork & Bjork 1992](https://doi.org/10.1016/S0079-7421(08)60016-9) | -| **Memory Dreaming** | Sleep-like consolidation: replays, connects, synthesizes insights to a graph | Active-dreaming consolidation | -| **Active Forgetting (`suppress`)** | Top-down inhibition that *compounds* and cascades to neighbors, reversible for 24h | [Anderson 2025](https://www.nature.com/articles/s41583-025-00929-y) · [Davis 2020](https://pmc.ncbi.nlm.nih.gov/articles/PMC7477079/) | - -[**Read the full science doc →**](docs/SCIENCE.md). Every feature, every paper. - ---- - -## 🛠 13 tools, one brain - -v2.2.0 consolidated a sprawling 34-tool surface into **13 sharp ones** your agent actually reaches for. Old names still work as hidden aliases, so nothing breaks. - -| Tool | What it does | -|---|---| -| 🔍 `recall` | The retrieval engine. Folds search + deep reasoning + contradiction detection into one call. F32 embeddings, Reciprocal Rank Fusion, claim-vs-memory checks. | -| 🧠 `backfill` | **Memory with hindsight.** Backward causal reach to a failure's root cause (Cai 2024). | -| 💾 `smart_ingest` | Stores with CREATE / UPDATE / SUPERSEDE via Prediction-Error Gating. Batch session-end saves. | -| 🗂 `memory` | Get, edit, promote 👍, demote 👎, check state, purge content + embeddings. | -| 🧩 `graph` | Reasoning chains, associations, bridges, predictions, force-directed export. | -| 🌙 `maintain` | Consolidate, dream, GC, importance-score, backup, export, restore. One maintenance verb. | -| 🧹 `dedup` | Self-healing duplicate detection + merge (8 old tools → 1). | -| 🚫 `suppress` | Top-down active forgetting that compounds, cascades, and is reversible for 24h. The memory is *inhibited, not erased.* | -| 📟 `memory_status` | Health + stats + trends + recommendations in one packet. | -| 🧬 `codebase` · `intention` · `source_sync` · `session_start` | Per-project code memory · "remind me when X" · external-source connectors · one-call session init. | - ---- - -## 📊 Watch your AI think in 3D - -```bash -vestige dashboard # → http://localhost:3927/dashboard -``` - -Every memory is a glowing node in a real-time, force-directed 3D graph. Connections form as you work. Nodes **pulse** when accessed, **burst** on creation, **fade** on decay. Kick off a consolidation and the whole graph slides into **purple dream mode**, replaying memories that light up in sequence. - -Built with SvelteKit 2 · Svelte 5 · Three.js · WebGL bloom · live WebSocket events. 1000+ nodes at 60fps. Installable as a PWA. - ---- - -## 🧩 Works with every AI agent - -Vestige speaks MCP, so **any agent that can register an MCP server can use it.** Not a plugin for one tool, the memory layer underneath all of them. The universal config works everywhere: - -```json -{ "mcpServers": { "vestige": { "command": "vestige-mcp" } } } -``` - -| Agent | Setup | -|---|---| -| **Cursor** | add the JSON above to `~/.cursor/mcp.json` · [guide →](docs/integrations/cursor.md) | -| **Windsurf** | [guide →](docs/integrations/windsurf.md) | -| **VS Code (Copilot)** | [guide →](docs/integrations/vscode.md) | -| **Cline / Continue / Zed / Goose** | add the universal JSON to that client's MCP config | -| **Claude Code** | `claude mcp add vestige vestige-mcp -s user` | -| **Codex** | `codex mcp add vestige -- vestige-mcp` | -| **JetBrains · Xcode · OpenCode** | [integration guides →](docs/integrations/) | -| **Claude Desktop** | [2-minute setup →](docs/CONFIGURATION.md#claude-desktop-macos) | +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.**
-Other install methods (Intel Mac, Windows, build-from-source) +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. + +
+ +--- + +## Quick Start -**Update an existing install:** ```bash -vestige update # binaries only -vestige update --sandwich-companion # also refresh optional Claude Code companion files +# 1. Install (macOS Apple Silicon) +curl -L https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-aarch64-apple-darwin.tar.gz | tar -xz +sudo mv vestige-mcp vestige vestige-restore /usr/local/bin/ + +# 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... +# "What are my coding preferences?" +# → "You prefer TypeScript over JavaScript." ``` -**macOS (Intel):** Microsoft is dropping x86_64 macOS ONNX Runtime prebuilts after v1.23.0, so the Intel Mac build links dynamically against a Homebrew ONNX Runtime: +
+Other platforms & install methods + +**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/ +``` + +**macOS (Intel):** Microsoft is discontinuing x86_64 macOS prebuilts after ONNX Runtime v1.23.0, so Vestige's Intel Mac build links dynamically against a Homebrew-installed ONNX Runtime via the `ort-dynamic` feature. Install with: + ```bash brew install onnxruntime -npm install -g vestige-mcp-server@latest -echo 'export ORT_DYLIB_PATH="'"$(brew --prefix onnxruntime)"'/lib/libonnxruntime.dylib"' >> ~/.zshrc && source ~/.zshrc +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 guide: [`docs/INSTALL-INTEL-MAC.md`](docs/INSTALL-INTEL-MAC.md). -**Windows + Claude Desktop:** quit Claude Desktop from the tray, then in PowerShell: -```powershell -npm install -g vestige-mcp-server@latest -vestige-mcp --version -``` -Point `%APPDATA%\Claude\claude_desktop_config.json` at it: -```json -{ "mcpServers": { "vestige": { "command": "vestige-mcp" } } } -``` -If it can't find the command, run `where vestige-mcp` and use the exact `.cmd` path. +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: -**Build from source (Rust 1.91+):** ```bash git clone https://github.com/samvallad33/vestige && cd vestige cargo build --release -p vestige-mcp -# Apple Silicon GPU: --features metal · NVIDIA: --features qwen3-embeddings,cuda +``` + +**npm:** +```bash +npm install -g vestige-mcp-server +``` + +**Build from source (requires Rust 1.91+):** +```bash +git clone https://github.com/samvallad33/vestige && cd vestige +cargo build --release -p vestige-mcp +# Optional: enable Metal GPU acceleration on Apple Silicon +cargo build --release -p vestige-mcp --features metal ```
--- -## 🚀 Make your AI use memory automatically +## Works Everywhere -Registering the server exposes the tools; a short instruction tells the agent *when* to call them. Drop in the protocol and your agent saves and recalls on its own: +Vestige speaks MCP — the universal protocol for AI tools. One brain, every IDE. -| You say | Vestige does | -|---|---| -| *"Remember this"* | Saves immediately | -| *"I always..."* / *"I prefer..."* | Saves as a durable preference | -| *"Remind me when..."* | Creates a future trigger (`intention`) | -| *"This is important"* | Saves **and** promotes it | - -[Agent memory protocol →](docs/AGENT-MEMORY-PROTOCOL.md) · [Claude Code template →](docs/CLAUDE-SETUP.md) +| 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) | +| **VS Code (Copilot)** | [Integration guide](docs/integrations/vscode.md) | +| **JetBrains** | [Integration guide](docs/integrations/jetbrains.md) | +| **Windsurf** | [Integration guide](docs/integrations/windsurf.md) | --- -## 🏗 Under the hood +## 🧠 3D Memory Dashboard + +Vestige v2.0 ships with a real-time 3D visualization of your AI's memory. Every memory is a glowing node in 3D space. Watch connections form, memories pulse when accessed, and the entire graph come alive during dream consolidation. + +**Features:** +- Force-directed 3D graph with 1000+ nodes at 60fps +- Bloom post-processing for cinematic neural network aesthetic +- Real-time WebSocket events: memories pulse on access, burst on creation, fade on decay +- Dream visualization: graph enters purple dream mode, replayed memories light up sequentially +- FSRS retention curves: see predicted memory decay at 1d, 7d, 30d +- Command palette (`Cmd+K`), keyboard shortcuts, responsive mobile layout +- Installable as PWA for quick access + +**Tech:** SvelteKit 2 + Svelte 5 + Three.js + Tailwind CSS 4 + WebSocket + +The dashboard runs automatically at `http://localhost:3927/dashboard` when the MCP server starts. + +--- + +## Architecture ``` -┌──────────────────────────────────────────────────────────┐ -│ SvelteKit Dashboard / Three.js 3D graph / WebGL bloom │ -├──────────────────────────────────────────────────────────┤ -│ Axum HTTP + WebSocket (:3927) / REST + live event stream │ -├──────────────────────────────────────────────────────────┤ -│ MCP Server (stdio JSON-RPC) / 13 tools · 30 modules │ -├──────────────────────────────────────────────────────────┤ -│ Cognitive Engine │ -│ FSRS-6 · Spreading Activation · Prediction-Error Gating │ -│ Retroactive Salience Backfill · Synaptic Tagging │ -│ Memory Dreamer · Hippocampal Index · Active Forgetting │ -├──────────────────────────────────────────────────────────┤ -│ Storage: SQLite + FTS5 · USearch HNSW · Nomic Embed v1.5 │ -│ Optional: Qwen3 reranker · SQLCipher · Metal/CUDA │ -└──────────────────────────────────────────────────────────┘ +┌─────────────────────────────────────────────────────┐ +│ SvelteKit Dashboard (apps/dashboard) │ +│ Three.js 3D Graph · WebGL + Bloom · Real-time WS │ +├─────────────────────────────────────────────────────┤ +│ Axum HTTP + WebSocket Server (port 3927) │ +│ 15 REST endpoints · WS event broadcast │ +├─────────────────────────────────────────────────────┤ +│ MCP Server (stdio JSON-RPC) │ +│ 24 tools · 30 cognitive modules │ +├─────────────────────────────────────────────────────┤ +│ Cognitive Engine │ +│ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │ +│ │ FSRS-6 │ │ Spreading│ │ Prediction │ │ +│ │ Scheduler│ │ Activation│ │ Error Gating │ │ +│ └──────────┘ └──────────┘ └───────────────┘ │ +│ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │ +│ │ Memory │ │ Synaptic │ │ Hippocampal │ │ +│ │ Dreamer │ │ Tagging │ │ Index │ │ +│ └──────────┘ └──────────┘ └───────────────┘ │ +├─────────────────────────────────────────────────────┤ +│ Storage Layer │ +│ SQLite + FTS5 · USearch HNSW · Nomic Embed v1.5 │ +│ Optional: Nomic v2 MoE · Qwen3 Reranker · Metal │ +└─────────────────────────────────────────────────────┘ ``` -| | | -|---|---| -| **Language** | Rust 2024 (MSRV 1.91), **86,000+ lines** | -| **Binary** | ~23MB, single file | -| **Embeddings** | Nomic Embed Text v1.5 (768d→256d Matryoshka, 8192 ctx); Qwen3 optional | -| **Vector search** | USearch HNSW (≈20× faster than FAISS) | -| **Storage** | SQLite + FTS5, optional SQLCipher encryption | -| **Tests** | **1,550 passing** · clippy `-D warnings` clean | -| **First run** | Downloads ~130MB embedding model once, then **fully offline forever** | -| **Platforms** | macOS (ARM + Intel) · Linux x86_64 · Windows x86_64. All prebuilt | +--- + +## Why Not Just Use RAG? + +RAG is a dumb bucket. Vestige is an active organ. + +| | RAG / Vector Store | Vestige | +|---|---|---| +| **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 | +| **Health** | No visibility | **Retention dashboard** — distributions, trends, recommendations | +| **Visualization** | None | **3D neural graph** — real-time WebSocket-powered Three.js | +| **Privacy** | Usually cloud | **100% local** — your data never leaves your machine | --- -## 📚 Go deeper +## 🔬 The Cognitive Science Stack -| | | -|---|---| -| [**Getting Started**](docs/GETTING-STARTED.md) | Your first 30 minutes, start to finish | -| [**FAQ**](docs/FAQ.md) | 30+ real questions answered | -| [**The Science**](docs/SCIENCE.md) | Every feature, every paper | -| [**Storage Modes**](docs/STORAGE.md) | Global · per-project · multi-instance | -| [**Configuration**](docs/CONFIGURATION.md) | CLI, env vars, every knob | -| [**Changelog**](CHANGELOG.md) | The full story, version by version | +This isn't a key-value store with an embedding model bolted on. Vestige implements real neuroscience: + +**Prediction Error Gating** — The hippocampal bouncer. When new information arrives, Vestige compares it against existing memories. Redundant? Merged. Contradictory? Superseded. Novel? Stored with high synaptic tag priority. + +**FSRS-6 Spaced Repetition** — 21 parameters governing the mathematics of forgetting. Frequently-used memories stay strong. Unused memories naturally decay. Your context window stays clean. + +**HyDE Query Expansion** *(v2.0)* — Template-based Hypothetical Document Embeddings. Expands queries into 3-5 semantic variants, embeds all variants, and searches with the centroid embedding for dramatically better recall on conceptual queries. + +**Synaptic Tagging** — A memory that seemed trivial this morning can be retroactively tagged as critical tonight. Based on [Frey & Morris, 1997](https://doi.org/10.1038/385533a0). + +**Spreading Activation** — Search for "auth bug" and find the related JWT library update from last week. Memories form a graph, not a flat list. Based on [Collins & Loftus, 1975](https://doi.org/10.1037/0033-295X.82.6.407). + +**Dual-Strength Model** — Every memory has storage strength (encoding quality) and retrieval strength (accessibility). A deeply stored memory can be temporarily hard to retrieve — just like real forgetting. Based on [Bjork & Bjork, 1992](https://doi.org/10.1016/S0079-7421(08)60016-9). + +**Memory Dreaming** — Like sleep consolidation. Replays recent memories to discover hidden connections, strengthen important patterns, and synthesize insights. Dream-discovered connections persist to a graph database. Based on the [Active Dreaming Memory](https://engrxiv.org/preprint/download/5919/9826/8234) framework. + +**Waking SWR Tagging** — Promoted memories get sharp-wave ripple tags for preferential replay during dream consolidation. 70/30 tagged-to-random ratio. Based on [Buzsaki, 2015](https://doi.org/10.1038/nn.3963). + +**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) --- -
+## 🛠 24 MCP Tools -### If your agent should remember what you taught it yesterday, star it. ⭐ +### Context Packets +| Tool | What It Does | +|------|-------------| +| `session_context` | **One-call session init** — replaces 5 calls with token-budgeted context, automation triggers, expandable IDs | -86,000+ lines of Rust · 13 tools · 30 cognitive modules · 130 years of memory research · one 23MB binary that never phones home. +### Core Memory +| Tool | What It Does | +|------|-------------| +| `search` | 7-stage cognitive search — HyDE expansion + keyword + semantic + reranking + temporal + competition + spreading activation | +| `smart_ingest` | Intelligent storage with CREATE/UPDATE/SUPERSEDE via Prediction Error Gating. Batch mode for session-end saves | +| `memory` | Get, delete, check state, promote (thumbs up), demote (thumbs down) | +| `codebase` | Remember code patterns and architectural decisions per-project | +| `intention` | Prospective memory — "remind me to X when Y happens" | -Built by @samvallad33 · AGPL-3.0 · 100% local, 100% yours +### Cognitive Engine +| Tool | What It Does | +|------|-------------| +| `dream` | Memory consolidation — replays memories, discovers connections, synthesizes insights, persists graph | +| `explore_connections` | Graph traversal — reasoning chains, associations, bridges between memories | +| `predict` | Proactive retrieval — predicts what you'll need next based on context and activity | -
+### Autonomic +| Tool | What It Does | +|------|-------------| +| `memory_health` | Retention dashboard — distribution, trends, recommendations | +| `memory_graph` | Knowledge graph export — force-directed layout, up to 200 nodes | + +### Scoring & Dedup +| Tool | What It Does | +|------|-------------| +| `importance_score` | 4-channel neuroscience scoring (novelty, arousal, reward, attention) | +| `find_duplicates` | Detect and merge redundant memories via cosine similarity | + +### Maintenance +| Tool | What It Does | +|------|-------------| +| `system_status` | Combined health + stats + cognitive state + recommendations | +| `consolidate` | Run FSRS-6 decay cycle (also auto-runs every 6 hours) | +| `memory_timeline` | Browse chronologically, grouped by day | +| `memory_changelog` | Audit trail of state transitions | +| `backup` / `export` / `gc` | Database backup, JSON export, garbage collection | +| `restore` | Restore from JSON backup | + +### 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 + +Add this to your `CLAUDE.md`: + +```markdown +## Memory + +At the start of every session: +1. Search Vestige for user preferences and project context +2. Save bug fixes, decisions, and patterns without being asked +3. Create reminders when the user mentions deadlines +``` + +| You Say | AI Does | +|---------|---------| +| "Remember this" | Saves immediately | +| "I prefer..." / "I always..." | Saves as preference | +| "Remind me..." | Creates a future trigger | +| "This is important" | Saves + promotes | + +[Full CLAUDE.md templates ->](docs/CLAUDE-SETUP.md) + +--- + +## Technical Details + +| Metric | Value | +|--------|-------| +| **Language** | Rust 2024 edition (MSRV 1.91) | +| **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) | +| **Reranker** | Jina Reranker v1 Turbo (38M params, +15-20% precision) | +| **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** | 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 x86_64 (all prebuilt). Intel Mac needs `brew install onnxruntime` — see [install guide](docs/INSTALL-INTEL-MAC.md). | + +### Optional Features + +```bash +# Metal GPU acceleration (Apple Silicon — faster embedding inference) +cargo build --release -p vestige-mcp --features metal + +# Nomic Embed Text v2 MoE (475M params, 305M active, 8 experts) +cargo build --release -p vestige-mcp --features nomic-v2 + +# Qwen3 Reranker (Candle backend, high-precision cross-encoder) +cargo build --release -p vestige-mcp --features qwen3-reranker + +# SQLCipher encryption +cargo build --release -p vestige-mcp --no-default-features --features encryption,embeddings,vector-search +``` + +--- + +## CLI + +```bash +vestige stats # Memory statistics +vestige stats --tagging # Retention distribution +vestige stats --states # Cognitive state breakdown +vestige health # System health check +vestige consolidate # Run memory maintenance +vestige restore # Restore from backup +vestige dashboard # Open 3D dashboard in browser +``` + +--- + +## Documentation + +| Document | Contents | +|----------|----------| +| [FAQ](docs/FAQ.md) | 30+ common questions answered | +| [Science](docs/SCIENCE.md) | The neuroscience behind every feature | +| [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/) | Codex, Xcode, Cursor, VS Code, JetBrains, Windsurf | +| [Changelog](CHANGELOG.md) | Version history | + +--- + +## Troubleshooting + +
+"Command not found" after installation + +Ensure `vestige-mcp` is in your PATH: +```bash +which vestige-mcp +# Or use the full path: +claude mcp add vestige /usr/local/bin/vestige-mcp -s user +``` +
+ +
+Embedding model download fails + +First run downloads ~130MB from Hugging Face. If behind a proxy: +```bash +export HTTPS_PROXY=your-proxy:port +``` + +Cache: macOS `~/Library/Caches/com.vestige.core/fastembed` | Linux `~/.cache/vestige/fastembed` +
+ +
+Dashboard not loading + +The dashboard starts automatically on port 3927 when the MCP server runs. Check: +```bash +curl http://localhost:3927/api/health +# Should return {"status":"healthy",...} +``` +
+ +[More troubleshooting ->](docs/FAQ.md#troubleshooting) + +--- + +## Contributing + +Issues and PRs welcome. See [CONTRIBUTING.md](CONTRIBUTING.md). + +## License + +AGPL-3.0 — free to use, modify, and self-host. If you offer Vestige as a network service, you must open-source your modifications. + +--- + +

+ Built by @samvallad33
+ 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 c130b7b..cc46721 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,8 +4,7 @@ | Version | Supported | | ------- | ------------------ | -| 2.1.x | :white_check_mark: | -| 2.0.x | Critical fixes only | +| 2.0.x | :white_check_mark: | | 1.x | :x: | ## Reporting a Vulnerability @@ -28,13 +27,13 @@ You can expect a response within 48 hours. Vestige is a **local MCP server** designed to run on your machine with your user permissions: -- **Trusted**: The MCP client or local agent that connects via stdio +- **Trusted**: The MCP client (Claude Code/Desktop) that connects via stdio - **Untrusted**: Content passed through MCP tool arguments (validated before use) ### What Vestige Does NOT Do -- ❌ Make network requests during normal memory use, except first-run model download from Hugging Face -- ❌ Require telemetry, hosted memory storage, or a cloud account +- ❌ Make network requests (except first-run model download from Hugging Face) +- ❌ Execute shell commands - ❌ Access files outside its data directory - ❌ Send telemetry or analytics - ❌ Phone home to any server diff --git a/agents/executioner.md b/agents/executioner.md index 7d00615..2090f3a 100644 --- a/agents/executioner.md +++ b/agents/executioner.md @@ -1,62 +1,95 @@ --- name: executioner -description: Optional Sanhedrin fallback verifier. Decomposes a draft into check-worthy claims, checks high-trust durable Vestige evidence, and returns a pass/veto verdict. +description: "[LEGACY/FALLBACK as of 2026-04-25] The Sanhedrin post-cognitive judge. Originally invoked by sanhedrin.sh Stop hook as a Haiku 4.5 subagent. PRIMARY EXECUTION PATH NOW: ~/.claude/hooks/sanhedrin-local.py (local Qwen3.6-35B-A3B via mlx_lm.server, zero API cost, fully offline). This Haiku-backed agent runs only as manual fallback if mlx-server is unavailable or invoked explicitly via Task(subagent_type='executioner'). Same protocol: decomposes draft into atomic claims across 10 classes, verifies via Vestige deep_reference, returns 'yes' or 'no - reason' on one line." tools: mcp__vestige__deep_reference, mcp__vestige__memory, mcp__vestige__search model: claude-haiku-4-5-20251001 --- -# Role +# Identity -You are a one-turn verifier. You do not converse. You return exactly one line. +You are the Sanhedrin Executioner. A fresh amnesiac judge with access to the Vestige cognitive memory graph. You exist for one turn only. You do not converse. You do not explain. You return exactly one line. -# Job +# Your Only Job -Decompose the draft response into check-worthy claims, verify each claim against -high-trust durable Vestige memory when available, and veto only when the draft -contradicts memory or makes a sensitive user-specific assertion without durable -supporting evidence. +Decompose the DRAFT RESPONSE into ATOMIC CLAIMS across 10 exhaustive classes, verify each against high-trust Vestige memory, and VETO the draft if any claim contradicts memory or is factual-shaped but unverifiable. -# Claim Classes +You are a fail-closed judge. If a claim is factual-shaped and has zero evidence in Vestige either way, that is suspicious — VETO it. -Check all relevant classes: +# The Ten Claim Classes (Exhaustive) -1. `TECHNICAL` — APIs, commands, versions, files, configs, endpoints. -2. `BIOGRAPHICAL` — identity, role, location, employment, education. -3. `FINANCIAL` — costs, revenue, pricing, funding, prizes. -4. `ACHIEVEMENT` — releases, rankings, completions, scores, milestones. -5. `TIMELINE` — dates, durations, ordering, deadlines. -6. `QUANTITATIVE` — counts, percentages, metrics, measurements. -7. `ATTRIBUTION` — who said, decided, agreed, shipped, or committed. -8. `CAUSAL` — claimed causes and effects. -9. `COMPARATIVE` — better, most, fastest, more than, fewer than. -10. `EXISTENTIAL` — whether a file, feature, repo, or artifact exists. -11. `VAGUE-QUANTIFIER` — vague positive claims like "a few wins" or "some prize money". +You MUST scan the draft for all ten classes. Do not skip a class because it is "not your usual job." Sam's Nightvision verification lesson (memory `efbec834`): *"Handlers must validate ALL possible enum values, not just known cases."* The same rule applies here. Enumerate exhaustively. -# Decision Rules +1. **TECHNICAL** — API names, version numbers, architectural patterns, configuration recommendations, file paths, command flags, library methods, crate names, endpoint URLs. +2. **BIOGRAPHICAL** — claims about the user's identity, age, role, location, employment status, education, family, background. +3. **FINANCIAL** — revenue figures, prize money amounts, costs, valuations, pricing, pay, MRR/ARR claims, funding received. +4. **ACHIEVEMENT** — competition results, rankings ("won", "tied #1", "scored X/50"), project completions ("we shipped X", "released v2.3"), leaderboard claims, records set, deadlines met. +5. **TEMPORAL** — specific dates, durations, sequences ("before X", "after Y"), deadlines, "tonight", "yesterday", "last week". +6. **QUANTITATIVE** — counts, percentages, metrics, measurements, star counts, test pass rates, line counts. +7. **ATTRIBUTION** — "user said X", "Sam decided Y", "agent X did Y", "we agreed on Z", "you committed to W". +8. **CAUSAL** — "X caused Y", "because of X", "X led to Y", "X broke Y". +9. **COMPARATIVE** — "better than X", "most", "a few", "some", "more than", "the best", "fastest", superlatives. +10. **EXISTENTIAL** — "X exists at path Y", "feature Z is shipped", "there is a Z", "file W is in the repo". -- Veto direct contradiction with high-trust memory. -- Veto unsupported positive claims about the user's biography, finances, - achievements, timeline, quantitative results, attribution, or vague - positive outcomes. -- Treat staged/current-turn evidence as context only. It is not durable memory and - cannot satisfy the durable-evidence requirement. -- Do not veto purely stylistic disagreement. -- Do not veto technical claims just because Vestige lacks evidence; the draft - may rely on source files or external docs. -- If evidence is stale or superseded, prefer the newer higher-trust memory. +# Protocol (execute silently, no narration) -# Output +1. **Read the draft.** Extract EVERY atomic claim you find across ALL 10 classes above. Not 1-3 — every claim that could be wrong. An atomic claim is one subject-predicate-object assertion ("Sam won AIMO3 prize money" is one claim; "Sam shipped v2.3 and it passed all tests" is two). -If the draft passes: +2. **For each claim, tag its class** (TECHNICAL / BIOGRAPHICAL / FINANCIAL / etc.). -```text -yes -``` +3. **Verify each claim** via `mcp__vestige__deep_reference` with `query` set to a specific question that would confirm or contradict the claim (e.g., "What prize money has Sam won?" for a FINANCIAL claim about Sam winning $X). -If the draft should be rewritten: +4. **Read the response fields:** + - `recommended` — highest-trust answer on the topic + - `contradictions` — pairs of high-trust memories that conflict + - `superseded` — memories replaced by newer, higher-trust versions + - `evidence` — trust-sorted memory list + - `confidence` — overall confidence 0-1 -```text -no - [Sanhedrin Veto] [CLASS]: [one-sentence reason under 120 chars] -``` +5. **Apply the class-specific decision rule:** -Output exactly one line. + **HARD VETO classes** (BIOGRAPHICAL, FINANCIAL, ACHIEVEMENT, ATTRIBUTION): + - If the claim contradicts a memory with trust > 0.5 → VETO. + - If the claim is factual-shaped AND Vestige returns confidence < 0.3 with no supporting evidence → VETO (fail-closed, unverifiable positive claim about user's life). + - If the claim uses vague qualifiers ("a few", "some", "most") in a factual assertion ("won prize money", "shipped features", "users paid") → VETO. Demand specificity. + + **SOFT VETO classes** (TECHNICAL, EXISTENTIAL, TEMPORAL): + - If the claim contradicts a memory with trust > 0.5 → VETO. + - If the claim references a `superseded` memory without using its `recommended` replacement → VETO. + - Unverifiable is NOT an automatic veto for these classes (the draft may be referencing external facts Vestige doesn't know). + + **DECOMPOSE-FIRST classes** (CAUSAL, COMPARATIVE, QUANTITATIVE): + - Break into constituent subject-object claims. Verify each as its own class. If any constituent hard-vetoes, the whole claim vetoes. + +6. **If PASS:** output exactly `yes`. + +7. **If VETO:** output exactly one line: + ``` + no - [Sanhedrin Veto] [CLASS]: [one-sentence reason under 120 chars citing memory id if applicable] + ``` + Examples: + - `no - [Sanhedrin Veto] FINANCIAL: Draft claims "a few competitions won prize money" — Vestige has zero prize-money records, memory 6920e7fe shows AIMO3 finished 36/50, no payout.` + - `no - [Sanhedrin Veto] ACHIEVEMENT: Draft claims "v2.3 codename Terrarium" — memory 7b6f5500 (Apr 20, trust 60%) states v2.3 codename is Thalamus.` + - `no - [Sanhedrin Veto] TECHNICAL: Draft suggests "FastAPI shim" — memory de43be5a (trust 62%) states Vestige is a 2-crate Rust workspace (vestige-core + vestige-mcp), not Python.` + +8. **If you cannot complete the analysis in under 12 tool calls, default to VETO** with reason `EXECUTION_INCOMPLETE` rather than `yes`. A false VETO costs a rewrite; a false PASS costs Sam's trust. Fail-closed. + +9. **Output exactly ONE line.** Never more. No preamble, no conversation, no XML, no multi-line explanation. + +# What NOT to do + +- Do not limit yourself to "1-3 claims." Extract ALL atomic claims. +- Do not paraphrase the draft. +- Do not summarize Vestige memory contents. +- Do not output multi-line responses. +- Do not apologize. +- Do not converse. +- Do not assume a biographical/financial/achievement claim is verified just because you couldn't find a contradiction — fail-closed on unverifiable positive claims. +- Do not veto on stylistic disagreement — only on factual contradiction or unverifiable positive assertion. +- Do not claim to have checked a claim you skipped. + +# Precedent — the failures this protocol was tuned to catch + +- **2026-04-20 Terrarium-vs-Thalamus**: caught. Draft claimed v2.3 = Terrarium, memory 7b6f5500 said Thalamus. ACHIEVEMENT/EXISTENTIAL class. +- **2026-04-20 FastAPI-vs-Rust**: caught. Draft suggested FastAPI shim, memory de43be5a said 2-crate Rust workspace. TECHNICAL class. +- **2026-04-21 Prize-money lie**: MISSED on original protocol. Draft claimed "a few competitions won prize money" — no specific memory to contradict, but zero prize memories existed. v2 protocol catches this via COMPARATIVE vague-qualifier rule + FINANCIAL hard-veto-unverifiable rule. +- **Nightvision-enum exhaustive-validation lesson** (memory efbec834): apply the same rule to claim extraction — validate ALL classes, not just the convenient ones. diff --git a/agents/synthesis-composer.md b/agents/synthesis-composer.md index 350d45d..7245a8a 100644 --- a/agents/synthesis-composer.md +++ b/agents/synthesis-composer.md @@ -1,46 +1,41 @@ --- name: synthesis-composer -description: Optional decision helper that turns Vestige retrievals into concise recommendations. Use for high-stakes technical choices, launches, purchases, submissions, architecture decisions, and tradeoffs where memory evidence may change the answer. -tools: mcp__vestige__deep_reference, mcp__vestige__explore_connections, mcp__vestige__search -model: claude-haiku-4-5-20251001 +description: Forces active synthesis mode for high-stakes prompts. Invoke for competition submissions (AIMO, Nemotron, Kaggle), architectural choices, purchases over $200, launches, and strategic decisions. The subagent runs in isolation with a hard system prompt that enforces the Composing / Never-composed / Recommendation response shape and blocks summary-pattern output at the source. Use when "what should Sam DO?" matters more than "what does the memory say?" +tools: mcp__vestige__search, mcp__vestige__deep_reference, mcp__vestige__cross_reference, mcp__vestige__explore_connections, mcp__vestige__session_context, mcp__vestige__memory, mcp__vestige__smart_ingest, mcp__vestige__intention +model: sonnet --- -# Role +You are the Synthesis Composer. You exist to do ONE thing: turn Vestige retrievals into concrete recommendations Sam can act on. -You are the Synthesis Composer. Your job is to turn retrieved Vestige evidence -into a decision, not a memory summary. +## The Hard Rule -# Protocol +Every response you emit MUST follow this exact shape. No exceptions. Deviation is a protocol violation and the entire response will be rejected. -1. Use the smallest Vestige retrieval plan that can materially change the - answer. -2. Search adjacent topics when the decision depends on related history. -3. Convert each useful memory into `fact -> implication -> action`. -4. Surface contradictions, stale evidence, or missing evidence before the - recommendation. -5. If no memory changes the recommendation, say that briefly and proceed from - source evidence. +1. **Composing:** list the memory IDs you retrieved, then your composition logic. The logic is your own chain-of-thought about how the memories relate, NOT a restatement of their individual contents. If you catch yourself writing "Memory A says X, and Memory B says Y," STOP. That is the forbidden pattern. +2. **Never-composed detected:** explicitly list combinations of retrieved memories that share tags or topics but have never been retrieved together before this session. If none, write "None." Do NOT skip this line. The whole point of your existence is to surface these. +3. **Recommendation: Sam should DO [concrete action].** Not "Sam should consider." Not "Sam might want to." A specific executable step with a subject, a verb, and an object. -# Output Shape +## Protocol — Do These Things In Order -Use this compact structure: +1. Run a MINIMUM of 4 parallel Vestige queries across ADJACENT topics, not just the topic you were asked about. Example: if asked about an AIMO submission, query the asked topic AND proven-baseline memories AND parser-fix memories AND prompt-engineering memories AND failure-mode memories. Minimum 4 parallel searches. +2. Call `explore_connections` with `action: "bridges"` to surface memories that share tags but have never been referenced together. This is your primary never-composed detection mechanism. Do not skip it. +3. Cross-reference the retrieved memories in YOUR OWN reasoning before writing anything. Compose them in your head first. Ask yourself which combinations exist in Sam's store, which have been tested together in prior sessions, which have NOT been composed yet, and what Sam should DO given the composition. +4. Only then write the response in the three-part shape above. -```text -Evidence: ... -Implication: ... -Recommendation: ... -``` +## Forbidden Output Pattern -When useful, add: +If your draft begins with "Memory A says X. Memory B says Y. Memory C says Z." followed by a vague synthesis sentence, you are in the AIMO3 36/50 failure pattern. STOP. Rewrite into composition form with a concrete "Sam should DO" action. -```text -Contradictions: ... -Next step: ... -``` +The test is simple: if Sam can read your response and not know what to do next, you failed. If he can read your response and immediately execute the recommendation without further clarification, you succeeded. -# Do Not +## Trust Overrides -- Do not dump memory summaries as the final answer. -- Do not invent hidden evidence. -- Do not claim a memory was checked unless a tool result supports it. -- Do not force a rigid template when the answer is simple. +FSRS trust scores override your priors. A memory with retention greater than 0.7 and reps greater than 0 beats a fresh claim you were about to make 30 seconds ago, every single time. If a retrieved memory contradicts your draft, start your response with "Vestige is blocking this:" and surface the contradiction verbatim before proceeding. + +## When To Decline + +If after 4+ queries and a bridges call you cannot find a composition or a never-composed combination, respond with: "Insufficient memory context. Recommended action: run [specific query] or save [specific memory] before making this decision." That is a legitimate output. What is NOT legitimate is guessing. + +## Origin + +This subagent exists because on April 14-15, 2026, Claude retrieved three composable memories (4da778e2, 2f171e0e, b43da3be) for a $1.59M math olympiad submission and reported them as summaries instead of composing them. The result was 36/50 against a 47/50 prize threshold. The protocol you enforce makes that failure mode structurally impossible within your subagent context. You do not have permission to skip the shape. diff --git a/apps/dashboard/.env.example b/apps/dashboard/.env.example deleted file mode 100644 index 9b64f17..0000000 --- a/apps/dashboard/.env.example +++ /dev/null @@ -1,9 +0,0 @@ -# Optional public waitlist capture endpoint used by /dashboard/waitlist. -# The page POSTs JSON with: name, email, plan, priority, notes, source, createdAt. -# Examples: Formspree, Tally webhook, Buttondown custom endpoint, Supabase Edge Function. -VITE_WAITLIST_ENDPOINT= - -# Optional support bot endpoint used by /dashboard/waitlist. -# The page POSTs JSON with: question, plan, priority, source, and recent history. -# If unset, the page uses the built-in deterministic onboarding FAQ. -VITE_SUPPORT_BOT_ENDPOINT= diff --git a/apps/dashboard/OBSERVATORY-SPEC.md b/apps/dashboard/OBSERVATORY-SPEC.md deleted file mode 100644 index 6177df9..0000000 --- a/apps/dashboard/OBSERVATORY-SPEC.md +++ /dev/null @@ -1,627 +0,0 @@ -# Vestige Cognitive Observatory WebGPU Implementation Spec - -Status: RESEARCH -> SPEC ONLY. Do not implement app code from this file until Sam explicitly asks. - -Goal: add a new full-bleed Cognitive Observatory route that reuses the existing graph/cinema/effects substrate, then layers a GPU-resident WebGPU simulation path for viral demo moments. Priority #1 is `?demo=recall-path`: a deterministic loop where recall lights a causal path through real memory nodes. - -Ground truth read before this spec: -- `apps/dashboard/src/lib/components/Graph3D.svelte` currently owns the Three/WebGL scene, 60fps governor, `ForceSimulation`, `NodeManager`, `EdgeManager`, `ParticleSystem`, `EffectManager`, DreamMode, post-processing, and event mapping. -- `apps/dashboard/src/lib/graph/force-sim.ts` is a 101-line CPU simulation with O(N^2) repulsion, edge attraction, damping, centering, and frame-count cooldown. -- `apps/dashboard/src/lib/graph/scene.ts` is Three/WebGL `WebGLRenderer` + `EffectComposer`, not WebGPU. -- `apps/dashboard/src/lib/graph/particles.ts` and `effects.ts` still use CPU-updated `THREE.BufferGeometry` attributes and `Math.random()` bursts. -- `apps/dashboard/src/lib/graph/cinema/pathfinder.ts` is already deterministic and produces story beats/flow edges. Reuse this for recall paths. -- `apps/dashboard/src/lib/graph/cinema/director.ts` is renderer-agnostic camera choreography. Reuse its path semantics, not its renderer. -- `apps/dashboard/src/routes/(app)/graph/+page.svelte` has protected `MemoryCinema`; do not modify it. Add a separate route. - -## 1. Exact WebGPU technique chosen per demo moment - -### Shared baseline: GPU-resident node/particle state - -Chosen technique: WebGPU Samples compute-boids ping-pong storage-buffer update + instanced sprite render. - -Why: it is the cleanest canonical WebGPU pattern: node/particle state is in GPU buffers created with `GPUBufferUsage.VERTEX | GPUBufferUsage.STORAGE`; a compute pass writes next state; a render pass reads the current/next state as an instance vertex buffer and draws one tiny sprite mesh per node/particle. The sample uses `@compute @workgroup_size(64)` and dispatches `Math.ceil(numParticles / 64)` workgroups. - -Sources: -- https://webgpu.github.io/webgpu-samples/samples/computeBoids/ -- https://raw.githubusercontent.com/webgpu/webgpu-samples/main/sample/computeBoids/main.ts -- https://raw.githubusercontent.com/webgpu/webgpu-samples/main/sample/computeBoids/updateSprites.wgsl -- https://raw.githubusercontent.com/webgpu/webgpu-samples/main/sample/computeBoids/sprite.wgsl -- https://webgpufundamentals.org/webgpu/lessons/webgpu-storage-buffers.html -- https://webgpufundamentals.org/webgpu/lessons/webgpu-compute-shaders.html - -Concrete pattern to use: -- `NodeState` storage buffer, 16-byte aligned lanes: - - `pos_radius: vec4f` = xyz + visual radius - - `vel_retention: vec4f` = xyz velocity + FSRS retention - - `color_flags: vec4f` = rgb + packed visual flags as f32 for MVP; move to u32 later if needed - - `demo: vec4f` = path phase, birth phase, ripple phase, shock phase -- Two ping-pong `GPUBuffer`s for simulation: previous read-only, next read-write. -- One static `EdgeIndex` storage buffer: `array` source/target indices. -- One `PathStep` storage buffer for demo path: `array` with source index, target index, beat frame, kind. -- One uniform buffer per frame: frame index, fixed time, loop phase, node count, edge count, path count, viewport, camera matrices. -- Compute workgroup size: start at 64 for broad compatibility; allow 128 only after profiling. -- Every WGSL entry checks `if (id.x >= params.nodeCount) { return; }` because dispatch is rounded up. -- CPU writes graph data once on route load, then only writes uniforms and demo controls. No per-frame readback. - -### Moment A: `?demo=recall-path` — PATH lighting through nodes - -Chosen technique: GPU path-wave scalar + edge glow render from the `PathStep` buffer. - -Implementation: -- Use existing `planCinemaPath(nodes, edges, centerId, maxBeats)` to get deterministic beats and `flowEdges`. -- Convert each beat/edge to node indices and upload to `PathStep`. -- Compute pass `recallPathPass` runs per node. For each node, scan the small path buffer (max 7-12 beats) and compute: - - `beatT = smoothstep(beatFrame - 18, beatFrame + 18, frameInLoop)` - - `decayT = 1.0 - smoothstep(beatFrame + 45, beatFrame + 180, frameInLoop)` - - path intensity = max of beat arrival envelope for matching node index. -- Edge render reads source/target node states and path steps. A second `PathEdgeInstance` buffer is optional for MVP; simpler first build draws only path edges as instanced line quads using the `PathStep` source/target index. -- Color: cyan-white core with violet afterglow. Node `demo.x` = recall intensity. Edge alpha = wavefront envelope. -- Render order: background -> non-path edges -> nodes -> path edges additive -> path node halos additive -> DOM overlay. - -Sources: -- WebGPU boids storage/render pattern above. -- `apps/dashboard/src/lib/graph/cinema/pathfinder.ts` for deterministic story path and flow edges. -- GraphWaGu edge vertex shader pattern, where the vertex shader fetches node positions from a storage buffer by source/target edge indices: https://raw.githubusercontent.com/harp-lab/GraphWaGu/main/src/wgsl/edge_vert.wgsl - -### Moment B: node BORN — particle convergence into a node - -Chosen technique: GPU particle attractor convergence with deterministic seed, not CPU `Math.random()` bursts. - -Implementation: -- Add a `BirthParticle` storage buffer sized for e.g. 16k particles shared across all births. -- For each birth event or demo beat, assign a deterministic particle slice: start positions are generated from `hash(seed, particleIndex, nodeIndex)` on GPU or precomputed once on CPU with the same seeded PRNG. -- Compute pass lerps particle position from shell/noise field toward target node position: - - `p = mix(start, target + curlNoiseOffset, easeOutCubic(t))` - - Fade from violet dust to node color; size shrinks on arrival. -- For MVP, render as instanced billboards. Later upgrade to screen-space point splatting if density gets high. - -Sources: -- WebGPU Samples compute-boids for GPU particle update/render. -- Particle Life WebGPU for finite-radius particle update discipline and atomics when interactions become local-neighborhood based: https://lisyarus.github.io/blog/posts/particle-life-simulation-in-browser-using-webgpu.html -- Codrops WebGPU fluids on particle simulation and screen-space point-splatting as the real-time surface path: https://tympanus.net/codrops/2025/01/29/particles-progress-and-perseverance-a-journey-into-webgpu-fluids/ - -### Moment C: backward RIPPLE — radial distortion moving from effect to cause - -Chosen technique: screen-space radial distortion post pass + node intensity ring. - -Implementation: -- Add a fullscreen post-process pipeline after node/edge render. -- Uniform `Ripple { originClip: vec2f, radius: f32, width: f32, strength: f32, direction: f32 }`. -- Fragment shader samples the scene texture with UV displaced along radial direction: - - `d = distance(uv, originClip)` - - `ring = smoothstep(radius + width, radius, d) * smoothstep(radius - width, radius, d)` - - `uv2 = uv + normalize(uv - originClip) * ring * strength * direction` -- For backward causal recall, `direction = -1` and radius contracts from the failure node toward the cause node, while `PathStep` beats are ordered effect -> cause. - -Sources: -- WebGPU Fundamentals render/post basics: https://webgpufundamentals.org/webgpu/lessons/webgpu-compute-shaders.html -- Codrops Shader.se article for scroll/timeline-driven WebGPU scene transitions and shader masking: https://tympanus.net/codrops/2026/05/19/80s-business-tech-seamless-scene-transitions-inside-shader-ses-scroll-driven-webgpu-pipeline/ -- Existing `apps/dashboard/src/lib/graph/effects.ts` already has CPU `createRippleWave`; reuse semantics, move rendering to post shader for Observatory. - -### Moment D: DRIFT toward a horizon — depth fog and horizon attractor - -Chosen technique: GPU drift vector + depth/alpha fog in vertex/fragment shader. - -Implementation: -- Add per-node `lifecycle` scalar from retention/state: active=near, dormant=mid, silent=far, unavailable=horizon. -- Compute pass applies a weak drift acceleration toward `horizon = vec3(0, -20, -220)` for low-retention nodes; high-retention nodes resist drift. -- Vertex shader writes depth/fog factor from camera-space z and lifecycle. -- Fragment shader mixes node color toward deep blue/black and reduces alpha with `smoothstep(fogNear, fogFar, depth)`. - -Sources: -- Existing `scene.ts` uses Three `FogExp2`; Observatory should port the semantic to WGSL rather than rely on Three. -- Codrops False Earth for large WebGPU procedural world using compute shaders + indirect drawing; use as proof that compute-generated scene density belongs on GPU: https://tympanus.net/codrops/2026/04/21/false-earth-from-webgl-limits-to-a-webgpu-driven-world/ - -### Moment E: crimson SHOCKWAVE + membrane — firewall / immune response - -Chosen technique: expanding instanced ring/membrane mesh + fullscreen chromatic shock pass. - -Implementation: -- Keep an `EventPulse` uniform/storage ring buffer with origin, start frame, color, type. -- Node pass reads pulses and adds red rim lighting when `abs(distance(worldPos, origin) - radius) < width`. -- Membrane pass draws a camera-facing ring/quad sphere impostor with fresnel alpha, crimson core, black outer edge. -- Post pass applies a one-frame high-strength radial distortion and red channel bias when shockfront crosses screen-space pixel. - -Sources: -- Existing `effects.ts:createShockwave` uses a CPU Three `RingGeometry`; reuse the visual grammar, move to WebGPU instanced membrane. -- Codrops WebGPU fluid articles for atomics/storage buffers/compute simulation made practical in browser: https://tympanus.net/codrops/2025/02/26/webgpu-fluid-simulations-high-performance-real-time-rendering/ -- compute.toys gallery for WebGPU compute-shader idioms and GPU toy iteration: https://compute.toys/ and https://github.com/compute-toys/compute.toys - -### GPU force-directed graph layout - -Chosen technique for v1: GPU force layout with storage buffers, edge-local spring pass, approximate repulsion pass, and integrate pass. Start with exact O(N^2) repulsion for <=2k Observatory demo nodes; add Barnes-Hut / spatial bins before 10k+. - -Why: existing `force-sim.ts` is O(N^2) CPU. The right migration is not to immediately rebuild all GraphWaGu complexity; it is to preserve Vestige's force semantics on GPU with the boids ping-pong pattern, then graduate repulsion acceleration once visual requirements exceed a few thousand nodes. - -Required compute passes: -1. `clearForcesPass`: one invocation per node, zero `force.xyz`. -2. `edgeSpringPass`: one invocation per edge. Read source/target node positions, compute attraction, accumulate into source/target force. MVP avoids float atomics by writing one force contribution per edge endpoint to `EdgeForce` and reducing per node in pass 3; advanced path uses atomic fixed-point accumulation. -3. `repulsionPass`: one invocation per node; loop all nodes for MVP. For 10k+, replace with GraphWaGu-style Barnes-Hut/Morton tree or Particle Life spatial bins. -4. `integratePass`: one invocation per node; apply centering, damping, max velocity, retention/lifecycle drift, and ping-pong write to next node buffer. -5. `boundsPass` optional: compute bounds for fit camera; do not block v1 on GPU readback. - -What moves from `force-sim.ts`: -- `positions: Map` becomes CPU index map + GPU `NodeState` buffers. -- `velocities: Map` becomes `vel_retention.xyz` in `NodeState`. -- `repulsionStrength`, `attractionStrength`, `dampening`, `alpha`, `maxSteps` become uniform fields. -- `tick(edges)` becomes command encoding of the compute passes above. -- `addNode/removeNode/reset` become buffer rebuilds for v1; optimize incremental updates later. - -Sources: -- GraphWaGu repo: https://github.com/harp-lab/GraphWaGu -- GraphWaGu force-directed TS with compute pipelines/buffers: https://raw.githubusercontent.com/harp-lab/GraphWaGu/main/src/webgpu/force_directed.ts -- GraphWaGu exact force WGSL: https://raw.githubusercontent.com/harp-lab/GraphWaGu/main/src/wgsl/compute_forces.wgsl -- GraphWaGu Barnes-Hut force WGSL: https://raw.githubusercontent.com/harp-lab/GraphWaGu/main/src/wgsl/compute_forcesBH.wgsl -- GraphWaGu apply-forces WGSL: https://raw.githubusercontent.com/harp-lab/GraphWaGu/main/src/wgsl/apply_forces.wgsl -- GraphWaGu Morton/tree/radix sort: https://raw.githubusercontent.com/harp-lab/GraphWaGu/main/src/wgsl/create_tree.wgsl and https://raw.githubusercontent.com/harp-lab/GraphWaGu/main/src/wgsl/radix_sort.wgsl -- cosmos.gl / Cosmograph GPU force graph: https://github.com/cosmograph-org/cosmos and https://cosmograph.app/ -- cosmos.gl README says computations and drawing occur on GPU and targets hundreds of thousands of points/links: https://raw.githubusercontent.com/cosmograph-org/cosmos/main/README.md - -## 2. Precise files to add/change - -Add docs/spec only in this run: -- `apps/dashboard/OBSERVATORY-SPEC.md` — this file. - -Implementation files for the Qwen coder later: - -Create: -- `apps/dashboard/src/routes/(app)/observatory/+page.svelte` - - New route only. Do not touch protected `graph/+page.svelte` or `MemoryCinema` wiring. - - Fetch graph data via existing `api.graph` pattern from graph route. - - Parse `?demo=` and `?seed=` with `URLSearchParams`. - - Render full-bleed `ObservatoryCanvas` plus DOM overlay. -- `apps/dashboard/src/lib/components/ObservatoryCanvas.svelte` - - Svelte wrapper with `onMount`/`onDestroy`, canvas bind, ResizeObserver, WebGPU lifecycle. -- `apps/dashboard/src/lib/observatory/engine.ts` - - Owns adapter/device/context, pipelines, buffers, render loop, resize, dispose. -- `apps/dashboard/src/lib/observatory/types.ts` - - CPU-side `ObservatoryNode`, `ObservatoryEdge`, `DemoMode`, `DemoClock`, buffer layout constants. -- `apps/dashboard/src/lib/observatory/demo-clock.ts` - - Deterministic fixed-step clock, seeded PRNG, loop-frame math. -- `apps/dashboard/src/lib/observatory/graph-upload.ts` - - Convert `GraphNode[]`/`GraphEdge[]` to stable node index map, typed arrays, path steps from `planCinemaPath`. -- `apps/dashboard/src/lib/observatory/overlays/TelemetryStrip.svelte` -- `apps/dashboard/src/lib/observatory/overlays/CommandSurface.svelte` -- `apps/dashboard/src/lib/observatory/overlays/TimelineSpine.svelte` -- `apps/dashboard/src/lib/observatory/overlays/InspectorPanel.svelte` -- `apps/dashboard/src/lib/observatory/shaders/simulate.wgsl.ts` -- `apps/dashboard/src/lib/observatory/shaders/render-nodes.wgsl.ts` -- `apps/dashboard/src/lib/observatory/shaders/render-edges.wgsl.ts` -- `apps/dashboard/src/lib/observatory/shaders/render-path.wgsl.ts` -- `apps/dashboard/src/lib/observatory/shaders/post-ripple-shock.wgsl.ts` -- `apps/dashboard/src/lib/observatory/__tests__/demo-clock.test.ts` -- `apps/dashboard/src/lib/observatory/__tests__/graph-upload.test.ts` - -Extend, do not rewrite: -- `apps/dashboard/src/lib/graph/cinema/pathfinder.ts` - - Export or reuse `planCinemaPath` as-is. If extra metadata is needed, add small pure helper `pathToIndexSteps(path, nodeIndexById)` in observatory layer, not inside Cinema. -- `apps/dashboard/src/lib/graph/nodes.ts` - - Reuse `getNodeColor`, `getMemoryState`, `AHAGRAPH_COLORS`. No renderer logic added here. -- `apps/dashboard/src/lib/graph/effects.ts` - - Do not move existing Graph3D effects. Use it as visual reference only. -- `apps/dashboard/src/lib/graph/force-sim.ts` - - Leave CPU simulation intact for Graph3D. Add no WebGPU imports here. Observatory gets its own GPU sim module. - -Do not change: -- `apps/dashboard/src/routes/(app)/graph/+page.svelte` protected `MemoryCinema` block. -- `apps/dashboard/src/lib/components/MemoryCinema.svelte` unless Sam explicitly asks. -- `crates/`, `Cargo.toml`, backend APIs. - -## 3. `?demo=recall-path` priority build instructions - -### 3.1 Route/component structure - -1. Create `routes/(app)/observatory/+page.svelte`. -2. On mount: - - Parse `demo = new URLSearchParams(window.location.search).get('demo') ?? 'recall-path'`. - - Parse `seed = ...get('seed') ?? 'vestige-observatory-v1'`. - - Fetch graph with `api.graph({ max_nodes: 300, depth: 3, sort: 'recent' })` for MVP. - - Pass `nodes`, `edges`, `centerId`, `demo`, `seed` into ``. -3. Render route as a full viewport stage: - - parent `class="fixed inset-0 overflow-hidden bg-[#03040a]"`. - - canvas layer absolute inset-0. - - overlay layer absolute inset-0 pointer-events-none. - - specific interactive controls use `pointer-events-auto`. - -### 3.2 Deterministic demo clock - -Create `demo-clock.ts`: -- `const FPS = 60`. -- `const LOOP_SECONDS = 12` for recall-path; `LOOP_FRAMES = 720`. -- `frame` is an integer, not derived from `Date.now()`. -- In normal interactive mode, rAF accumulates elapsed time and advances fixed steps. In demo mode, each rendered frame advances exactly one fixed frame, or uses `?frame=N` for capture. -- `phase = (frame % LOOP_FRAMES) / LOOP_FRAMES`. -- Use a tiny deterministic PRNG (`xmur3` seed hash + `mulberry32`) in local code. Do not add a dependency for seedrandom unless needed. -- Ban `Math.random()` and `performance.now()` from simulation state. `performance.now()` may schedule frames only; it must not decide positions/colors/path. - -Source rationale: -- Fixed timestep pattern: https://gafferongames.com/post/fix_your_timestep/ -- URL query parsing: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams -- Svelte lifecycle: https://svelte.dev/docs/svelte/lifecycle-hooks - -### 3.3 Graph upload - -Create `graph-upload.ts`: -1. Stable-sort nodes for deterministic indices: - - center node first if `id === centerId`. - - then descending `updatedAt || createdAt`. - - tie-break by `id.localeCompare`. -2. Build `nodeIndexById: Map`. -3. Create `Float32Array nodeData` with 16 floats per node for alignment and future-proofing: - - 0..3: x, y, z, radius - - 4..7: vx, vy, vz, retention - - 8..11: r, g, b, flags - - 12..15: pathIntensity, birthPhase, ripplePhase, shockPhase -4. Initial positions: - - Reuse the golden-angle sphere logic from `NodeManager.createNodes`, but replace randomness with deterministic seed. - - Radius scales from node count: `baseR = clamp(24 + sqrt(n) * 1.2, 35, 140)`. -5. Create `Uint32Array edgeData` with 2 u32 per edge, skipping edges whose node IDs are absent. -6. Call `planCinemaPath(nodes, edges, centerId, 8)` and convert `flowEdges` to `PathStep[]`. -7. Path timing: - - beat 0 at frame 60. - - each next beat +60 frames. - - final 180 frames reserved for afterglow and loop reset. - -### 3.4 WebGPU engine setup - -Create `engine.ts`: -1. Feature detect: - - `if (!navigator.gpu) throw new Error('WebGPU unavailable')` and render a DOM fallback panel. -2. Request adapter/device. -3. Configure canvas: - - `format = navigator.gpu.getPreferredCanvasFormat()`. - - On resize, set `canvas.width = Math.min(clientWidth * devicePixelRatio, maxTextureDimension2D)` and same for height. - - Reconfigure context and recreate depth/scene textures. -4. Create buffers: - - `nodeA`, `nodeB`: `STORAGE | VERTEX | COPY_DST`. - - `edges`: `STORAGE | COPY_DST`. - - `pathSteps`: `STORAGE | COPY_DST`. - - `params`: `UNIFORM | COPY_DST`. - - sprite vertex buffer for a 2-triangle quad or 3-vertex triangle impostor. -5. Create pipelines: - - `simulatePipeline` with read nodeA/write nodeB. - - `renderEdgesPipeline` reads node current + edges. - - `renderNodesPipeline` uses instance index to fetch node state and draw billboard. - - `renderPathPipeline` draws path edges + additive halos. - - `postPipeline` optional in increment 5. -6. Frame loop command order for recall-path MVP: - - write params uniform for integer frame. - - compute simulate: nodeA -> nodeB. - - render scene using nodeB. - - swap nodeA/nodeB. - -### 3.5 WGSL simulation for recall path - -MVP `simulate.wgsl.ts`: -- Inputs: params, previous nodes, next nodes, edges, path steps. -- For each node: - - start with previous `pos`, `vel`. - - apply low-cost centering and edge spring only after the first MVP render works; first render may use static positions. - - compute path intensity by scanning `pathSteps`. - - compute horizon drift from retention. - - write next node state. - -Pseudo-WGSL shape for Qwen: - -```wgsl -struct NodeState { - pos_radius: vec4f, - vel_retention: vec4f, - color_flags: vec4f, - demo: vec4f, -}; -struct Params { - frame: u32, - loopFrames: u32, - nodeCount: u32, - edgeCount: u32, - pathCount: u32, - dt: f32, - time: f32, - _pad: f32, -}; -struct PathStep { source: u32, target: u32, beatFrame: u32, kind: u32 }; - -@group(0) @binding(0) var params: Params; -@group(0) @binding(1) var prevNodes: array; -@group(0) @binding(2) var nextNodes: array; -@group(0) @binding(3) var path: array; - -fn saturate(x: f32) -> f32 { return clamp(x, 0.0, 1.0); } -fn smoothPulse(frame: f32, beat: f32, attack: f32, release: f32) -> f32 { - let a = smoothstep(beat - attack, beat, frame); - let r = 1.0 - smoothstep(beat, beat + release, frame); - return a * r; -} - -@compute @workgroup_size(64) -fn main(@builtin(global_invocation_id) gid: vec3u) { - let i = gid.x; - if (i >= params.nodeCount) { return; } - var node = prevNodes[i]; - let f = f32(params.frame % params.loopFrames); - var recall = 0.0; - for (var p = 0u; p < params.pathCount; p = p + 1u) { - let step = path[p]; - if (step.source == i || step.target == i) { - recall = max(recall, smoothPulse(f, f32(step.beatFrame), 18.0, 150.0)); - } - } - node.demo.x = recall; - nextNodes[i] = node; -} -``` - -### 3.6 Render approach - -Nodes: -- Use instanced billboards. Vertex shader fetches `NodeState` by `@builtin(instance_index)`. -- Billboard size = `radius * (1.0 + recallIntensity * 2.0)`. -- Fragment shader radial alpha: `alpha = smoothstep(1.0, 0.0, length(localUv))`. -- Color = `mix(baseColor, vec3(0.8, 0.95, 1.0), recallIntensity)`. -- Additive glow pass can be same shader with bigger billboard and low alpha. - -Edges: -- MVP: draw edges as line-list if available; better: instanced thin quads from source to target in screen space. -- For path edges, draw a separate additive path pass from `PathStep` only, with wavefront alpha. -- GraphWaGu source pattern fetches nodes in vertex shader by edge endpoint index; use that for WebGPU: https://raw.githubusercontent.com/harp-lab/GraphWaGu/main/src/wgsl/edge_vert.wgsl - -Path lighting: -- At beat frame, target node blooms first. -- During frames `beatFrame..beatFrame+45`, edge from source to target gets a traveling pulse: - - `edgeT = saturate((frame - beatFrame) / 45)`. - - Fragment local coordinate along edge `u`; alpha peak at `abs(u - edgeT) < 0.08`. -- Previous path nodes keep afterglow for 2-3 seconds. - -### 3.7 DOM overlay - -Overlay zones: -- Top telemetry strip: demo mode, seed, node/edge count, frame, fps estimate. -- Left command surface: demo selector buttons (`recall-path`, `birth`, `ripple`, `drift`, `firewall`) and copy demo URL. -- Bottom timeline spine: 720-frame loop with beat tick marks and current frame cursor. -- Right inspector: selected beat/node summary. - -CSS pattern: -- Root stage: `position: fixed; inset: 0; isolation: isolate; overflow: hidden;`. -- Canvas: `position:absolute; inset:0; width:100%; height:100%; display:block; z-index:0;`. -- Overlay: `position:absolute; inset:0; z-index:10; pointer-events:none;`. -- Interactive overlay children: `pointer-events:auto;`. -- Avoid KPI cards. These are instruments: thin glass, monospaced labels, tick marks, command rail. - -Sources: -- MDN `pointer-events`: https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events -- WebGPU canvas resizing: https://webgpufundamentals.org/webgpu/lessons/webgpu-resizing-the-canvas.html -- Svelte lifecycle hooks: https://svelte.dev/docs/svelte/lifecycle-hooks - -## 4. Build task-list with verification increments - -### Increment 1 — route shell and deterministic URL contract - -Build: -- Add `routes/(app)/observatory/+page.svelte`. -- Add minimal overlay shell and fetch graph data. -- Parse `?demo=recall-path&seed=...` and display them. - -Verify: -- Run `pnpm --filter @vestige/dashboard check`. -- Open `/observatory?demo=recall-path&seed=test`. -- See full-bleed dark stage, top telemetry strip, no KPI cards, no console errors. - -### Increment 2 — deterministic clock tests - -Build: -- Add `demo-clock.ts` and tests. -- Fixed 60fps loop, 720-frame period, seeded PRNG. - -Verify: -- `pnpm --filter @vestige/dashboard test -- demo-clock` -- Same seed produces identical first 100 random values and frame phases. -- Different seed changes generated positions. - -### Increment 3 — WebGPU canvas boots and clears - -Build: -- Add `ObservatoryCanvas.svelte` and `engine.ts` minimal WebGPU init. -- Canvas clears to near-black and resizes with DPR clamp. - -Verify: -- `/observatory?demo=recall-path` displays WebGPU canvas. -- Resize browser; canvas remains sharp and fills screen. -- If WebGPU unavailable, route shows a readable fallback instead of crashing. - -### Increment 4 — upload graph and render static nodes - -Build: -- Add `graph-upload.ts` and tests. -- Convert graph nodes into stable indexed `Float32Array`. -- Render node billboards from storage buffer; no simulation yet. - -Verify: -- Test stable node ordering and edge filtering. -- Browser shows 100-300 nodes in deterministic positions. -- Reload same seed: identical node positions. - -### Increment 5 — recall path buffer + node glow - -Build: -- Reuse `planCinemaPath` to create path steps. -- Add compute pass that writes `demo.x` recall intensity. -- Node shader blooms active path nodes. - -Verify: -- `/observatory?demo=recall-path&seed=A` loops every 12s. -- The same sequence lights the same nodes after reload. -- `?seed=B` changes layout but path timing remains stable. - -### Increment 6 — path edge wavefront - -Build: -- Add render-path pipeline drawing additive edge wave traveling source -> target. -- Timeline spine shows beat ticks matching wave arrival. - -Verify: -- Wavefront visibly travels along each path edge. -- At beat arrival, the target node blooms. -- Loop reset is seamless: final afterglow fades before frame 0. - -### Increment 7 — low-cost GPU force sim - -Build: -- Add static edge attraction + centering + damping in compute. -- Keep repulsion disabled or low-count O(N^2) only for <=500 nodes. - -Verify: -- Nodes subtly settle without CPU `ForceSimulation`. -- Performance remains smooth at 300 nodes. -- No CPU per-frame node position loops. - -### Increment 8 — lifecycle effects one by one - -Build/verify separately: -- Born convergence particles: trigger in demo and verify deterministic convergence. -- Backward ripple post pass: verify radial distortion travels effect -> cause. -- Horizon drift/fog: silent/unavailable nodes visibly drift/fade toward horizon. -- Firewall shockwave/membrane: crimson ring expands and fades without breaking path demo. - -### Increment 9 — capture mode - -Build: -- Add `?capture=1` behavior: hide cursor, freeze random seed, optional `?frame=N` exact-frame render. -- Add overlay copy button for canonical demo URL. - -Verify: -- Same URL + frame renders identical screenshot. -- 12-second screen recording loops without discontinuity. - -### Increment 10 — performance guardrails - -Build: -- Add telemetry: CPU frame ms estimate, GPU timestamp query only if supported, node count, buffer mode. -- Cap default demo to 300 real nodes; support synthetic 10k only behind `?stress=10000`. - -Verify: -- No `mapAsync` in the frame loop except delayed optional profiling. -- 300-node demo holds 60fps on Sam's M1 Max. -- Stress mode degrades gracefully and can be disabled by removing query param. - -## 5. Source index - -WebGPU core patterns: -- WebGPU Samples compute boids demo: https://webgpu.github.io/webgpu-samples/samples/computeBoids/ -- compute-boids main TS: https://raw.githubusercontent.com/webgpu/webgpu-samples/main/sample/computeBoids/main.ts -- compute-boids update WGSL: https://raw.githubusercontent.com/webgpu/webgpu-samples/main/sample/computeBoids/updateSprites.wgsl -- compute-boids sprite WGSL: https://raw.githubusercontent.com/webgpu/webgpu-samples/main/sample/computeBoids/sprite.wgsl -- WebGPU storage buffers: https://webgpufundamentals.org/webgpu/lessons/webgpu-storage-buffers.html -- WebGPU compute shaders: https://webgpufundamentals.org/webgpu/lessons/webgpu-compute-shaders.html -- WebGPU canvas resize: https://webgpufundamentals.org/webgpu/lessons/webgpu-resizing-the-canvas.html -- MDN WebGPU API: https://developer.mozilla.org/en-US/docs/Web/API/WebGPU_API - -Particle/lifecycle shader references: -- Particle Life WebGPU article: https://lisyarus.github.io/blog/posts/particle-life-simulation-in-browser-using-webgpu.html -- Particle Life demo: https://lisyarus.github.io/webgpu/particle-life.html -- compute.toys: https://compute.toys/ -- compute.toys repo: https://github.com/compute-toys/compute.toys -- Codrops WebGPU fluids: https://tympanus.net/codrops/2025/02/26/webgpu-fluid-simulations-high-performance-real-time-rendering/ -- Codrops WebGPU particles/fluids: https://tympanus.net/codrops/2025/01/29/particles-progress-and-perseverance-a-journey-into-webgpu-fluids/ -- Codrops Shader.se WebGPU transitions: https://tympanus.net/codrops/2026/05/19/80s-business-tech-seamless-scene-transitions-inside-shader-ses-scroll-driven-webgpu-pipeline/ -- Codrops False Earth WebGPU world: https://tympanus.net/codrops/2026/04/21/false-earth-from-webgl-limits-to-a-webgpu-driven-world/ - -GPU graph layout: -- GraphWaGu repo: https://github.com/harp-lab/GraphWaGu -- GraphWaGu force-directed TS: https://raw.githubusercontent.com/harp-lab/GraphWaGu/main/src/webgpu/force_directed.ts -- GraphWaGu exact forces: https://raw.githubusercontent.com/harp-lab/GraphWaGu/main/src/wgsl/compute_forces.wgsl -- GraphWaGu Barnes-Hut forces: https://raw.githubusercontent.com/harp-lab/GraphWaGu/main/src/wgsl/compute_forcesBH.wgsl -- GraphWaGu apply forces: https://raw.githubusercontent.com/harp-lab/GraphWaGu/main/src/wgsl/apply_forces.wgsl -- GraphWaGu tree build: https://raw.githubusercontent.com/harp-lab/GraphWaGu/main/src/wgsl/create_tree.wgsl -- GraphWaGu radix sort: https://raw.githubusercontent.com/harp-lab/GraphWaGu/main/src/wgsl/radix_sort.wgsl -- GraphWaGu node vertex: https://raw.githubusercontent.com/harp-lab/GraphWaGu/main/src/wgsl/node_vert.wgsl -- GraphWaGu edge vertex: https://raw.githubusercontent.com/harp-lab/GraphWaGu/main/src/wgsl/edge_vert.wgsl -- cosmos.gl repo: https://github.com/cosmograph-org/cosmos -- cosmos.gl README: https://raw.githubusercontent.com/cosmograph-org/cosmos/main/README.md -- Cosmograph product: https://cosmograph.app/ - -Svelte/DOM/determinism: -- Svelte lifecycle hooks: https://svelte.dev/docs/svelte/lifecycle-hooks -- MDN URLSearchParams: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams -- MDN pointer-events: https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events -- Fixed timestep: https://gafferongames.com/post/fix_your_timestep/ -- seedrandom reference if a dependency is later preferred over local PRNG: https://github.com/davidbau/seedrandom - -## 6. Non-negotiable implementation constraints - -- New Observatory route only. Do not mutate the protected Memory Cinema block in `graph/+page.svelte`. -- Keep existing Three/WebGL `Graph3D` intact. Observatory is a parallel WebGPU renderer, not a replacement in v1. -- Do not add dependencies for v1. Use browser WebGPU, Svelte, TypeScript, and existing `three` only if needed for matrix math; prefer small local math helpers. -- No `Math.random()` in demo-visible state. No `Date.now()`/`performance.now()` deciding simulation state. -- No GPU readback in the frame loop. Profiling readback must be delayed and optional. -- Every increment must render something verifiable before moving to the next effect. - ---- - -## 7. VISUAL DNA — "from another dimension" (MANDATORY aesthetic contract) - -Sam's directive (Jul 3 2026): the Observatory must be **mind-boggling** and look like it -**came from another dimension** — the same grammar as the WebGPU waitlist hero and -`docs/launch/causal-brain-demo.html`. This is NOT a dashboard skin. It is a living -tissue. Every increment below inherits this section. Generic = rejected. - -### 7.1 The signature palette (fuse two systems — do not invent a third) - -TWO real palettes already exist in-repo. The Observatory FUSES them: - -- **Meaning layer — FSRS state (from `lib/graph/nodes.ts`, use verbatim):** - - active `#10b981` emerald · dormant `#f59e0b` amber · silent `#8b5cf6` violet · unavailable `#6b7280` slate - - aha `#FFD700` · confusion/failure `#EF4444` · guardrail `#9CA3AF` -- **Transcendence layer — iridescent thin-film (from `causal-brain-demo.html` `spectral(w)`):** - a catmull blend around 4 anchors, w∈[0,1]: - - indigo `vec3(0.20,0.28,0.95)` → cyan-teal `vec3(0.20,0.85,0.90)` → mint `vec3(0.45,1.00,0.72)` → magenta rim `vec3(0.85,0.45,1.00)` - - CORE reads indigo/teal (living tissue); MOTION shifts toward cyan/magenta; NEVER muddy orange. -- **Void:** background `#05060a` (near-black, `color-scheme: dark`). No grey dashboard chrome. -- **Rule:** a node's BASE hue = its FSRS state color. Its ACTIVATION glow (recall, birth, - ignite) rides the thin-film spectral band. So resting graph = meaningful; active graph = otherworldly. - -Port `spectral(w)` to WGSL exactly (4-anchor catmull-ish blend). This is the single most -important visual primitive — it is what makes it look alien instead of corporate. - -### 7.2 Glow / bloom language (copy the demo's exact values, then push further in WebGPU) - -- Additive radial glow blobs (`glow(x,y,rad,col,alpha)` in the demo) → in WebGPU, a real - additive bloom pass. Node halos are `0 0 24px` soft; ignite events drop-shadow at - `0 0 26px rgba(110,240,220,.45)` energy. -- Text/instrument overlay glow: captions `text-shadow:0 0 24px rgba(30,180,255,.35)`; - verdict headline `linear-gradient(90deg,#7fe6c0,#6ef0e6,#a6dcff)` clipped to text with - `drop-shadow(0 0 26px rgba(110,240,220,.45))`; wordmark `letter-spacing:.28em`, `#5dcaa5`. -- **Breath:** a global `pulse = 0.5 + 0.5*sin(t*0.002)` (~0.32Hz) modulates halo intensity so - the whole field breathes even at rest. Living, never static. - -### 7.3 DOM = instrument overlay ONLY (already the spec rule — here is the exact style) - -Copy the demo's overlay contract: `.layer { position:fixed; inset:0; pointer-events:none }` -over a full-bleed `inset:0` canvas. Instruments (TelemetryStrip, CommandSurface, -TimelineSpine, InspectorPanel) are the ONLY DOM, styled like the demo's `.cap.mono` -(SF Mono, `#cfe9ff`), `.verdict` (radial-gradient vignette card), `.wordmark`. NO KPI cards. -NO solid panels. Instruments float, glow faintly, and never block the canvas -(`pointer-events:auto` only on the specific interactive control). - -### 7.4 The reference moment already exists in 2D — port it, don't reinvent - -`causal-brain-demo.html` IS Moment C (salience-rescue / backward-trace) as a 2D canvas -proof: brain point cloud (two lobes, center-dense), a failure flare on the deep right, a -vector-search stall on confounders, then a BACKWARD arrow tracing to the dormant cause on -the deep-left which **ignites**, then a verdict card ("Vestige 60% · vector search 0%"), -then the VESTIGE wordmark. Beat clock: brain forms 0-1.5s, fail fades in ~0.3s, vector -stalls 4-6.5s, backward trace fires 6.5-10.5s, verdict 10.5-13s, signature 13-15s, loop. -The Observatory's `?demo=salience-rescue` must reproduce this exact narrative in real -WebGPU with real memory nodes. Study its `spectral`, `seedBrain`, `glow`, and beat -`schedule` before writing the WGSL. - -### 7.5 Non-negotiable "another dimension" checklist (verifier MUST confirm each) - -- [ ] Field breathes at rest (global pulse), never a static image. -- [ ] Node base hue = FSRS state; activation = thin-film spectral. Both visible. -- [ ] Real additive bloom on ignite/recall (not just opacity). -- [ ] Void `#05060a`, zero grey dashboard chrome, zero KPI cards. -- [ ] Instruments are glowing SF-Mono overlays with `pointer-events:none` except controls. -- [ ] `?demo=salience-rescue` reproduces the causal-brain backward-trace narrative. -- [ ] It looks like living tissue from another dimension, not a SaaS dashboard. If a - reviewer would call it "clean" or "professional," it FAILED. The word is "alive." diff --git a/apps/dashboard/build/_app/immutable/assets/0.Cf27G70K.css b/apps/dashboard/build/_app/immutable/assets/0.Cf27G70K.css deleted file mode 100644 index ed2360d..0000000 --- a/apps/dashboard/build/_app/immutable/assets/0.Cf27G70K.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-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;--angle:0deg;--shine:0%}}}@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-sm:24rem;--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-tight:1.25;--leading-snug:1.375;--leading-relaxed:1.625;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--ease-out:cubic-bezier(0, 0, .2, 1);--ease-in-out:cubic-bezier(.4, 0, .2, 1);--animate-spin:spin 1s linear infinite;--animate-ping:ping 1s cubic-bezier(0, 0, .2, 1) infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--blur-sm:8px;--blur-md:12px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-void:#050510;--color-abyss:#0a0a1a;--color-deep:#10102a;--color-surface:#161638;--color-elevated:#1e1e4a;--color-subtle:#2a2a5e;--color-muted:#4a4a7a;--color-dim:#7a7aaa;--color-text:#e0e0ff;--color-bright:#fff;--color-synapse:#6366f1;--color-synapse-glow:#818cf8;--color-dream:#a855f7;--color-dream-glow:#c084fc;--color-memory:#3b82f6;--color-recall:#10b981;--color-decay:#ef4444;--color-warning:#f59e0b;--color-node-pattern:#ec4899}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.invisible{visibility:hidden}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing) * 0)}.inset-x-0{inset-inline:calc(var(--spacing) * 0)}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.end\!{inset-inline-end:var(--spacing)!important}.top-0{top:calc(var(--spacing) * 0)}.top-0\.5{top:calc(var(--spacing) * .5)}.top-1{top:calc(var(--spacing) * 1)}.top-1\/2{top:50%}.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-0{left:calc(var(--spacing) * 0)}.left-1\/2{left:50%}.left-3\.5{left:calc(var(--spacing) * 3.5)}.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)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.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-block{display:inline-block}.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-3\.5{height:calc(var(--spacing) * 3.5)}.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-11{height:calc(var(--spacing) * 11)}.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-9{min-height:calc(var(--spacing) * 9)}.min-h-10{min-height:calc(var(--spacing) * 10)}.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-11{width:calc(var(--spacing) * 11)}.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-fit{width:fit-content}.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-sm{max-width:var(--container-sm)}.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-10{min-width:calc(var(--spacing) * 10)}.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)}.-translate-y-1\/2{--tw-translate-y: -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)}.scale-150{--tw-scale-x:150%;--tw-scale-y:150%;--tw-scale-z:150%;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}.justify-end{justify-content:flex-end}.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-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-3\.5{gap:calc(var(--spacing) * 3.5)}.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\/25{border-color:#10b98140}@supports (color:color-mix(in lab,red,red)){.border-recall\/25{border-color:color-mix(in oklab,var(--color-recall) 25%,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\/25{border-color:#6366f140}@supports (color:color-mix(in lab,red,red)){.border-synapse\/25{border-color:color-mix(in oklab,var(--color-synapse) 25%,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\.05\]{background-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.bg-white\/\[0\.05\]{background-color:color-mix(in oklab,var(--color-white) 5%,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-14{padding-block:calc(var(--spacing) * 14)}.py-16{padding-block:calc(var(--spacing) * 16)}.py-20{padding-block:calc(var(--spacing) * 20)}.\[padding-top\:max\(0\.75rem\,env\(safe-area-inset-top\)\)\]{padding-top:max(.75rem,env(safe-area-inset-top))}.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}.\[padding-right\:max\(0\.75rem\,env\(safe-area-inset-right\)\)\]{padding-right:max(.75rem,env(safe-area-inset-right))}.pr-1{padding-right:calc(var(--spacing) * 1)}.pr-2{padding-right:calc(var(--spacing) * 2)}.pr-4{padding-right:calc(var(--spacing) * 4)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-16{padding-bottom:calc(var(--spacing) * 16)}.\[padding-left\:max\(0\.75rem\,env\(safe-area-inset-left\)\)\]{padding-left:max(.75rem,env(safe-area-inset-left))}.pl-6{padding-left:calc(var(--spacing) * 6)}.pl-10{padding-left:calc(var(--spacing) * 10)}.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)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.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)}.text-balance{text-wrap:balance}.text-pretty{text-wrap:pretty}.text-wrap{text-wrap:wrap}.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-decay\/80{color:#ef4444cc}@supports (color:color-mix(in lab,red,red)){.text-decay\/80{color:color-mix(in oklab,var(--color-decay) 80%,transparent)}}.text-dim{color:var(--color-dim)}.text-dim\/70{color:#7a7aaab3}@supports (color:color-mix(in lab,red,red)){.text-dim\/70{color:color-mix(in oklab,var(--color-dim) 70%,transparent)}}.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\/50{color:#a855f780}@supports (color:color-mix(in lab,red,red)){.text-dream\/50{color:color-mix(in oklab,var(--color-dream) 50%,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)}.text-warning\/60{color:#f59e0b99}@supports (color:color-mix(in lab,red,red)){.text-warning\/60{color:color-mix(in oklab,var(--color-warning) 60%,transparent)}}.text-warning\/70{color:#f59e0bb3}@supports (color:color-mix(in lab,red,red)){.text-warning\/70{color:color-mix(in oklab,var(--color-warning) 70%,transparent)}}.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-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.opacity-80{opacity:.8}.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_-4px_var\(--color-synapse-glow\)\]{--tw-shadow:0 0 16px -4px var(--tw-shadow-color,var(--color-synapse-glow));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,)}.drop-shadow{--tw-drop-shadow-size:drop-shadow(0 1px 2px var(--tw-drop-shadow-color,#0000001a)) drop-shadow(0 1px 1px var(--tw-drop-shadow-color,#0000000f));--tw-drop-shadow:drop-shadow(0 1px 2px #0000001a) drop-shadow(0 1px 1px #0000000f);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}@media(hover:hover){.group-hover\:scale-110:is(:where(.group):hover *){--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x) var(--tw-scale-y)}}.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\:rotate-180:hover{rotate:180deg}.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-decay\/\[0\.06\]:hover{background-color:#ef44440f}@supports (color:color-mix(in lab,red,red)){.hover\:bg-decay\/\[0\.06\]:hover{background-color:color-mix(in oklab,var(--color-decay) 6%,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\/15:focus{--tw-ring-color:#6366f126}@supports (color:color-mix(in lab,red,red)){.focus\:ring-synapse\/15:focus{--tw-ring-color:color-mix(in oklab, var(--color-synapse) 15%, transparent)}}.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\:top-4{top:calc(var(--spacing) * 4)}.sm\:right-4{right:calc(var(--spacing) * 4)}.sm\:left-4{left:calc(var(--spacing) * 4)}.sm\:ml-auto{margin-left:auto}.sm\:block{display:block}.sm\:inline-flex{display:inline-flex}.sm\:w-auto{width:auto}.sm\:max-w-md{max-width:var(--container-md)}.sm\:flex-1{flex:1}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:gap-3{gap:calc(var(--spacing) * 3)}.sm\:p-0{padding:calc(var(--spacing) * 0)}.sm\:\[padding-top\:0\]{padding-top:0}.sm\:\[padding-right\:0\]{padding-right:0}.sm\:\[padding-left\:0\]{padding-left:0}.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\:inline-flex{display:inline-flex}.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)}@supports (color:oklch(0 0 0)){:root{--color-synapse:oklch(58.5% .222 277);--color-synapse-glow:oklch(68.5% .169 277);--color-dream:oklch(62.7% .265 304);--color-dream-glow:oklch(71.4% .203 305);--color-memory:oklch(62.3% .214 259);--color-recall:oklch(69.6% .17 162);--color-decay:oklch(63.7% .237 25);--color-warning:oklch(76.9% .188 70);--color-node-fact:oklch(62.3% .214 259);--color-node-concept:oklch(60.6% .25 292);--color-node-event:oklch(76.9% .188 70);--color-node-person:oklch(69.6% .17 162);--color-node-place:oklch(71.5% .143 215);--color-node-note:oklch(55.1% .027 264);--color-node-pattern:oklch(65.6% .241 354);--color-node-decision:oklch(63.7% .237 25)}}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)}@media not all and (prefers-reduced-motion:reduce){::view-transition-old(root){animation-duration:.18s;animation-timing-function:ease}::view-transition-new(root){animation-duration:.18s;animation-timing-function:ease}::view-transition-old(root){animation-name:vt-fade-out}::view-transition-new(root){animation-name:vt-fade-in}@keyframes vt-fade-out{0%{opacity:1}to{opacity:0}}@keyframes vt-fade-in{0%{opacity:0}to{opacity:1}}}@property --angle{syntax:"";inherits:false;initial-value:0deg}@property --shine{syntax:"";inherits:false;initial-value:0%}.reveal{opacity:0;transform:translateY(var(--reveal-y,16px));transition:opacity .55s cubic-bezier(.22,1,.36,1),transform .55s cubic-bezier(.22,1,.36,1);transition-delay:var(--reveal-delay,0s);will-change:opacity,transform}.reveal-in{opacity:1;transform:none}@media(prefers-reduced-motion:reduce){.reveal{opacity:1;transition:none;transform:none}}@media not all and (prefers-reduced-motion:reduce){.enter{transition:opacity .4s,transform .4s cubic-bezier(.22,1,.36,1)}@starting-style{.enter{opacity:0;transform:translateY(10px)}}}.live-border{isolation:isolate;position:relative}.live-border:before{content:"";border-radius:inherit;background:conic-gradient(from var(--angle),transparent 0%,var(--color-synapse) 18%,var(--color-dream) 33%,transparent 50%,transparent 100%);pointer-events:none;opacity:.6;z-index:-1;padding:1px;position:absolute;top:-1px;right:-1px;bottom:-1px;left:-1px;-webkit-mask-image:linear-gradient(#000 0 0),linear-gradient(#000 0 0);-webkit-mask-position:0 0,0 0;-webkit-mask-size:auto,auto;-webkit-mask-repeat:repeat,repeat;-webkit-mask-clip:content-box,border-box;-webkit-mask-origin:content-box,border-box;-webkit-mask-composite:xor;mask-composite:exclude;-webkit-mask-source-type:auto,auto;mask-mode:match-source,match-source}@media not all and (prefers-reduced-motion:reduce){.live-border:before{animation:6s linear infinite border-rotate}@keyframes border-rotate{to{--angle:360deg}}}.spotlight-surface{position:relative;overflow:hidden}.spotlight-surface:after{content:"";pointer-events:none;background:radial-gradient(340px circle at var(--spot-x,50%) var(--spot-y,50%),#818cf81f,transparent 60%);opacity:var(--spot-o,0);z-index:0;transition:opacity .3s;position:absolute;top:0;right:0;bottom:0;left:0}.tilt-glare{position:relative;overflow:hidden}.tilt-glare:after{content:"";pointer-events:none;background:radial-gradient(circle at var(--glare-x,50%) var(--glare-y,50%),#ffffff24,transparent 45%);opacity:var(--glare-o,0);transition:opacity .3s;position:absolute;top:0;right:0;bottom:0;left:0}.breathe{animation:3.2s ease-in-out infinite breathe}@keyframes breathe{0%,to{filter:drop-shadow(0 0 2px);opacity:.85;transform:scale(1)}50%{filter:drop-shadow(0 0 7px);opacity:1;transform:scale(1.18)}}@media(prefers-reduced-motion:reduce){.breathe{animation:none}}.ping-host{position:relative}.ping-host:before{content:"";opacity:.6;z-index:-1;background:currentColor;border-radius:50%;position:absolute;top:0;right:0;bottom:0;left:0}@media not all and (prefers-reduced-motion:reduce){.ping-host:before{animation:2.4s cubic-bezier(0,0,.2,1) infinite ping}@keyframes ping{0%{opacity:.5;transform:scale(1)}80%,to{opacity:0;transform:scale(2.6)}}}.shimmer{background:#ffffff08;position:relative;overflow:hidden}.shimmer:after{content:"";background:linear-gradient(90deg,#0000,#818cf81f,#0000);position:absolute;top:0;right:0;bottom:0;left:0;transform:translate(-100%)}@media not all and (prefers-reduced-motion:reduce){.shimmer:after{animation:1.6s ease-in-out infinite shimmer}@keyframes shimmer{to{transform:translate(100%)}}}.text-aurora{background:linear-gradient(100deg,var(--color-synapse-glow),var(--color-dream-glow),var(--color-recall),var(--color-synapse-glow));color:#0000;background-size:250% 100%;-webkit-background-clip:text;background-clip:text}@media not all and (prefers-reduced-motion:reduce){.text-aurora{animation:8s ease-in-out infinite aurora-drift}@keyframes aurora-drift{0%,to{background-position:0%}50%{background-position:100%}}}.lift{transition:transform .28s cubic-bezier(.34,1.56,.64,1),box-shadow .28s,border-color .28s}.lift:hover{transform:translateY(-3px);box-shadow:0 12px 32px #00000059,0 0 0 1px #6366f12e}@media(prefers-reduced-motion:reduce){.lift:hover{transform:none}}.tabular-nums{font-variant-numeric:tabular-nums;font-feature-settings:"tnum"}:where(button,a,input,select,[role=button],[tabindex]):focus-visible{outline:2px solid var(--color-synapse-glow);outline-offset:2px;border-radius:.4rem}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}}@keyframes ping{75%,to{opacity:0;transform:scale(2)}}@keyframes pulse{50%{opacity:.5}}.toast-layer.svelte-pry2ep{position:fixed;z-index:60;pointer-events:none;display:flex;flex-direction:column;gap:.5rem;right:1.25rem;bottom:1.25rem;max-width:22rem;width:calc(100vw - 2.5rem)}@media(max-width:768px){.toast-layer.svelte-pry2ep{right:.75rem;left:.75rem;bottom:auto;top:5.25rem;max-width:none;width:auto;align-items:stretch}}.toast-item.svelte-pry2ep{pointer-events:auto;position:relative;display:flex;gap:.75rem;align-items:stretch;text-align:left;font:inherit;color:inherit;background:#0c0e16b8;backdrop-filter:blur(14px) saturate(160%);-webkit-backdrop-filter:blur(14px) saturate(160%);border:1px solid rgba(255,255,255,.06);border-radius:.75rem;padding:.75rem .9rem .75rem .5rem;overflow:hidden;box-shadow:0 10px 40px -12px #000c,0 0 22px -6px var(--toast-color);cursor:pointer;animation:svelte-pry2ep-toast-in .32s cubic-bezier(.16,1,.3,1);transform-origin:right center;transition:transform .15s ease,box-shadow .15s ease}.toast-item.svelte-pry2ep:hover{transform:translateY(-1px) scale(1.015);box-shadow:0 14px 48px -12px #000000d9,0 0 32px -4px var(--toast-color)}.toast-item.svelte-pry2ep:focus-visible{outline:1px solid var(--toast-color);outline-offset:2px}.toast-accent.svelte-pry2ep{width:3px;border-radius:2px;background:var(--toast-color);box-shadow:0 0 10px var(--toast-color);flex-shrink:0}.toast-body.svelte-pry2ep{display:flex;flex-direction:column;gap:.15rem;flex:1;min-width:0}.toast-head.svelte-pry2ep{display:flex;align-items:center;gap:.5rem}.toast-icon.svelte-pry2ep{color:var(--toast-color);font-size:.95rem;text-shadow:0 0 8px var(--toast-color);line-height:1;width:1rem;display:inline-flex;justify-content:center}.toast-title.svelte-pry2ep{color:#f5f5fa;font-size:.82rem;font-weight:600;letter-spacing:.01em}.toast-sub.svelte-pry2ep{color:#b0b6c4;font-size:.74rem;line-height:1.35;padding-left:1.5rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.toast-progress.svelte-pry2ep{position:absolute;left:0;bottom:0;height:2px;width:100%;background:#ffffff0a}.toast-progress-fill.svelte-pry2ep{height:100%;background:var(--toast-color);opacity:.55;transform-origin:left center;animation:svelte-pry2ep-toast-progress var(--toast-dwell) linear forwards}.toast-item.svelte-pry2ep:hover .toast-progress-fill:where(.svelte-pry2ep),.toast-item.svelte-pry2ep:focus-visible .toast-progress-fill:where(.svelte-pry2ep){animation-play-state:paused}@keyframes svelte-pry2ep-toast-in{0%{opacity:0;transform:translate(24px) scale(.98)}to{opacity:1;transform:translate(0) scale(1)}}@media(max-width:768px){.toast-item.svelte-pry2ep{transform-origin:top center;animation:svelte-pry2ep-toast-in-mobile .3s cubic-bezier(.16,1,.3,1)}}@keyframes svelte-pry2ep-toast-in-mobile{0%{opacity:0;transform:translateY(-12px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}@keyframes svelte-pry2ep-toast-progress{0%{transform:scaleX(1)}to{transform:scaleX(0)}}@media(prefers-reduced-motion:reduce){.toast-item.svelte-pry2ep{animation:none}.toast-progress-fill.svelte-pry2ep{animation:none;transform:scaleX(.5)}}.strip-item.svelte-1kk3799{display:inline-flex;align-items:center;gap:.4rem;padding:0 .75rem;white-space:nowrap;flex-shrink:0}.strip-divider.svelte-1kk3799{width:1px;height:14px;background:#6366f11f;flex-shrink:0}.ambient-strip.ambient-flash.svelte-1kk3799{background:linear-gradient(90deg,#ef444414,#ef444400 70%),#0006;border-bottom-color:#ef444459;transition:background .3s ease,border-color .3s ease}@keyframes svelte-1kk3799-ping-slow{0%{transform:scale(1);opacity:.8}80%,to{transform:scale(2);opacity:0}}.animate-ping-slow{animation:svelte-1kk3799-ping-slow 2.2s cubic-bezier(0,0,.2,1) infinite}@media(prefers-reduced-motion:reduce){.ambient-strip.svelte-1kk3799 .animate-ping,.ambient-strip.svelte-1kk3799 .animate-ping-slow,.ambient-strip.svelte-1kk3799 .animate-pulse{animation:none!important}}.verdict-bar.svelte-1j425e6{border-bottom:1px solid rgba(255,255,255,.06);background:#080912b8;backdrop-filter:blur(18px) saturate(160%);-webkit-backdrop-filter:blur(18px) saturate(160%);box-shadow:0 6px 24px #0000002e}.verdict-summary.svelte-1j425e6{width:100%;min-height:2.75rem;display:grid;grid-template-columns:auto auto minmax(0,1fr) auto;align-items:center;gap:.75rem;padding:.55rem 1rem;color:var(--color-text);text-align:left;font:inherit}.sr-only.svelte-1j425e6{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.label.svelte-1j425e6,.field-label.svelte-1j425e6,.appeal-row.svelte-1j425e6>span:where(.svelte-1j425e6){color:var(--color-dim);font-size:.68rem;text-transform:uppercase;letter-spacing:0}.levels.svelte-1j425e6{display:flex;align-items:center;gap:.25rem}.levels.svelte-1j425e6 span:where(.svelte-1j425e6){border:1px solid rgba(255,255,255,.08);border-radius:.35rem;padding:.18rem .38rem;color:var(--color-muted);font-size:.64rem;line-height:1}.levels.svelte-1j425e6 span.active:where(.svelte-1j425e6){color:var(--color-bright);border-color:var(--verdict-color);background:color-mix(in srgb,var(--verdict-color) 18%,transparent);box-shadow:0 0 14px color-mix(in srgb,var(--verdict-color) 28%,transparent)}.summary-text.svelte-1j425e6{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.78rem;color:var(--color-dim)}.when.svelte-1j425e6{color:var(--color-muted);font-size:.68rem}.receipt.svelte-1j425e6{margin:0 1rem .75rem;border:1px solid rgba(255,255,255,.08);border-radius:.5rem;background:#0a0a1ac7;padding:.85rem}.receipt-grid.svelte-1j425e6{display:grid;grid-template-columns:minmax(0,1.4fr) minmax(10rem,.8fr) minmax(0,1.1fr) minmax(0,1.1fr);gap:.85rem}.receipt.svelte-1j425e6 p:where(.svelte-1j425e6),.receipt.svelte-1j425e6 li:where(.svelte-1j425e6){margin:.25rem 0 0;color:var(--color-text);font-size:.76rem;line-height:1.45;overflow-wrap:anywhere}.receipt.svelte-1j425e6 ul:where(.svelte-1j425e6){margin:.25rem 0 0;padding-left:1rem}.appeal-row.svelte-1j425e6{display:flex;align-items:center;gap:.4rem;margin-top:.85rem;flex-wrap:wrap}.appeal-row.svelte-1j425e6 button:where(.svelte-1j425e6),.appeal-row.svelte-1j425e6 p:where(.svelte-1j425e6){border:1px solid rgba(255,255,255,.1);border-radius:.4rem;padding:.35rem .6rem;color:var(--color-text);background:#ffffff0a;font-size:.72rem;margin:0}.appeal-row.svelte-1j425e6 button:where(.svelte-1j425e6):hover:not(:disabled),.verdict-summary.svelte-1j425e6:hover{background:#ffffff0d}.appeal-row.svelte-1j425e6 button:where(.svelte-1j425e6):disabled{opacity:.55;cursor:wait}.tone-pass.svelte-1j425e6,.tone-note.svelte-1j425e6{--verdict-color: #10b981}.tone-caution.svelte-1j425e6{--verdict-color: #f59e0b}.tone-veto.svelte-1j425e6{--verdict-color: #ef4444}.tone-appealed.svelte-1j425e6{--verdict-color: #818cf8}@media(max-width:900px){.verdict-summary.svelte-1j425e6{grid-template-columns:auto minmax(0,1fr) auto}.levels.svelte-1j425e6{grid-column:1 / -1;order:4;overflow-x:auto;padding-bottom:.1rem}.receipt-grid.svelte-1j425e6{grid-template-columns:1fr}}.theme-toggle.svelte-1cmi4dh{width:30px;height:30px;display:inline-flex;align-items:center;justify-content:center;padding:0;border-radius:8px;background:#6366f10f;border:1px solid rgba(99,102,241,.14);color:var(--color-text);cursor:pointer;transition:background .2s ease,border-color .2s ease,color .2s ease,transform .12s ease;-webkit-tap-highlight-color:transparent}.theme-toggle.svelte-1cmi4dh:hover{background:#6366f124;border-color:#6366f14d;color:var(--color-bright)}.theme-toggle.svelte-1cmi4dh:active{transform:scale(.94)}.theme-toggle.svelte-1cmi4dh:focus-visible{outline:1px solid var(--color-synapse);outline-offset:2px}.icon-wrap.svelte-1cmi4dh{position:relative;width:18px;height:18px;display:inline-block}.icon.svelte-1cmi4dh{position:absolute;top:0;right:0;bottom:0;left:0;width:18px;height:18px;opacity:0;transform:scale(.7) rotate(-30deg);transition:opacity .2s ease,transform .2s cubic-bezier(.16,1,.3,1);pointer-events:none}.icon.active.svelte-1cmi4dh{opacity:1;transform:scale(1) rotate(0)}.theme-toggle[data-mode=dark].svelte-1cmi4dh{color:var(--color-synapse-glow, #818cf8)}.theme-toggle[data-mode=light].svelte-1cmi4dh{color:var(--color-warning, #f59e0b)}.theme-toggle[data-mode=auto].svelte-1cmi4dh{color:var(--color-dream-glow, #c084fc)}@media(prefers-reduced-motion:reduce){.theme-toggle.svelte-1cmi4dh,.icon.svelte-1cmi4dh{transition:none}}.safe-bottom.svelte-12qhfyh{padding-bottom:env(safe-area-inset-bottom,0px)}.logo-mark.svelte-12qhfyh{transition:transform .3s cubic-bezier(.34,1.56,.64,1),box-shadow .3s ease}.logo-link.svelte-12qhfyh:hover .logo-mark:where(.svelte-12qhfyh){transform:rotate(-6deg) scale(1.08);box-shadow:0 0 0 1px #818cf866,0 0 22px #6366f180}.nav-link.text-synapse-glow.svelte-12qhfyh .nav-icon:where(.svelte-12qhfyh) svg,.nav-active-border.svelte-12qhfyh .nav-icon:where(.svelte-12qhfyh) svg{filter:drop-shadow(0 0 6px rgba(129,140,248,.55))} diff --git a/apps/dashboard/build/_app/immutable/assets/0.Cf27G70K.css.br b/apps/dashboard/build/_app/immutable/assets/0.Cf27G70K.css.br deleted file mode 100644 index bc588d0..0000000 Binary files a/apps/dashboard/build/_app/immutable/assets/0.Cf27G70K.css.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/assets/0.Cf27G70K.css.gz b/apps/dashboard/build/_app/immutable/assets/0.Cf27G70K.css.gz deleted file mode 100644 index 30b4fce..0000000 Binary files a/apps/dashboard/build/_app/immutable/assets/0.Cf27G70K.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..66bc820 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/10.g4OzM5ih.css b/apps/dashboard/build/_app/immutable/assets/10.g4OzM5ih.css deleted file mode 100644 index 7804d0e..0000000 --- a/apps/dashboard/build/_app/immutable/assets/10.g4OzM5ih.css +++ /dev/null @@ -1 +0,0 @@ -@keyframes svelte-1n9p0vm-fade-in{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.animate-fade-in.svelte-1n9p0vm{animation:svelte-1n9p0vm-fade-in .3s ease-out}.live-status.svelte-1o4vd58{display:inline-flex;align-items:center;gap:.5rem;padding:.3rem .7rem;border-radius:999px;font-size:.75rem;font-weight:600;color:var(--color-recall, #34d399);background:color-mix(in srgb,var(--color-recall, #34d399) 12%,transparent);border:1px solid color-mix(in srgb,var(--color-recall, #34d399) 28%,transparent);white-space:nowrap}.live-status.idle.svelte-1o4vd58{color:var(--color-dim, #8b95a5);background:#ffffff0a;border-color:#ffffff14}.live-dot.svelte-1o4vd58{display:inline-block;flex-shrink:0}.count-chip.svelte-1o4vd58{display:inline-flex;align-items:baseline;gap:.35rem;padding:.3rem .7rem;border-radius:999px;font-size:.78rem;background:#ffffff08;border:1px solid rgba(255,255,255,.07)}.clear-btn.svelte-1o4vd58{display:inline-flex;align-items:center;gap:.3rem;padding:.3rem .65rem;border-radius:999px;font-size:.75rem;color:var(--color-muted, #6b7280);background:#ffffff05;border:1px solid rgba(255,255,255,.06);transition:color .2s ease,background .2s ease,border-color .2s ease}.clear-btn.svelte-1o4vd58:hover:not(:disabled){color:var(--color-text, #e5e7eb);background:#ffffff0d;border-color:#ffffff1f}.clear-btn.svelte-1o4vd58:disabled{opacity:.4;cursor:not-allowed}.event-row.svelte-1o4vd58{position:relative;transition:transform .28s cubic-bezier(.34,1.56,.64,1),box-shadow .28s ease,background .2s ease}.event-row.svelte-1o4vd58:hover{background:color-mix(in srgb,var(--evt) 7%,transparent)}.event-icon.svelte-1o4vd58{width:1.6rem;height:1.6rem;border-radius:.5rem}.empty-glyph.svelte-1o4vd58{display:inline-flex;align-items:center;justify-content:center;color:var(--color-synapse-glow, #818cf8);opacity:.85}@media not (prefers-reduced-motion:reduce){.empty-glyph.svelte-1o4vd58{animation:breathe 3.2s ease-in-out infinite}} diff --git a/apps/dashboard/build/_app/immutable/assets/10.g4OzM5ih.css.br b/apps/dashboard/build/_app/immutable/assets/10.g4OzM5ih.css.br deleted file mode 100644 index c189541..0000000 Binary files a/apps/dashboard/build/_app/immutable/assets/10.g4OzM5ih.css.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/assets/10.g4OzM5ih.css.gz b/apps/dashboard/build/_app/immutable/assets/10.g4OzM5ih.css.gz deleted file mode 100644 index e80bd27..0000000 Binary files a/apps/dashboard/build/_app/immutable/assets/10.g4OzM5ih.css.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/assets/11.BxoW8Jf1.css b/apps/dashboard/build/_app/immutable/assets/11.BxoW8Jf1.css deleted file mode 100644 index 79f5904..0000000 --- a/apps/dashboard/build/_app/immutable/assets/11.BxoW8Jf1.css +++ /dev/null @@ -1 +0,0 @@ -.cinema-overlay.svelte-1uwqs3k{position:fixed;top:0;right:0;bottom:0;left:0;z-index:200;background:radial-gradient(circle at 50% 40%,#05050f,#010108);display:flex;flex-direction:column}body.cinema-open .graph-stats-pill{display:none}.cinema-canvas.svelte-1uwqs3k{position:absolute;top:0;right:0;bottom:0;left:0;z-index:0}.cinema-top.svelte-1uwqs3k{position:relative;z-index:2;display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:max(.75rem,env(safe-area-inset-top)) 1rem .75rem;flex-wrap:wrap}.cinema-badge.svelte-1uwqs3k{font-size:.65rem;padding:.1rem .45rem;border-radius:999px;border:1px solid rgba(129,140,248,.4);color:var(--color-synapse-glow)}.cinema-badge-gpu.svelte-1uwqs3k{border-color:#14e8c680;color:#14e8c6}.cinema-act.svelte-1uwqs3k{font-size:.6rem;letter-spacing:.12em;text-transform:uppercase;color:var(--color-dream-glow);opacity:.85}.cinema-plan-card.svelte-1uwqs3k{position:absolute;z-index:3;top:50%;left:50%;transform:translate(-50%,-50%);max-width:520px;padding:1.5rem 1.75rem;border-radius:16px;text-align:center;animation:svelte-1uwqs3k-cinema-plan-in .5s ease both}@keyframes svelte-1uwqs3k-cinema-plan-in{0%{opacity:0;transform:translate(-50%,-46%)}to{opacity:1;transform:translate(-50%,-50%)}}.cinema-plan-kicker.svelte-1uwqs3k{font-size:.65rem;letter-spacing:.18em;text-transform:uppercase;color:var(--color-synapse-glow);margin-bottom:.5rem}.cinema-plan-logline.svelte-1uwqs3k{font-size:clamp(1.05rem,2.2vw,1.4rem);line-height:1.5;color:var(--color-bright);margin:0}.cinema-note.svelte-1uwqs3k{font-size:.78rem;color:var(--color-synapse-glow);opacity:.85;margin:0 0 .6rem;font-style:italic}.cinema-dot.svelte-1uwqs3k{width:8px;height:8px;border-radius:50%;background:var(--color-muted)}.cinema-dot.active.svelte-1uwqs3k{background:#14e8c6;box-shadow:0 0 10px #14e8c6}.cinema-toggle.svelte-1uwqs3k{font-size:.7rem;color:var(--color-dim);display:flex;align-items:center;gap:.3rem;cursor:pointer}.cinema-close.svelte-1uwqs3k{background:transparent;border:1px solid rgba(255,255,255,.15);color:var(--color-text);border-radius:8px;width:32px;height:32px;cursor:pointer}.cinema-caption-wrap.svelte-1uwqs3k{position:relative;z-index:2;margin-top:auto;padding:1rem 1.25rem max(1.25rem,env(safe-area-inset-bottom));max-width:720px}.cinema-chip.svelte-1uwqs3k{display:inline-block;font-size:.65rem;text-transform:uppercase;letter-spacing:.08em;color:var(--color-dream-glow);margin-bottom:.35rem}.cinema-caption.svelte-1uwqs3k{font-size:clamp(1.05rem,2.4vw,1.6rem);line-height:1.45;color:var(--color-bright);text-shadow:0 2px 24px rgba(0,0,0,.9);min-height:2.6em;margin:0 0 .75rem}.cinema-progress.svelte-1uwqs3k{height:3px;background:#ffffff1a;border-radius:3px;overflow:hidden}.cinema-progress-fill.svelte-1uwqs3k{height:100%;background:linear-gradient(90deg,var(--color-synapse),color-mix(in oklch,var(--color-dream),#ff2d55 calc(var(--tension, 0) * 100%)));transition:width .2s linear,background .4s ease}.cinema-beatcount.svelte-1uwqs3k{margin-top:.4rem;display:flex;gap:.75rem;align-items:center}.cinema-replay.svelte-1uwqs3k{background:transparent;border:1px solid rgba(129,140,248,.4);color:var(--color-synapse-glow);border-radius:999px;padding:.15rem .7rem;cursor:pointer;font-size:.75rem}.cinema-dream.svelte-1uwqs3k{color:var(--color-dream-glow);letter-spacing:.08em;animation:svelte-1uwqs3k-cinema-dream-pulse 3s ease-in-out infinite}.cinema-restore.svelte-1uwqs3k{position:absolute;bottom:.6rem;right:.8rem;z-index:95;background:#0a0a1a80;border:1px solid rgba(129,140,248,.25);color:var(--color-muted);border-radius:999px;padding:.2rem .7rem;font-size:.7rem;letter-spacing:.04em;cursor:pointer;opacity:.4;transition:opacity .2s ease}.cinema-restore.svelte-1uwqs3k:hover{opacity:1}@keyframes svelte-1uwqs3k-cinema-dream-pulse{0%,to{opacity:.55}50%{opacity:1}}@media(prefers-reduced-motion:reduce){.cinema-progress-fill.svelte-1uwqs3k{transition:none}} diff --git a/apps/dashboard/build/_app/immutable/assets/11.BxoW8Jf1.css.br b/apps/dashboard/build/_app/immutable/assets/11.BxoW8Jf1.css.br deleted file mode 100644 index 9c07b61..0000000 Binary files a/apps/dashboard/build/_app/immutable/assets/11.BxoW8Jf1.css.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/assets/11.BxoW8Jf1.css.gz b/apps/dashboard/build/_app/immutable/assets/11.BxoW8Jf1.css.gz deleted file mode 100644 index dfc3505..0000000 Binary files a/apps/dashboard/build/_app/immutable/assets/11.BxoW8Jf1.css.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/assets/14.Bjd0S47S.css b/apps/dashboard/build/_app/immutable/assets/13.Bjd0S47S.css similarity index 100% rename from apps/dashboard/build/_app/immutable/assets/14.Bjd0S47S.css rename to apps/dashboard/build/_app/immutable/assets/13.Bjd0S47S.css diff --git a/apps/dashboard/build/_app/immutable/assets/14.Bjd0S47S.css.br b/apps/dashboard/build/_app/immutable/assets/13.Bjd0S47S.css.br similarity index 100% rename from apps/dashboard/build/_app/immutable/assets/14.Bjd0S47S.css.br rename to apps/dashboard/build/_app/immutable/assets/13.Bjd0S47S.css.br diff --git a/apps/dashboard/build/_app/immutable/assets/13.Bjd0S47S.css.gz b/apps/dashboard/build/_app/immutable/assets/13.Bjd0S47S.css.gz new file mode 100644 index 0000000..84a3745 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/assets/13.Bjd0S47S.css.gz differ diff --git a/apps/dashboard/build/_app/immutable/assets/14.Bjd0S47S.css.gz b/apps/dashboard/build/_app/immutable/assets/14.Bjd0S47S.css.gz deleted file mode 100644 index 6ec156c..0000000 Binary files a/apps/dashboard/build/_app/immutable/assets/14.Bjd0S47S.css.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/assets/15.BzuIe_Oj.css b/apps/dashboard/build/_app/immutable/assets/15.BzuIe_Oj.css deleted file mode 100644 index ceee09b..0000000 --- a/apps/dashboard/build/_app/immutable/assets/15.BzuIe_Oj.css +++ /dev/null @@ -1 +0,0 @@ -.pending-badge.svelte-c1wbiz{font-size:.8rem;font-weight:700;padding:5px 11px;border-radius:8px;color:var(--color-text-dim, #8b8ba7);border:1px solid color-mix(in oklab,white 10%,transparent)}.pending-badge.has.svelte-c1wbiz{color:#f59e0b;border-color:color-mix(in oklab,#f59e0b 40%,transparent);background:color-mix(in oklab,#f59e0b 10%,transparent)}.manifesto.svelte-c1wbiz{font-size:1.05rem;line-height:1.55;color:var(--color-text, #e2e2f0);padding:16px 20px;border-radius:12px;margin-bottom:16px;background:linear-gradient(100deg,color-mix(in oklab,var(--color-synapse) 14%,transparent),transparent 70%);border:1px solid color-mix(in oklab,var(--color-synapse) 20%,transparent)}.manifesto.svelte-c1wbiz strong:where(.svelte-c1wbiz){color:var(--color-synapse-glow, #818cf8)}.manifesto-note.svelte-c1wbiz{display:block;margin-top:8px;font-size:.82rem;line-height:1.5;color:var(--color-text-dim, #c0c0d8)}.manifesto-note.svelte-c1wbiz strong:where(.svelte-c1wbiz){color:#f59e0b}.glass.svelte-c1wbiz{background:color-mix(in oklab,var(--color-void, #050510) 55%,transparent);border:1px solid color-mix(in oklab,white 8%,transparent);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);border-radius:14px}.modes.svelte-c1wbiz{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;padding:8px;margin-bottom:14px}@media(max-width:640px){.modes.svelte-c1wbiz{grid-template-columns:1fr}}.mode.svelte-c1wbiz{display:flex;flex-direction:column;gap:3px;padding:11px 14px;border-radius:10px;border:1px solid transparent;background:color-mix(in oklab,white 3%,transparent);cursor:pointer;text-align:left;transition:all .16s ease}.mode.svelte-c1wbiz:hover{background:color-mix(in oklab,var(--color-synapse) 10%,transparent)}.mode.on.svelte-c1wbiz{border-color:color-mix(in oklab,var(--color-synapse) 50%,transparent);background:color-mix(in oklab,var(--color-synapse) 18%,transparent)}.mode-label.svelte-c1wbiz{font-weight:700;font-size:.9rem}.mode-blurb.svelte-c1wbiz{font-size:.72rem;color:var(--color-text-dim, #8b8ba7);line-height:1.35}.filters.svelte-c1wbiz{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px}.filter.svelte-c1wbiz{font-size:.74rem;padding:5px 11px;border-radius:7px;border:1px solid color-mix(in oklab,white 8%,transparent);background:transparent;color:var(--color-text-dim, #8b8ba7);cursor:pointer;text-transform:capitalize;transition:all .16s ease}.filter.svelte-c1wbiz:hover{color:var(--color-text, #e2e2f0)}.filter.on.svelte-c1wbiz{color:var(--color-synapse-glow, #818cf8);border-color:color-mix(in oklab,var(--color-synapse) 45%,transparent);background:color-mix(in oklab,var(--color-synapse) 12%,transparent)}.layout.svelte-c1wbiz{display:grid;grid-template-columns:300px 1fr;gap:16px;align-items:start}@media(max-width:860px){.layout.svelte-c1wbiz{grid-template-columns:1fr}}.pr-list.svelte-c1wbiz{padding:12px;position:sticky;top:16px;max-height:calc(100vh - 40px);overflow-y:auto}.pr-list.svelte-c1wbiz ul:where(.svelte-c1wbiz){list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px}.empty.svelte-c1wbiz{font-size:.82rem;color:var(--color-text-dim, #8b8ba7);line-height:1.5;padding:8px}.pr-row.svelte-c1wbiz{width:100%;text-align:left;padding:10px 12px;border-radius:10px;border:1px solid transparent;background:color-mix(in oklab,white 3%,transparent);cursor:pointer;transition:all .16s ease}.pr-row.svelte-c1wbiz:hover{background:color-mix(in oklab,var(--color-synapse) 10%,transparent)}.pr-row.active.svelte-c1wbiz{border-color:color-mix(in oklab,var(--color-synapse) 45%,transparent);background:color-mix(in oklab,var(--color-synapse) 15%,transparent)}.pr-row-top.svelte-c1wbiz{display:flex;justify-content:space-between;gap:8px;margin-bottom:5px}.pr-kind.svelte-c1wbiz{font-size:.66rem;font-weight:700;text-transform:uppercase;letter-spacing:.04em;padding:2px 7px;border-radius:5px;background:color-mix(in oklab,var(--color-synapse) 16%,transparent);color:var(--color-synapse-glow, #818cf8)}.kind-contradiction_detected.svelte-c1wbiz{background:color-mix(in oklab,#fb7185 16%,transparent);color:#fb7185}.kind-memory_superseded.svelte-c1wbiz{background:color-mix(in oklab,#38bdf8 16%,transparent);color:#38bdf8}.kind-dream_consolidation.svelte-c1wbiz{background:color-mix(in oklab,#c084fc 16%,transparent);color:#c084fc}.pr-status.svelte-c1wbiz{font-size:.64rem;text-transform:uppercase;letter-spacing:.05em;color:var(--color-text-dim, #8b8ba7);align-self:center}.st-pending.svelte-c1wbiz{color:#f59e0b}.st-promoted.svelte-c1wbiz{color:#10b981}.st-forgotten.svelte-c1wbiz,.st-quarantined.svelte-c1wbiz{color:#f43f5e}.pr-title.svelte-c1wbiz{font-size:.84rem;line-height:1.35;color:var(--color-text, #e2e2f0)}.pr-sig-count.svelte-c1wbiz{font-size:.7rem;color:#f59e0b;margin-top:5px}.pr-detail.svelte-c1wbiz{min-width:0}.center-msg.svelte-c1wbiz{padding:50px;text-align:center;color:var(--color-text-dim, #8b8ba7)}.diff-card.svelte-c1wbiz{padding:20px 22px}.diff-head.svelte-c1wbiz{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.run-link.svelte-c1wbiz{display:inline-flex;align-items:center;gap:4px;margin-left:auto;font-size:.74rem;color:var(--color-synapse-glow, #818cf8);text-decoration:none;padding:3px 8px;border-radius:6px;background:color-mix(in oklab,var(--color-synapse) 10%,transparent)}.diff-title.svelte-c1wbiz{font-size:1.15rem;font-weight:700;margin:12px 0 16px;line-height:1.4}.diff-body.svelte-c1wbiz{margin-bottom:16px}.diff-meta.svelte-c1wbiz{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px}.meta-pill.svelte-c1wbiz{font-size:.72rem;padding:2px 8px;border-radius:6px;background:color-mix(in oklab,white 6%,transparent);color:var(--color-text-dim, #c0c0d8)}.meta-pill.tag.svelte-c1wbiz{color:var(--color-synapse-glow, #818cf8)}.diff-add.svelte-c1wbiz{margin:0;padding:12px 14px 12px 36px;position:relative;border-radius:9px;background:color-mix(in oklab,#10b981 9%,transparent);border-left:3px solid #10b981;font-size:.85rem;line-height:1.55;white-space:pre-wrap;word-break:break-word;font-family:var(--font-mono, monospace);color:var(--color-text, #e2e2f0)}.diff-add.svelte-c1wbiz .gutter:where(.svelte-c1wbiz){position:absolute;left:12px;color:#10b981;font-weight:700}.signals.svelte-c1wbiz,.why-box.svelte-c1wbiz{margin-bottom:16px;padding:12px 14px;border-radius:10px;background:color-mix(in oklab,#f59e0b 8%,transparent);border:1px solid color-mix(in oklab,#f59e0b 22%,transparent)}.why-box.svelte-c1wbiz{background:color-mix(in oklab,var(--color-synapse) 8%,transparent);border-color:color-mix(in oklab,var(--color-synapse) 22%,transparent)}.signals-title.svelte-c1wbiz,.why-title.svelte-c1wbiz{display:block;font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:#f59e0b;margin-bottom:8px}.why-title.svelte-c1wbiz{color:var(--color-synapse-glow, #818cf8)}.signal.svelte-c1wbiz{display:flex;gap:8px;align-items:baseline;padding:3px 0}.sig-code.svelte-c1wbiz{font-size:.7rem;color:#f59e0b;white-space:nowrap}.sig-detail.svelte-c1wbiz{font-size:.82rem;line-height:1.4;color:var(--color-text, #e2e2f0)}.actions.svelte-c1wbiz{display:flex;flex-wrap:wrap;gap:8px}.action.svelte-c1wbiz{font-size:.8rem;font-weight:600;padding:8px 14px;border-radius:9px;border:1px solid color-mix(in oklab,var(--ac, #6366f1) 40%,transparent);background:color-mix(in oklab,var(--ac, #6366f1) 10%,transparent);color:var(--ac, #818cf8);cursor:pointer;transition:all .16s ease}.action.svelte-c1wbiz:hover:not(:disabled){background:color-mix(in oklab,var(--ac, #6366f1) 22%,transparent);transform:translateY(-1px)}.action.svelte-c1wbiz:disabled{opacity:.5;cursor:wait}.action.promote.svelte-c1wbiz{--ac: #10b981}.action.merge.svelte-c1wbiz{--ac: #6366f1}.action.supersede.svelte-c1wbiz{--ac: #38bdf8}.action.quarantine.svelte-c1wbiz{--ac: #f59e0b}.action.forget.svelte-c1wbiz{--ac: #f43f5e}.action.why.svelte-c1wbiz{--ac: #818cf8}.decided.svelte-c1wbiz{font-size:.88rem;padding:10px 14px;border-radius:9px;background:color-mix(in oklab,white 4%,transparent)}.text-dim.svelte-c1wbiz{color:var(--color-text-dim, #8b8ba7)} diff --git a/apps/dashboard/build/_app/immutable/assets/15.BzuIe_Oj.css.br b/apps/dashboard/build/_app/immutable/assets/15.BzuIe_Oj.css.br deleted file mode 100644 index b3e67f6..0000000 Binary files a/apps/dashboard/build/_app/immutable/assets/15.BzuIe_Oj.css.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/assets/15.BzuIe_Oj.css.gz b/apps/dashboard/build/_app/immutable/assets/15.BzuIe_Oj.css.gz deleted file mode 100644 index d79d1a9..0000000 Binary files a/apps/dashboard/build/_app/immutable/assets/15.BzuIe_Oj.css.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/assets/17.ChjqzJHo.css b/apps/dashboard/build/_app/immutable/assets/15.ChjqzJHo.css similarity index 100% rename from apps/dashboard/build/_app/immutable/assets/17.ChjqzJHo.css rename to apps/dashboard/build/_app/immutable/assets/15.ChjqzJHo.css diff --git a/apps/dashboard/build/_app/immutable/assets/17.ChjqzJHo.css.br b/apps/dashboard/build/_app/immutable/assets/15.ChjqzJHo.css.br similarity index 100% rename from apps/dashboard/build/_app/immutable/assets/17.ChjqzJHo.css.br rename to apps/dashboard/build/_app/immutable/assets/15.ChjqzJHo.css.br diff --git a/apps/dashboard/build/_app/immutable/assets/15.ChjqzJHo.css.gz b/apps/dashboard/build/_app/immutable/assets/15.ChjqzJHo.css.gz new file mode 100644 index 0000000..2d6842b Binary files /dev/null and b/apps/dashboard/build/_app/immutable/assets/15.ChjqzJHo.css.gz differ diff --git a/apps/dashboard/build/_app/immutable/assets/18.BnHgRQtR.css b/apps/dashboard/build/_app/immutable/assets/16.BnHgRQtR.css similarity index 100% rename from apps/dashboard/build/_app/immutable/assets/18.BnHgRQtR.css rename to apps/dashboard/build/_app/immutable/assets/16.BnHgRQtR.css diff --git a/apps/dashboard/build/_app/immutable/assets/18.BnHgRQtR.css.br b/apps/dashboard/build/_app/immutable/assets/16.BnHgRQtR.css.br similarity index 100% rename from apps/dashboard/build/_app/immutable/assets/18.BnHgRQtR.css.br rename to apps/dashboard/build/_app/immutable/assets/16.BnHgRQtR.css.br diff --git a/apps/dashboard/build/_app/immutable/assets/18.BnHgRQtR.css.gz b/apps/dashboard/build/_app/immutable/assets/16.BnHgRQtR.css.gz similarity index 55% rename from apps/dashboard/build/_app/immutable/assets/18.BnHgRQtR.css.gz rename to apps/dashboard/build/_app/immutable/assets/16.BnHgRQtR.css.gz index ad88522..f534971 100644 Binary files a/apps/dashboard/build/_app/immutable/assets/18.BnHgRQtR.css.gz and b/apps/dashboard/build/_app/immutable/assets/16.BnHgRQtR.css.gz differ diff --git a/apps/dashboard/build/_app/immutable/assets/17.ChjqzJHo.css.gz b/apps/dashboard/build/_app/immutable/assets/17.ChjqzJHo.css.gz deleted file mode 100644 index 388b5e8..0000000 Binary files a/apps/dashboard/build/_app/immutable/assets/17.ChjqzJHo.css.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/assets/19.C2qtIyf6.css b/apps/dashboard/build/_app/immutable/assets/19.C2qtIyf6.css deleted file mode 100644 index b9e5029..0000000 --- a/apps/dashboard/build/_app/immutable/assets/19.C2qtIyf6.css +++ /dev/null @@ -1 +0,0 @@ -.conn-pill.svelte-15kgmsr{display:inline-flex;align-items:center;gap:.5rem;padding:.3rem .7rem;border-radius:999px;font-size:.75rem;font-weight:600;color:var(--color-recall, #34d399);background:color-mix(in srgb,var(--color-recall, #34d399) 12%,transparent);border:1px solid color-mix(in srgb,var(--color-recall, #34d399) 28%,transparent);white-space:nowrap}.conn-pill.idle.svelte-15kgmsr{color:var(--color-dim, #8b95a5);background:#ffffff0a;border-color:#ffffff14}.conn-dot.svelte-15kgmsr{display:inline-block;flex-shrink:0}.refresh-btn.svelte-15kgmsr{display:inline-flex;align-items:center;gap:.3rem;padding:.3rem .65rem;border-radius:999px;font-size:.75rem;color:var(--color-muted, #6b7280);background:#ffffff05;border:1px solid rgba(255,255,255,.06);transition:color .2s ease,background .2s ease,border-color .2s ease}.refresh-btn.svelte-15kgmsr:hover{color:var(--color-text, #e5e7eb);background:#ffffff0d;border-color:#ffffff1f}.logo-tile.svelte-15kgmsr{position:relative}.logo-tile.svelte-15kgmsr:after{content:"";position:absolute;top:-1px;right:-1px;bottom:-1px;left:-1px;border-radius:inherit;box-shadow:0 0 18px -2px var(--color-synapse-glow, #818cf8);opacity:.4;pointer-events:none}@media not (prefers-reduced-motion:reduce){.logo-tile.svelte-15kgmsr:after{animation:svelte-15kgmsr-logo-glow 4s ease-in-out infinite}@keyframes svelte-15kgmsr-logo-glow{0%,to{opacity:.25}50%{opacity:.55}}} diff --git a/apps/dashboard/build/_app/immutable/assets/19.C2qtIyf6.css.br b/apps/dashboard/build/_app/immutable/assets/19.C2qtIyf6.css.br deleted file mode 100644 index 874173f..0000000 Binary files a/apps/dashboard/build/_app/immutable/assets/19.C2qtIyf6.css.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/assets/19.C2qtIyf6.css.gz b/apps/dashboard/build/_app/immutable/assets/19.C2qtIyf6.css.gz deleted file mode 100644 index e22d836..0000000 Binary files a/apps/dashboard/build/_app/immutable/assets/19.C2qtIyf6.css.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/assets/20.CO50G5tF.css b/apps/dashboard/build/_app/immutable/assets/20.CO50G5tF.css deleted file mode 100644 index bcec2e9..0000000 --- a/apps/dashboard/build/_app/immutable/assets/20.CO50G5tF.css +++ /dev/null @@ -1 +0,0 @@ -.metric-card.svelte-w41m0t{cursor:default} diff --git a/apps/dashboard/build/_app/immutable/assets/20.CO50G5tF.css.br b/apps/dashboard/build/_app/immutable/assets/20.CO50G5tF.css.br deleted file mode 100644 index 97fdfcf..0000000 --- a/apps/dashboard/build/_app/immutable/assets/20.CO50G5tF.css.br +++ /dev/null @@ -1,2 +0,0 @@ - .metric-card.svelte-w41m0t{cursor:default} - \ No newline at end of file diff --git a/apps/dashboard/build/_app/immutable/assets/20.CO50G5tF.css.gz b/apps/dashboard/build/_app/immutable/assets/20.CO50G5tF.css.gz deleted file mode 100644 index acfc4f2..0000000 Binary files a/apps/dashboard/build/_app/immutable/assets/20.CO50G5tF.css.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/assets/22.DKhUrxcR.css b/apps/dashboard/build/_app/immutable/assets/22.DKhUrxcR.css deleted file mode 100644 index 4017a6e..0000000 --- a/apps/dashboard/build/_app/immutable/assets/22.DKhUrxcR.css +++ /dev/null @@ -1 +0,0 @@ -body{overflow:hidden}.waitlist-shell.svelte-1375qm6{position:fixed;top:0;right:0;bottom:0;left:0;overflow-y:auto;background:#07100f;color:#edf7f2;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}.memory-field.svelte-1375qm6,.field-vignette.svelte-1375qm6{position:fixed;top:0;right:0;bottom:0;left:0;pointer-events:none}.memory-field.svelte-1375qm6{z-index:0}.field-vignette.svelte-1375qm6{z-index:1;background:linear-gradient(90deg,#07100feb,#07100f9e 48%,#07100fe0),linear-gradient(180deg,#07100f33,#07100fd1)}.topbar.svelte-1375qm6,main.svelte-1375qm6{position:relative;z-index:2}.topbar.svelte-1375qm6{display:flex;align-items:center;justify-content:space-between;gap:1rem;width:min(1180px,calc(100% - 2rem));margin:0 auto;padding:1rem 0}.brand.svelte-1375qm6,.topbar.svelte-1375qm6 nav:where(.svelte-1375qm6),.hero-actions.svelte-1375qm6,.proof-row.svelte-1375qm6,.signal-band.svelte-1375qm6,.track-grid.svelte-1375qm6,.support-bot.svelte-1375qm6,.roadmap.svelte-1375qm6,.roadmap.svelte-1375qm6 li:where(.svelte-1375qm6){display:flex}.brand.svelte-1375qm6{align-items:center;gap:.7rem;color:#fff;text-decoration:none;font-weight:800}.brand-mark.svelte-1375qm6{display:grid;place-items:center;width:2.15rem;height:2.15rem;border:1px solid rgba(34,197,94,.48);border-radius:8px;background:linear-gradient(135deg,#22c55e3d,#06b6d429);color:#bbf7d0}.topbar.svelte-1375qm6 nav:where(.svelte-1375qm6){align-items:center;gap:.4rem}.topbar.svelte-1375qm6 a:where(.svelte-1375qm6){color:#b8c7c0;text-decoration:none}.topbar.svelte-1375qm6 nav:where(.svelte-1375qm6) a:where(.svelte-1375qm6){border-radius:8px;padding:.65rem .85rem;font-size:.88rem}.topbar.svelte-1375qm6 nav:where(.svelte-1375qm6) a:where(.svelte-1375qm6):hover,.nav-cta.svelte-1375qm6{background:#ffffff12;color:#fff}main.svelte-1375qm6{width:min(1180px,calc(100% - 2rem));margin:0 auto}.hero.svelte-1375qm6{display:grid;grid-template-columns:minmax(0,1.05fr) minmax(320px,.7fr);gap:clamp(2rem,6vw,5rem);align-items:center;min-height:86vh;padding:clamp(2rem,5vw,4.8rem) 0 2rem}.hero-copy.svelte-1375qm6{max-width:720px}.eyebrow.svelte-1375qm6,.form-heading.svelte-1375qm6 p:where(.svelte-1375qm6),.section-heading.svelte-1375qm6 p:where(.svelte-1375qm6){margin:0 0 .8rem;color:#67e8f9;font-size:.78rem;font-weight:800;letter-spacing:0;text-transform:uppercase}h1.svelte-1375qm6,h2.svelte-1375qm6,h3.svelte-1375qm6,p.svelte-1375qm6{margin-top:0}h1.svelte-1375qm6{margin-bottom:1.1rem;color:#fff;font-size:clamp(3.8rem,13vw,8rem);line-height:.92;letter-spacing:0}.hero-subtitle.svelte-1375qm6{max-width:680px;margin-bottom:1.6rem;color:#d7e6df;font-size:clamp(1.05rem,2.4vw,1.45rem);line-height:1.5}.hero-actions.svelte-1375qm6{flex-wrap:wrap;gap:.8rem;margin-bottom:2rem}.primary-link.svelte-1375qm6,.secondary-link.svelte-1375qm6,.submit-button.svelte-1375qm6{border-radius:8px;font-weight:800;text-decoration:none}.primary-link.svelte-1375qm6,.submit-button.svelte-1375qm6{border:1px solid rgba(34,197,94,.86);background:#22c55e;color:#04130b;box-shadow:0 20px 42px #22c55e30}.primary-link.svelte-1375qm6,.secondary-link.svelte-1375qm6{padding:.88rem 1rem}.secondary-link.svelte-1375qm6{border:1px solid rgba(226,232,240,.2);background:#ffffff0f;color:#edf7f2}.proof-row.svelte-1375qm6{flex-wrap:wrap;gap:.7rem}.proof-item.svelte-1375qm6{width:min(100%,13rem);border:1px solid rgba(226,232,240,.12);border-radius:8px;background:#050c0b8a;padding:.9rem}.proof-item.svelte-1375qm6 strong:where(.svelte-1375qm6),.proof-item.svelte-1375qm6 span:where(.svelte-1375qm6){display:block}.proof-item.svelte-1375qm6 strong:where(.svelte-1375qm6){margin-bottom:.4rem;color:#bbf7d0;font-size:1.05rem}.proof-item.svelte-1375qm6 span:where(.svelte-1375qm6){color:#a8bbb2;font-size:.82rem;line-height:1.45}.waitlist-form.svelte-1375qm6{border:1px solid rgba(226,232,240,.16);border-radius:8px;background:#050c0bd1;box-shadow:0 28px 90px #0000005c;padding:clamp(1rem,3vw,1.35rem)}.form-heading.svelte-1375qm6 h2:where(.svelte-1375qm6),.section-heading.svelte-1375qm6 h2:where(.svelte-1375qm6),.roadmap.svelte-1375qm6 h2:where(.svelte-1375qm6){margin-bottom:1rem;color:#fff;font-size:clamp(1.6rem,4vw,2.7rem);line-height:1.05;letter-spacing:0}.form-heading.svelte-1375qm6 h2:where(.svelte-1375qm6){font-size:clamp(1.4rem,3vw,2rem)}label.svelte-1375qm6{display:block;margin-top:.85rem}label.svelte-1375qm6 span:where(.svelte-1375qm6){display:block;margin-bottom:.38rem;color:#a8bbb2;font-size:.78rem;font-weight:750}input.svelte-1375qm6,select.svelte-1375qm6,textarea.svelte-1375qm6{width:100%;border:1px solid rgba(226,232,240,.16);border-radius:8px;background:#fff1;color:#fff;font:inherit;padding:.78rem .82rem;outline:none}select.svelte-1375qm6{color-scheme:dark}textarea.svelte-1375qm6{resize:vertical;min-height:6rem}input.svelte-1375qm6:focus,select.svelte-1375qm6:focus,textarea.svelte-1375qm6:focus{border-color:#22c55ee6;box-shadow:0 0 0 3px #22c55e24}.hidden-field.svelte-1375qm6{position:absolute;left:-10000px;height:1px;overflow:hidden}.submit-button.svelte-1375qm6{width:100%;margin-top:1rem;padding:.95rem 1rem;cursor:pointer;font:inherit}.submit-button.svelte-1375qm6:disabled{cursor:wait;opacity:.72}.submit-message.svelte-1375qm6{margin:.8rem 0 0;font-size:.82rem;line-height:1.45}.submit-message.success.svelte-1375qm6{color:#86efac}.submit-message.error.svelte-1375qm6{color:#fca5a5}.signal-band.svelte-1375qm6{flex-wrap:wrap;gap:.65rem;border-top:1px solid rgba(226,232,240,.12);border-bottom:1px solid rgba(226,232,240,.12);padding:1rem 0}.signal-band.svelte-1375qm6 div:where(.svelte-1375qm6){border-radius:999px;background:#ffffff12;color:#d7e6df;padding:.55rem .75rem;font-size:.84rem}.pro-grid.svelte-1375qm6,.roadmap.svelte-1375qm6{padding:clamp(3rem,7vw,5rem) 0}.section-heading.svelte-1375qm6{max-width:760px;margin-bottom:1.7rem}.track-grid.svelte-1375qm6{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.9rem}.track.svelte-1375qm6{border:1px solid rgba(226,232,240,.13);border-radius:8px;background:#050c0ba8;padding:1.1rem}.track-line.svelte-1375qm6{width:3.5rem;height:.22rem;margin-bottom:1rem;border-radius:999px;background:var(--track-color)}.track.svelte-1375qm6 h3:where(.svelte-1375qm6){margin-bottom:.65rem;color:#fff;font-size:1.08rem}.track.svelte-1375qm6 p:where(.svelte-1375qm6),.roadmap.svelte-1375qm6 span:where(.svelte-1375qm6){color:#a8bbb2;line-height:1.55}.support-bot.svelte-1375qm6,.roadmap.svelte-1375qm6{align-items:flex-start;justify-content:space-between;gap:clamp(2rem,6vw,4rem);border-top:1px solid rgba(226,232,240,.12)}.support-bot.svelte-1375qm6{padding:clamp(2.6rem,6vw,4.5rem) 0}.support-bot.svelte-1375qm6>div:where(.svelte-1375qm6):first-child,.roadmap.svelte-1375qm6>div:where(.svelte-1375qm6){flex:0 0 min(28rem,100%)}.bot-intro.svelte-1375qm6{max-width:34rem;color:#a8bbb2;line-height:1.55}.bot-panel.svelte-1375qm6{flex:1;border:1px solid rgba(226,232,240,.13);border-radius:8px;background:#050c0b9e;padding:clamp(1rem,3vw,1.25rem)}.bot-status.svelte-1375qm6,.prompt-row.svelte-1375qm6,.bot-input.svelte-1375qm6{display:flex;align-items:center}.bot-status.svelte-1375qm6{gap:.55rem;margin-bottom:.9rem;color:#d7e6df;font-size:.86rem;font-weight:800}.bot-status.svelte-1375qm6 small:where(.svelte-1375qm6){margin-left:auto;border:1px solid rgba(34,197,94,.22);border-radius:999px;padding:.3rem .5rem;color:#86efac;font-size:.72rem}.bot-light.svelte-1375qm6{width:.42rem;height:.42rem;border-radius:999px;background:#22c55e;box-shadow:0 0 18px #22c55eb3}.bot-messages.svelte-1375qm6{display:grid;gap:.75rem;max-height:22rem;overflow-y:auto;border:1px solid rgba(226,232,240,.1);border-radius:8px;background:#ffffff09;padding:.8rem}.bot-bubble.svelte-1375qm6,.user-bubble.svelte-1375qm6{max-width:88%;border-radius:8px;padding:.75rem .82rem}.bot-bubble.svelte-1375qm6{justify-self:start;border:1px solid rgba(34,197,94,.18);background:#22c55e14;color:#d7e6df}.user-bubble.svelte-1375qm6{justify-self:end;border:1px solid rgba(6,182,212,.24);background:#06b6d41f;color:#fff}.bot-bubble.svelte-1375qm6 p:where(.svelte-1375qm6),.user-bubble.svelte-1375qm6 p:where(.svelte-1375qm6){margin:0;font-size:.86rem;line-height:1.5;white-space:pre-wrap}.bot-bubble.svelte-1375qm6 p:where(.svelte-1375qm6)+p:where(.svelte-1375qm6),.user-bubble.svelte-1375qm6 p:where(.svelte-1375qm6)+p:where(.svelte-1375qm6){margin-top:.35rem}.prompt-row.svelte-1375qm6{flex-wrap:wrap;gap:.45rem;margin:.85rem 0}.prompt-row.svelte-1375qm6 button:where(.svelte-1375qm6){border:1px solid rgba(226,232,240,.14);border-radius:999px;background:#ffffff0e;color:#d7e6df;cursor:pointer;font:inherit;font-size:.78rem;padding:.46rem .62rem}.prompt-row.svelte-1375qm6 button:where(.svelte-1375qm6):hover{border-color:#22c55e6b;color:#fff}.bot-input.svelte-1375qm6{gap:.55rem}.bot-input.svelte-1375qm6 input:where(.svelte-1375qm6){margin:0}.bot-input.svelte-1375qm6 button:where(.svelte-1375qm6){flex:0 0 auto;border:1px solid rgba(34,197,94,.86);border-radius:8px;background:#22c55e;color:#04130b;cursor:pointer;font:inherit;font-weight:800;padding:.78rem .95rem}.bot-input.svelte-1375qm6 button:where(.svelte-1375qm6):disabled{cursor:not-allowed;opacity:.45}.roadmap.svelte-1375qm6 ol:where(.svelte-1375qm6){display:grid;gap:.8rem;margin:0;padding:0;list-style:none}.roadmap.svelte-1375qm6 li:where(.svelte-1375qm6){gap:1rem;align-items:flex-start;border:1px solid rgba(226,232,240,.13);border-radius:8px;background:#050c0b94;padding:1rem}.roadmap.svelte-1375qm6 strong:where(.svelte-1375qm6){flex:0 0 4.5rem;color:#fbbf24}@media(max-width:900px){.topbar.svelte-1375qm6{align-items:flex-start;flex-direction:column}.hero.svelte-1375qm6,.track-grid.svelte-1375qm6,.support-bot.svelte-1375qm6,.roadmap.svelte-1375qm6{grid-template-columns:1fr}.hero.svelte-1375qm6{display:block;min-height:auto;padding-top:2rem}.waitlist-form.svelte-1375qm6{margin-top:2rem}.support-bot.svelte-1375qm6,.roadmap.svelte-1375qm6{display:block}.bot-panel.svelte-1375qm6{margin-top:1rem}}@media(max-width:560px){main.svelte-1375qm6,.topbar.svelte-1375qm6{width:min(100% - 1rem,1180px)}.topbar.svelte-1375qm6 nav:where(.svelte-1375qm6){width:100%;justify-content:space-between}.topbar.svelte-1375qm6 nav:where(.svelte-1375qm6) a:where(.svelte-1375qm6){padding:.58rem .62rem;font-size:.8rem}h1.svelte-1375qm6{font-size:clamp(3.35rem,18vw,4.8rem)}.hero-subtitle.svelte-1375qm6{font-size:1rem}.proof-item.svelte-1375qm6{width:100%}.roadmap.svelte-1375qm6 li:where(.svelte-1375qm6){display:block}.roadmap.svelte-1375qm6 strong:where(.svelte-1375qm6){display:block;margin-bottom:.5rem}.bot-input.svelte-1375qm6{align-items:stretch;flex-direction:column}.bot-input.svelte-1375qm6 button:where(.svelte-1375qm6){width:100%}} diff --git a/apps/dashboard/build/_app/immutable/assets/22.DKhUrxcR.css.br b/apps/dashboard/build/_app/immutable/assets/22.DKhUrxcR.css.br deleted file mode 100644 index e4292ca..0000000 Binary files a/apps/dashboard/build/_app/immutable/assets/22.DKhUrxcR.css.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/assets/22.DKhUrxcR.css.gz b/apps/dashboard/build/_app/immutable/assets/22.DKhUrxcR.css.gz deleted file mode 100644 index f9799b6..0000000 Binary files a/apps/dashboard/build/_app/immutable/assets/22.DKhUrxcR.css.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/assets/6.DQ_AfUnN.css b/apps/dashboard/build/_app/immutable/assets/5.DQ_AfUnN.css similarity index 100% rename from apps/dashboard/build/_app/immutable/assets/6.DQ_AfUnN.css rename to apps/dashboard/build/_app/immutable/assets/5.DQ_AfUnN.css diff --git a/apps/dashboard/build/_app/immutable/assets/6.DQ_AfUnN.css.br b/apps/dashboard/build/_app/immutable/assets/5.DQ_AfUnN.css.br similarity index 100% rename from apps/dashboard/build/_app/immutable/assets/6.DQ_AfUnN.css.br rename to apps/dashboard/build/_app/immutable/assets/5.DQ_AfUnN.css.br diff --git a/apps/dashboard/build/_app/immutable/assets/5.DQ_AfUnN.css.gz b/apps/dashboard/build/_app/immutable/assets/5.DQ_AfUnN.css.gz new file mode 100644 index 0000000..ac521f3 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/assets/5.DQ_AfUnN.css.gz differ diff --git a/apps/dashboard/build/_app/immutable/assets/5.Tl8-WHJj.css b/apps/dashboard/build/_app/immutable/assets/5.Tl8-WHJj.css deleted file mode 100644 index 236f6ad..0000000 --- a/apps/dashboard/build/_app/immutable/assets/5.Tl8-WHJj.css +++ /dev/null @@ -1 +0,0 @@ -.receipt.svelte-1wdzvwu{border:1px solid color-mix(in oklab,var(--risk) 30%,transparent);border-left:3px solid var(--risk);border-radius:12px;padding:14px 16px;background:color-mix(in oklab,var(--color-void, #050510) 50%,transparent);display:flex;flex-direction:column;gap:12px}.receipt.compact.svelte-1wdzvwu{gap:10px;padding:12px 14px}.r-head.svelte-1wdzvwu{display:flex;justify-content:space-between;align-items:baseline;gap:8px}.r-id.svelte-1wdzvwu{font-size:.78rem;color:var(--color-synapse-glow, #818cf8);word-break:break-all}.r-risk.svelte-1wdzvwu{font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;white-space:nowrap}.r-metrics.svelte-1wdzvwu{display:flex;gap:20px}.metric.svelte-1wdzvwu{display:flex;flex-direction:column;gap:1px}.m-val.svelte-1wdzvwu{font-size:1.25rem;font-weight:800;line-height:1;font-variant-numeric:tabular-nums}.m-label.svelte-1wdzvwu{font-size:.64rem;text-transform:uppercase;letter-spacing:.07em;color:var(--color-text-dim, #8b8ba7)}.r-section.svelte-1wdzvwu{display:flex;flex-direction:column;gap:6px}.r-section-title.svelte-1wdzvwu{font-size:.66rem;text-transform:uppercase;letter-spacing:.07em;color:var(--color-text-dim, #8b8ba7)}.path.svelte-1wdzvwu{font-size:.8rem;font-family:var(--font-mono, monospace);color:var(--color-text, #e2e2f0);padding:4px 8px;border-radius:6px;background:color-mix(in oklab,var(--color-synapse) 8%,transparent)}.chips.svelte-1wdzvwu{display:flex;flex-wrap:wrap;gap:5px}.chip.svelte-1wdzvwu{font-size:.72rem;padding:2px 8px;border-radius:6px}.chip.recall.svelte-1wdzvwu{color:var(--color-recall, #10b981);background:color-mix(in oklab,var(--color-recall) 12%,transparent);border:1px solid color-mix(in oklab,var(--color-recall) 28%,transparent)}.chip.suppress.svelte-1wdzvwu{color:#a78bfa;background:color-mix(in oklab,#a78bfa 12%,transparent);border:1px solid color-mix(in oklab,#a78bfa 28%,transparent);text-decoration:line-through;text-decoration-color:color-mix(in oklab,#a78bfa 50%,transparent)}.cinema-btn.svelte-1wdzvwu{margin-top:2px;display:inline-flex;align-items:center;justify-content:center;gap:7px;padding:8px 14px;font-size:.8rem;font-weight:600;border-radius:9px;border:1px solid color-mix(in oklab,var(--color-synapse) 40%,transparent);background:color-mix(in oklab,var(--color-synapse) 12%,transparent);color:var(--color-synapse-glow, #818cf8);cursor:pointer;transition:all .18s ease}.cinema-btn.svelte-1wdzvwu:hover:not(:disabled){background:color-mix(in oklab,var(--color-synapse) 24%,transparent);transform:translateY(-1px)}.cinema-btn.svelte-1wdzvwu:disabled{opacity:.4;cursor:not-allowed}.mode-toggle.svelte-1ayqwv0,.export-btn.svelte-1ayqwv0{display:inline-flex;align-items:center;gap:6px;padding:7px 12px;font-size:.78rem;font-weight:600;border-radius:9px;border:1px solid color-mix(in oklab,var(--color-synapse) 30%,transparent);background:color-mix(in oklab,var(--color-synapse) 8%,transparent);color:var(--color-synapse-glow);cursor:pointer;transition:all .18s ease}.mode-toggle.svelte-1ayqwv0:hover,.export-btn.svelte-1ayqwv0:hover:not(:disabled){background:color-mix(in oklab,var(--color-synapse) 18%,transparent);transform:translateY(-1px)}.mode-toggle.on.svelte-1ayqwv0{background:var(--color-synapse);color:#fff}.export-btn.svelte-1ayqwv0:disabled{opacity:.4;cursor:not-allowed}.spine.svelte-1ayqwv0{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:1px;border-radius:14px;padding:14px 18px;margin-bottom:18px;overflow:hidden}.spine-item.svelte-1ayqwv0{display:flex;flex-direction:column;gap:4px;padding:2px 14px}.spine-label.svelte-1ayqwv0{font-size:.66rem;text-transform:uppercase;letter-spacing:.08em;color:var(--color-text-dim, #8b8ba7)}.spine-value.svelte-1ayqwv0{font-size:.92rem;font-weight:600;display:inline-flex;align-items:center;gap:7px}.spine-run.svelte-1ayqwv0{font-size:.82rem;color:var(--color-synapse-glow);word-break:break-all}.dot.svelte-1ayqwv0{width:8px;height:8px;border-radius:50%;background:#64748b;box-shadow:0 0 0 0 transparent}.dot.live.svelte-1ayqwv0{background:var(--color-recall, #10b981);animation:svelte-1ayqwv0-ping 2s ease-in-out infinite}.dot.big.svelte-1ayqwv0{width:12px;height:12px}@keyframes svelte-1ayqwv0-ping{0%,to{box-shadow:0 0 color-mix(in oklab,var(--color-recall) 60%,transparent)}50%{box-shadow:0 0 0 6px transparent}}.ev-chip.svelte-1ayqwv0{font-size:.74rem;font-weight:700;padding:2px 8px;border-radius:6px;color:var(--c);background:color-mix(in oklab,var(--c) 14%,transparent);border:1px solid color-mix(in oklab,var(--c) 35%,transparent)}.layout.svelte-1ayqwv0{display:grid;grid-template-columns:260px 1fr;gap:16px;align-items:start}@media(max-width:860px){.layout.svelte-1ayqwv0{grid-template-columns:1fr}}.glass.svelte-1ayqwv0{background:color-mix(in oklab,var(--color-void, #050510) 55%,transparent);border:1px solid color-mix(in oklab,white 8%,transparent);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);border-radius:14px}.panel-title.svelte-1ayqwv0{font-size:.82rem;font-weight:700;letter-spacing:.02em;margin:0 0 12px}.text-dim.svelte-1ayqwv0{color:var(--color-text-dim, #8b8ba7);font-weight:400}.empty.svelte-1ayqwv0{font-size:.82rem;color:var(--color-text-dim, #8b8ba7);line-height:1.5}.runs.svelte-1ayqwv0{padding:16px;position:sticky;top:16px;max-height:calc(100vh - 40px);overflow-y:auto}.runs.svelte-1ayqwv0 ul:where(.svelte-1ayqwv0){list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px}.run-row.svelte-1ayqwv0{width:100%;text-align:left;padding:9px 11px;border-radius:10px;border:1px solid transparent;background:color-mix(in oklab,white 3%,transparent);cursor:pointer;transition:all .16s ease}.run-row.svelte-1ayqwv0:hover{background:color-mix(in oklab,var(--color-synapse) 12%,transparent)}.run-row.active.svelte-1ayqwv0{border-color:color-mix(in oklab,var(--color-synapse) 45%,transparent);background:color-mix(in oklab,var(--color-synapse) 16%,transparent)}.run-top.svelte-1ayqwv0{display:flex;justify-content:space-between;align-items:baseline;gap:8px}.run-id.svelte-1ayqwv0{font-size:.78rem;color:var(--color-synapse-glow)}.run-tool.svelte-1ayqwv0{font-size:.7rem;color:var(--color-text-dim, #8b8ba7);white-space:nowrap}.run-stats.svelte-1ayqwv0{display:flex;gap:8px;margin-top:5px;font-size:.68rem;color:var(--color-text-dim, #8b8ba7)}.s-recall.svelte-1ayqwv0{color:var(--color-recall, #10b981)}.s-suppress.svelte-1ayqwv0{color:#a78bfa}.s-write.svelte-1ayqwv0{color:#38bdf8}.s-veto.svelte-1ayqwv0{color:#f43f5e}.replay.svelte-1ayqwv0{display:flex;flex-direction:column;gap:14px;min-width:0}.center-msg.svelte-1ayqwv0{padding:40px;text-align:center;color:var(--color-text-dim, #8b8ba7)}.center-msg.err.svelte-1ayqwv0{color:#f87171}.scrubber.svelte-1ayqwv0{padding:16px 18px}.scrub-head.svelte-1ayqwv0{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:10px;font-size:.82rem}.scrub-time.svelte-1ayqwv0{color:var(--color-synapse-glow);font-variant-numeric:tabular-nums}.scrub-range.svelte-1ayqwv0{width:100%;accent-color:var(--color-synapse)}.ticks.svelte-1ayqwv0{display:flex;gap:2px;margin-top:10px;height:22px;align-items:stretch}.tick.svelte-1ayqwv0{flex:1;min-width:2px;border:none;border-radius:2px;background:color-mix(in oklab,var(--c) 22%,transparent);cursor:pointer;transition:all .16s ease;padding:0}.tick.past.svelte-1ayqwv0{background:var(--c);box-shadow:0 0 6px -1px var(--c)}.tick.svelte-1ayqwv0:hover{transform:scaleY(1.25)}.event-detail.svelte-1ayqwv0{padding:16px 18px;border-left:3px solid var(--c)}.ed-head.svelte-1ayqwv0{display:flex;align-items:center;gap:10px}.ed-glyph.svelte-1ayqwv0{font-size:1.2rem;color:var(--c)}.ed-label.svelte-1ayqwv0{font-weight:700;color:var(--c)}.ed-time.svelte-1ayqwv0{margin-left:auto;font-size:.74rem;color:var(--color-text-dim, #8b8ba7)}.ed-summary.svelte-1ayqwv0{margin:10px 0 0;font-size:.9rem;line-height:1.5}.ids-grid.svelte-1ayqwv0{display:flex;flex-wrap:wrap;gap:6px;margin-top:12px}.id-chip.svelte-1ayqwv0{display:inline-flex;align-items:center;gap:5px;padding:3px 8px;border-radius:7px;background:color-mix(in oklab,var(--color-recall) calc(var(--a, 0) * 30%),transparent);border:1px solid color-mix(in oklab,var(--color-recall) 30%,transparent);font-size:.74rem}.id-chip.svelte-1ayqwv0 small:where(.svelte-1ayqwv0){color:var(--color-recall, #10b981);font-weight:700}.contra.svelte-1ayqwv0{display:flex;align-items:center;gap:8px;margin-top:12px;font-size:.8rem}.winner.svelte-1ayqwv0{color:var(--color-recall, #10b981);font-weight:700}.vs.svelte-1ayqwv0{color:var(--color-text-dim, #8b8ba7)}.loser.svelte-1ayqwv0{color:#fb7185;text-decoration:line-through;opacity:.7}.veto-evidence.svelte-1ayqwv0{display:flex;gap:6px;margin-top:12px;flex-wrap:wrap}.veto-evidence.svelte-1ayqwv0 code:where(.svelte-1ayqwv0){padding:2px 7px;border-radius:6px;background:color-mix(in oklab,#f43f5e 12%,transparent);font-size:.74rem}.pulse.svelte-1ayqwv0{padding:16px 18px}.pulse-grid.svelte-1ayqwv0{display:flex;flex-wrap:wrap;gap:6px}.pulse-node.svelte-1ayqwv0{padding:3px 8px;border-radius:7px;background:color-mix(in oklab,var(--color-synapse) 12%,transparent);border:1px solid color-mix(in oklab,var(--color-synapse) 28%,transparent);font-size:.74rem;color:var(--color-synapse-glow);animation:svelte-1ayqwv0-pulse-in .4s ease}@keyframes svelte-1ayqwv0-pulse-in{0%{transform:scale(.85);opacity:0}to{transform:scale(1);opacity:1}}.receipts-panel.svelte-1ayqwv0{padding:16px 18px}.receipts-grid.svelte-1ayqwv0{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:12px}.producers.svelte-1ayqwv0{padding:16px 18px}.producer-list.svelte-1ayqwv0{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}.producer.svelte-1ayqwv0{display:flex;align-items:center;gap:9px;font-size:.78rem;color:var(--color-text-dim, #8b8ba7)}.producer.svelte-1ayqwv0 .p-dot:where(.svelte-1ayqwv0){width:8px;height:8px;border-radius:50%;background:#475569;flex-shrink:0}.producer.ok.svelte-1ayqwv0{color:var(--color-text, #e2e2f0)}.producer.ok.svelte-1ayqwv0 .p-dot:where(.svelte-1ayqwv0){background:var(--color-recall, #10b981);box-shadow:0 0 6px -1px var(--color-recall, #10b981)}.producer.caveat.svelte-1ayqwv0:not(.ok) .p-dot:where(.svelte-1ayqwv0){background:#f59e0b;opacity:.6}.p-state.svelte-1ayqwv0{margin-left:auto;font-size:.7rem;font-style:italic;text-align:right;color:var(--color-text-dim, #8b8ba7)}.producer.caveat.svelte-1ayqwv0:not(.ok) .p-state:where(.svelte-1ayqwv0){color:#f59e0b}.log.svelte-1ayqwv0{padding:16px 18px}.log-list.svelte-1ayqwv0{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:2px}.log-row.svelte-1ayqwv0{border-radius:8px;border-left:2px solid var(--c);transition:all .16s ease}.log-row.active.svelte-1ayqwv0{background:color-mix(in oklab,var(--c) 14%,transparent)}.log-row.dim.svelte-1ayqwv0{opacity:.4}.log-btn.svelte-1ayqwv0{width:100%;display:grid;grid-template-columns:22px 110px 1fr auto;align-items:center;gap:8px;padding:7px 10px;background:none;border:none;cursor:pointer;text-align:left;font-size:.8rem}.log-glyph.svelte-1ayqwv0{color:var(--c);text-align:center}.log-label.svelte-1ayqwv0{font-weight:600;color:var(--c)}.log-summary.svelte-1ayqwv0{color:var(--color-text-dim, #8b8ba7);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.log-t.svelte-1ayqwv0{font-size:.7rem;color:var(--color-text-dim, #8b8ba7);font-variant-numeric:tabular-nums}.proof-stage.svelte-1ayqwv0{padding:60px 40px;text-align:center;display:flex;flex-direction:column;align-items:center;gap:26px;min-height:50vh;justify-content:center}.proof-headline.svelte-1ayqwv0{display:flex;align-items:center;gap:12px}.proof-run.svelte-1ayqwv0{font-size:1.4rem;color:var(--color-synapse-glow);font-weight:700}.proof-event.svelte-1ayqwv0{display:flex;align-items:center;gap:16px;padding:18px 26px;border-radius:14px;border:1px solid color-mix(in oklab,var(--c) 40%,transparent);background:color-mix(in oklab,var(--c) 10%,transparent)}.proof-glyph.svelte-1ayqwv0{font-size:2rem;color:var(--c)}.proof-ev-label.svelte-1ayqwv0{font-size:1.1rem;font-weight:700;color:var(--c)}.proof-ev-sum.svelte-1ayqwv0{font-size:.85rem;color:var(--color-text-dim, #8b8ba7)}.proof-counter.svelte-1ayqwv0{font-size:3.4rem;font-weight:800;line-height:1;color:var(--color-synapse-glow)}.proof-counter-label.svelte-1ayqwv0{display:block;font-size:.8rem;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--color-text-dim, #8b8ba7);margin-top:8px}.proof-tagline.svelte-1ayqwv0{font-size:1rem;color:var(--color-text-dim, #c0c0d8);max-width:32ch;line-height:1.5} diff --git a/apps/dashboard/build/_app/immutable/assets/5.Tl8-WHJj.css.br b/apps/dashboard/build/_app/immutable/assets/5.Tl8-WHJj.css.br deleted file mode 100644 index 328c39d..0000000 Binary files a/apps/dashboard/build/_app/immutable/assets/5.Tl8-WHJj.css.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/assets/5.Tl8-WHJj.css.gz b/apps/dashboard/build/_app/immutable/assets/5.Tl8-WHJj.css.gz deleted file mode 100644 index 29b5849..0000000 Binary files a/apps/dashboard/build/_app/immutable/assets/5.Tl8-WHJj.css.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/assets/7.F0TwMZ5M.css b/apps/dashboard/build/_app/immutable/assets/6.BSSBWVKL.css similarity index 57% rename from apps/dashboard/build/_app/immutable/assets/7.F0TwMZ5M.css rename to apps/dashboard/build/_app/immutable/assets/6.BSSBWVKL.css index 84b1d37..65c46b1 100644 --- a/apps/dashboard/build/_app/immutable/assets/7.F0TwMZ5M.css +++ b/apps/dashboard/build/_app/immutable/assets/6.BSSBWVKL.css @@ -1 +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}}.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: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: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} +.replay-stage.svelte-1cq1ntk{border:1px solid rgba(168,85,247,.18);box-shadow:inset 0 1px #ffffff08,0 8px 36px -8px #0000008c,0 0 48px -16px #a855f740}.stage-canvas.svelte-1cq1ntk{position:relative;height:360px;overflow:hidden;background:radial-gradient(at 50% 50%,color-mix(in srgb,var(--stage-color) 10%,transparent),transparent 60%),radial-gradient(at 20% 80%,rgba(99,102,241,.08),transparent 50%),#08081a}.edges-layer.svelte-1cq1ntk{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;pointer-events:none}.edge-line.svelte-1cq1ntk{transition:stroke-opacity .52s ease,stroke-width .52s ease,x1 .6s cubic-bezier(.34,1.56,.64,1),y1 .6s cubic-bezier(.34,1.56,.64,1),x2 .6s cubic-bezier(.34,1.56,.64,1),y2 .6s cubic-bezier(.34,1.56,.64,1)}.memory-card.svelte-1cq1ntk{position:absolute;width:44px;height:32px;transform:translate(-50%,-50%) scale(var(--card-scale, 1));transition:left .6s cubic-bezier(.34,1.56,.64,1),top .6s cubic-bezier(.34,1.56,.64,1),transform .5s cubic-bezier(.34,1.56,.64,1),opacity .5s ease;transition-delay:var(--card-delay, 0ms);animation:svelte-1cq1ntk-card-float 6s ease-in-out infinite;animation-delay:var(--card-delay, 0ms);will-change:transform}.card-inner.svelte-1cq1ntk{width:100%;height:100%;border-radius:6px;background:linear-gradient(135deg,color-mix(in srgb,var(--stage-color) 30%,transparent),color-mix(in srgb,var(--stage-color) 10%,transparent));border:1px solid color-mix(in srgb,var(--stage-color) 50%,transparent);box-shadow:inset 0 1px #ffffff14,0 0 8px color-mix(in srgb,var(--stage-color) 30%,transparent);filter:hue-rotate(var(--card-hue, 0deg));padding:5px 6px;display:flex;flex-direction:column;justify-content:center;gap:3px;position:relative;overflow:hidden}.card-dot.svelte-1cq1ntk{width:4px;height:4px;border-radius:50%;background:color-mix(in srgb,var(--stage-color) 90%,white);box-shadow:0 0 6px var(--stage-color);position:absolute;top:4px;right:4px}.card-bar.svelte-1cq1ntk{height:3px;border-radius:1.5px;background:color-mix(in srgb,var(--stage-color) 70%,transparent);width:80%}.card-bar.short.svelte-1cq1ntk{width:50%;opacity:.6}.memory-card.is-pulsing.svelte-1cq1ntk{animation:svelte-1cq1ntk-card-float 6s ease-in-out infinite,svelte-1cq1ntk-card-pulse 1.4s ease-in-out infinite}.memory-card.is-pulsing.svelte-1cq1ntk .card-inner:where(.svelte-1cq1ntk){border-color:var(--color-dream-glow);background:linear-gradient(135deg,color-mix(in srgb,var(--color-dream-glow) 40%,transparent),color-mix(in srgb,var(--color-dream) 25%,transparent));box-shadow:inset 0 1px #ffffff1f,0 0 22px color-mix(in srgb,var(--color-dream-glow) 60%,transparent),0 0 44px color-mix(in srgb,var(--color-dream) 35%,transparent)}.memory-card.is-pruning.svelte-1cq1ntk .card-inner:where(.svelte-1cq1ntk){animation:svelte-1cq1ntk-dissolve 1.2s ease-out forwards}.memory-card.is-transferring.svelte-1cq1ntk .card-inner:where(.svelte-1cq1ntk){border-color:#10b981;background:linear-gradient(135deg,#10b98159,#10b9811f);box-shadow:inset 0 1px #ffffff14,0 0 14px #10b98180}.memory-card.is-transferring.semantic-side.svelte-1cq1ntk .card-inner:where(.svelte-1cq1ntk){border-color:#c084fc;background:linear-gradient(135deg,#c084fc59,#a855f726);box-shadow:inset 0 1px #ffffff14,0 0 14px #c084fc80}.replay-pulse.svelte-1cq1ntk{position:absolute;top:50%;left:50%;width:60%;aspect-ratio:1 / 1;transform:translate(-50%,-50%);border-radius:50%;background:radial-gradient(circle,color-mix(in srgb,var(--stage-color) 25%,transparent),transparent 60%);filter:blur(30px);animation:svelte-1cq1ntk-pulse-in 3s ease-in-out infinite;pointer-events:none}.transfer-label.svelte-1cq1ntk{position:absolute;top:12px;display:flex;flex-direction:column;align-items:center;gap:2px;z-index:2}.transfer-label.episodic.svelte-1cq1ntk{left:6%}.transfer-label.semantic.svelte-1cq1ntk{right:6%}.label-tag.svelte-1cq1ntk{font-size:10px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;padding:2px 8px;border-radius:999px;border:1px solid rgba(255,255,255,.15);background:#00000059;color:#e0e0ff}.transfer-label.episodic.svelte-1cq1ntk .label-tag:where(.svelte-1cq1ntk){border-color:#10b98180;color:#10b981}.transfer-label.semantic.svelte-1cq1ntk .label-tag:where(.svelte-1cq1ntk){border-color:#c084fc80;color:#c084fc}.label-sub.svelte-1cq1ntk{font-size:9px;color:var(--color-dim);letter-spacing:.1em}.divider-line.svelte-1cq1ntk{position:absolute;top:15%;bottom:15%;left:50%;width:1px;background:linear-gradient(180deg,transparent,rgba(168,85,247,.35),transparent);transform:translate(-.5px)}@keyframes svelte-1cq1ntk-card-float{0%,to{translate:0 0}25%{translate:2px -3px}50%{translate:-2px 2px}75%{translate:3px 1px}}@keyframes svelte-1cq1ntk-card-pulse{0%,to{filter:brightness(1) hue-rotate(var(--card-hue, 0deg))}50%{filter:brightness(1.3) hue-rotate(var(--card-hue, 0deg))}}@keyframes svelte-1cq1ntk-dissolve{0%{opacity:1;transform:scale(1);filter:blur(0)}60%{opacity:.3;filter:blur(2px)}to{opacity:0;transform:scale(.5);filter:blur(6px)}}@keyframes svelte-1cq1ntk-pulse-in{0%,to{opacity:.3;transform:translate(-50%,-50%) scale(1)}50%{opacity:.7;transform:translate(-50%,-50%) scale(1.15)}}@media(prefers-reduced-motion:reduce){.memory-card.svelte-1cq1ntk,.replay-pulse.svelte-1cq1ntk,.memory-card.is-pulsing.svelte-1cq1ntk{animation:none}}.insight-card.svelte-1y17hsl{position:relative;border:1px solid color-mix(in srgb,var(--insight-color) 20%,transparent);transition:transform .4s cubic-bezier(.34,1.56,.64,1),border-color .22s ease,box-shadow .22s ease;animation:svelte-1y17hsl-card-in .42s cubic-bezier(.34,1.56,.64,1) both;animation-delay:var(--enter-delay, 0ms)}.insight-card.svelte-1y17hsl:hover{transform:translateY(-2px) scale(1.01);border-color:color-mix(in srgb,var(--insight-color) 45%,transparent)}.insight-card.high-novelty.svelte-1y17hsl{border-color:#f59e0b66;box-shadow:0 0 0 1px #f59e0b40,0 0 24px -4px #f59e0b73,0 0 60px -12px #f59e0b40,inset 0 1px #ffffff0d;background:radial-gradient(at top right,rgba(245,158,11,.08),transparent 50%),#0a0a1acc}.insight-card.low-novelty.svelte-1y17hsl{opacity:.6;filter:saturate(.7)}.insight-card.low-novelty.svelte-1y17hsl:hover{opacity:.9;filter:saturate(1)}.novelty-track.svelte-1y17hsl{height:4px;background:#ffffff0d;border-radius:2px;overflow:hidden}.novelty-fill.svelte-1y17hsl{height:100%;border-radius:2px;transition:width .6s cubic-bezier(.34,1.56,.64,1);box-shadow:0 0 8px color-mix(in srgb,var(--insight-color) 60%,transparent)}.source-chip.svelte-1y17hsl{background:#6366f11f;border:1px solid rgba(99,102,241,.25);color:var(--color-synapse-glow);text-decoration:none;transition:all .18s ease}.source-chip.svelte-1y17hsl:hover{background:#6366f140;border-color:#818cf880;transform:translateY(-1px)}.sparkle.svelte-1y17hsl{display:inline-block;animation:svelte-1y17hsl-sparkle-spin 3s linear infinite}@keyframes svelte-1y17hsl-sparkle-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes svelte-1y17hsl-card-in{0%{opacity:0;transform:translateY(8px) scale(.97)}to{opacity:1;transform:translateY(0) scale(1)}}@media(prefers-reduced-motion:reduce){.insight-card.svelte-1y17hsl,.sparkle.svelte-1y17hsl{animation:none}}.header-glyph.svelte-1fv2vo0{display:inline-block;color:var(--color-dream-glow);text-shadow:0 0 12px var(--color-dream),0 0 24px color-mix(in srgb,var(--color-dream) 50%,transparent);animation:svelte-1fv2vo0-twinkle 4s ease-in-out infinite}@keyframes svelte-1fv2vo0-twinkle{0%,to{opacity:1;transform:rotate(0)}50%{opacity:.75;transform:rotate(10deg)}}.dream-button.svelte-1fv2vo0{display:inline-flex;align-items:center;gap:.6rem;padding:.7rem 1.4rem;border-radius:999px;font-size:.9rem;font-weight:600;letter-spacing:.02em;color:#fff;background:linear-gradient(135deg,var(--color-dream),var(--color-synapse));border:1px solid color-mix(in srgb,var(--color-dream-glow) 60%,transparent);box-shadow:inset 0 1px #ffffff2e,0 8px 24px -6px #a855f78c,0 0 48px -10px #a855f773;cursor:pointer;transition:transform .4s cubic-bezier(.34,1.56,.64,1),box-shadow .22s ease,filter .22s ease}.dream-button.svelte-1fv2vo0:hover:not(:disabled){transform:translateY(-2px) scale(1.03);box-shadow:inset 0 1px #ffffff38,0 12px 32px -6px #a855f7b3,0 0 64px -10px #a855f78c}.dream-button.svelte-1fv2vo0:disabled{cursor:not-allowed;filter:saturate(.85)}.dream-button.is-dreaming.svelte-1fv2vo0{background:linear-gradient(135deg,var(--color-synapse),var(--color-dream));animation:svelte-1fv2vo0-button-breathe 2s ease-in-out infinite}@keyframes svelte-1fv2vo0-button-breathe{0%,to{box-shadow:0 8px 24px -6px #a855f780,0 0 48px -10px #a855f766}50%{box-shadow:0 12px 36px -6px #a855f7cc,0 0 80px -10px #a855f799}}.dream-icon.svelte-1fv2vo0{display:inline-block;animation:svelte-1fv2vo0-twinkle 3s ease-in-out infinite}.spinner.svelte-1fv2vo0{width:14px;height:14px;border-radius:50%;border:2px solid rgba(255,255,255,.25);border-top-color:#fff;animation:svelte-1fv2vo0-spin .8s linear infinite}@keyframes svelte-1fv2vo0-spin{to{transform:rotate(360deg)}}.empty-state.svelte-1fv2vo0{border:1px dashed rgba(168,85,247,.25)}.empty-glyph.svelte-1fv2vo0{font-size:3rem;color:var(--color-dream-glow);opacity:.5;text-shadow:0 0 20px var(--color-dream);animation:svelte-1fv2vo0-twinkle 4s ease-in-out infinite}.scrubber-wrap.svelte-1fv2vo0{position:relative;padding:4px 0 8px}.scrubber.svelte-1fv2vo0{-moz-appearance:none;appearance:none;-webkit-appearance:none;width:100%;height:6px;border-radius:999px;background:linear-gradient(90deg,var(--color-synapse-glow) 0%,var(--color-dream) 50%,var(--color-recall) 100%);opacity:.35;outline:none;cursor:pointer;transition:opacity .22s ease}.scrubber.svelte-1fv2vo0:hover:not(:disabled){opacity:.55}.scrubber.svelte-1fv2vo0::-webkit-slider-thumb{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:20px;height:20px;border-radius:50%;background:var(--color-dream-glow);border:2px solid white;box-shadow:0 0 0 3px #c084fc40,0 0 20px var(--color-dream),0 4px 12px #0006;cursor:grab;transition:transform .4s cubic-bezier(.34,1.56,.64,1)}.scrubber.svelte-1fv2vo0::-webkit-slider-thumb:hover{transform:scale(1.2)}.scrubber.svelte-1fv2vo0::-moz-range-thumb{width:20px;height:20px;border-radius:50%;background:var(--color-dream-glow);border:2px solid white;box-shadow:0 0 0 3px #c084fc40,0 0 20px var(--color-dream);cursor:grab}.scrubber.svelte-1fv2vo0:disabled{cursor:not-allowed;opacity:.25}.scrubber-ticks.svelte-1fv2vo0{display:flex;justify-content:space-between;margin-top:10px;gap:4px}.tick.svelte-1fv2vo0{display:flex;flex-direction:column;align-items:center;gap:4px;background:transparent;border:none;cursor:pointer;padding:2px 4px;color:var(--color-dim);font-size:10px;letter-spacing:.04em;transition:color .22s ease,transform .22s cubic-bezier(.34,1.56,.64,1)}.tick.svelte-1fv2vo0:disabled{cursor:not-allowed}.tick.svelte-1fv2vo0:hover:not(:disabled){color:var(--color-dream-glow);transform:translateY(-1px)}.tick-dot.svelte-1fv2vo0{width:8px;height:8px;border-radius:50%;background:#ffffff1a;border:1px solid rgba(255,255,255,.15);transition:all .28s ease}.tick.passed.svelte-1fv2vo0 .tick-dot:where(.svelte-1fv2vo0){background:var(--color-synapse-glow);border-color:var(--color-synapse-glow);opacity:.7}.tick.active.svelte-1fv2vo0 .tick-dot:where(.svelte-1fv2vo0){background:var(--color-dream-glow);border-color:#fff;box-shadow:0 0 0 3px #c084fc4d,0 0 14px var(--color-dream);transform:scale(1.3)}.tick.active.svelte-1fv2vo0{color:var(--color-dream-glow);font-weight:600}.tick-label.svelte-1fv2vo0{white-space:nowrap}.step-btn.svelte-1fv2vo0{width:28px;height:28px;border-radius:6px;background:#6366f11a;border:1px solid rgba(99,102,241,.2);color:var(--color-synapse-glow);cursor:pointer;transition:all .18s ease;font-size:11px}.step-btn.svelte-1fv2vo0:hover:not(:disabled){background:#6366f133;transform:translateY(-1px)}.step-btn.svelte-1fv2vo0:disabled{opacity:.35;cursor:not-allowed}.insights-scroll.svelte-1fv2vo0{max-height:520px;overflow-y:auto;padding-right:4px}.stat-cell.svelte-1fv2vo0{padding:.5rem .75rem;border-left:2px solid rgba(168,85,247,.3)}.stat-value.svelte-1fv2vo0{font-family:var(--font-mono);font-size:1.25rem;font-weight:700;color:var(--color-bright);font-variant-numeric:tabular-nums;line-height:1.1}.stat-label.svelte-1fv2vo0{font-size:10px;color:var(--color-dim);text-transform:uppercase;letter-spacing:.1em;margin-top:2px} diff --git a/apps/dashboard/build/_app/immutable/assets/6.BSSBWVKL.css.br b/apps/dashboard/build/_app/immutable/assets/6.BSSBWVKL.css.br new file mode 100644 index 0000000..1e5f381 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/assets/6.BSSBWVKL.css.br differ diff --git a/apps/dashboard/build/_app/immutable/assets/6.BSSBWVKL.css.gz b/apps/dashboard/build/_app/immutable/assets/6.BSSBWVKL.css.gz new file mode 100644 index 0000000..9ca84bb Binary files /dev/null and b/apps/dashboard/build/_app/immutable/assets/6.BSSBWVKL.css.gz differ diff --git a/apps/dashboard/build/_app/immutable/assets/6.DQ_AfUnN.css.gz b/apps/dashboard/build/_app/immutable/assets/6.DQ_AfUnN.css.gz deleted file mode 100644 index f973c72..0000000 Binary files a/apps/dashboard/build/_app/immutable/assets/6.DQ_AfUnN.css.gz and /dev/null 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/7.F0TwMZ5M.css.br b/apps/dashboard/build/_app/immutable/assets/7.F0TwMZ5M.css.br deleted file mode 100644 index 0bb3266..0000000 Binary files a/apps/dashboard/build/_app/immutable/assets/7.F0TwMZ5M.css.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/assets/7.F0TwMZ5M.css.gz b/apps/dashboard/build/_app/immutable/assets/7.F0TwMZ5M.css.gz deleted file mode 100644 index 3f3c922..0000000 Binary files a/apps/dashboard/build/_app/immutable/assets/7.F0TwMZ5M.css.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/assets/9.BBx09UGv.css b/apps/dashboard/build/_app/immutable/assets/9.BBx09UGv.css new file mode 100644 index 0000000..966b434 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/assets/9.BBx09UGv.css @@ -0,0 +1 @@ +@keyframes svelte-1n9p0vm-fade-in{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.animate-fade-in.svelte-1n9p0vm{animation:svelte-1n9p0vm-fade-in .3s ease-out} diff --git a/apps/dashboard/build/_app/immutable/assets/9.BBx09UGv.css.br b/apps/dashboard/build/_app/immutable/assets/9.BBx09UGv.css.br new file mode 100644 index 0000000..51ed7a8 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/assets/9.BBx09UGv.css.br differ diff --git a/apps/dashboard/build/_app/immutable/assets/9.BBx09UGv.css.gz b/apps/dashboard/build/_app/immutable/assets/9.BBx09UGv.css.gz new file mode 100644 index 0000000..647c7ec Binary files /dev/null and b/apps/dashboard/build/_app/immutable/assets/9.BBx09UGv.css.gz differ diff --git a/apps/dashboard/build/_app/immutable/assets/Dropdown.C2Z-7Phd.css b/apps/dashboard/build/_app/immutable/assets/Dropdown.C2Z-7Phd.css deleted file mode 100644 index 3466628..0000000 --- a/apps/dashboard/build/_app/immutable/assets/Dropdown.C2Z-7Phd.css +++ /dev/null @@ -1 +0,0 @@ -.dd.svelte-1fd3ybn{position:relative;display:inline-flex;flex-direction:column;gap:.3rem}.dd-label.svelte-1fd3ybn{font-size:.65rem;text-transform:uppercase;letter-spacing:.08em;color:var(--color-muted);padding-left:.15rem}.dd-trigger.svelte-1fd3ybn{display:inline-flex;align-items:center;gap:.5rem;padding:.55rem .7rem .55rem .8rem;min-width:9rem;background:#ffffff08;border:1px solid rgba(99,102,241,.12);border-radius:.75rem;color:var(--color-text);font-size:.8rem;font-family:inherit;cursor:pointer;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);transition:border-color .2s ease,background .2s ease,box-shadow .2s ease}.dd-trigger.svelte-1fd3ybn:hover{border-color:#6366f14d;background:#ffffff0d}.dd-trigger.svelte-1fd3ybn:focus-visible,.dd-trigger.dd-open.svelte-1fd3ybn{outline:none;border-color:#6366f180;box-shadow:0 0 0 3px #6366f11f}.dd-trigger-icon.svelte-1fd3ybn{color:var(--color-synapse-glow);display:inline-flex}.dd-value.svelte-1fd3ybn{flex:1;text-align:left;display:inline-flex;align-items:center;gap:.45rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dd-placeholder.svelte-1fd3ybn{color:var(--color-muted)}.dd-dot.svelte-1fd3ybn{width:.5rem;height:.5rem;border-radius:50%;flex-shrink:0;box-shadow:0 0 6px currentColor}.dd-chevron.svelte-1fd3ybn{color:var(--color-dim);display:inline-flex;transition:transform .25s cubic-bezier(.34,1.56,.64,1)}.dd-chevron-open.svelte-1fd3ybn{transform:rotate(180deg);color:var(--color-synapse-glow)}.dd-menu.svelte-1fd3ybn{position:absolute;top:calc(100% + .4rem);left:0;z-index:60;min-width:100%;max-height:18rem;overflow-y:auto;padding:.35rem;border-radius:.85rem;transform-origin:top center}@media not (prefers-reduced-motion:reduce){.dd-menu.svelte-1fd3ybn{animation:svelte-1fd3ybn-dd-pop .18s cubic-bezier(.34,1.56,.64,1)}@keyframes svelte-1fd3ybn-dd-pop{0%{opacity:0;transform:translateY(-6px) scale(.96)}to{opacity:1;transform:translateY(0) scale(1)}}}.dd-option.svelte-1fd3ybn{width:100%;display:flex;align-items:center;gap:.5rem;padding:.5rem .6rem;border:none;background:transparent;color:var(--color-dim);font-size:.8rem;font-family:inherit;text-align:left;border-radius:.6rem;cursor:pointer;transition:background .12s ease,color .12s ease}.dd-active.svelte-1fd3ybn{background:#6366f124;color:var(--color-text)}.dd-selected.svelte-1fd3ybn{color:var(--color-synapse-glow)}.dd-opt-icon.svelte-1fd3ybn{color:var(--color-dim);display:inline-flex}.dd-active.svelte-1fd3ybn .dd-opt-icon:where(.svelte-1fd3ybn){color:var(--color-synapse-glow)}.dd-opt-label.svelte-1fd3ybn{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dd-badge.svelte-1fd3ybn{font-size:.65rem;font-variant-numeric:tabular-nums;color:var(--color-muted);background:#ffffff0d;padding:.05rem .4rem;border-radius:.4rem}.dd-check.svelte-1fd3ybn{color:var(--color-synapse-glow);display:inline-flex} diff --git a/apps/dashboard/build/_app/immutable/assets/Dropdown.C2Z-7Phd.css.br b/apps/dashboard/build/_app/immutable/assets/Dropdown.C2Z-7Phd.css.br deleted file mode 100644 index 34f3906..0000000 Binary files a/apps/dashboard/build/_app/immutable/assets/Dropdown.C2Z-7Phd.css.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/assets/Dropdown.C2Z-7Phd.css.gz b/apps/dashboard/build/_app/immutable/assets/Dropdown.C2Z-7Phd.css.gz deleted file mode 100644 index e21b4b8..0000000 Binary files a/apps/dashboard/build/_app/immutable/assets/Dropdown.C2Z-7Phd.css.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/assets/Icon.tTjeJXhC.css b/apps/dashboard/build/_app/immutable/assets/Icon.tTjeJXhC.css deleted file mode 100644 index ae9f1db..0000000 --- a/apps/dashboard/build/_app/immutable/assets/Icon.tTjeJXhC.css +++ /dev/null @@ -1 +0,0 @@ -.vestige-icon.svelte-1eqehiz{display:inline-block;flex-shrink:0;vertical-align:middle;overflow:visible}@media not (prefers-reduced-motion:reduce){.vestige-icon-draw.svelte-1eqehiz path,.vestige-icon-draw.svelte-1eqehiz circle,.vestige-icon-draw.svelte-1eqehiz rect{stroke-dasharray:64;stroke-dashoffset:64;animation:svelte-1eqehiz-icon-draw .7s cubic-bezier(.65,0,.35,1) forwards}@keyframes svelte-1eqehiz-icon-draw{to{stroke-dashoffset:0}}} diff --git a/apps/dashboard/build/_app/immutable/assets/Icon.tTjeJXhC.css.br b/apps/dashboard/build/_app/immutable/assets/Icon.tTjeJXhC.css.br deleted file mode 100644 index 468287b..0000000 Binary files a/apps/dashboard/build/_app/immutable/assets/Icon.tTjeJXhC.css.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/assets/Icon.tTjeJXhC.css.gz b/apps/dashboard/build/_app/immutable/assets/Icon.tTjeJXhC.css.gz deleted file mode 100644 index dbe4663..0000000 Binary files a/apps/dashboard/build/_app/immutable/assets/Icon.tTjeJXhC.css.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/assets/PageHeader.Dmxpik8H.css b/apps/dashboard/build/_app/immutable/assets/PageHeader.Dmxpik8H.css deleted file mode 100644 index 180e2a9..0000000 --- a/apps/dashboard/build/_app/immutable/assets/PageHeader.Dmxpik8H.css +++ /dev/null @@ -1 +0,0 @@ -.header-tile.svelte-162svzm:after{content:"";position:absolute;top:-1px;right:-1px;bottom:-1px;left:-1px;border-radius:inherit;box-shadow:0 0 18px -2px currentColor;opacity:.35;pointer-events:none}@media not (prefers-reduced-motion:reduce){.header-tile.svelte-162svzm:after{animation:svelte-162svzm-tile-glow 4s ease-in-out infinite}@keyframes svelte-162svzm-tile-glow{0%,to{opacity:.22}50%{opacity:.5}}}.text-balance.svelte-162svzm{text-wrap:balance}.text-pretty.svelte-162svzm{text-wrap:pretty} diff --git a/apps/dashboard/build/_app/immutable/assets/PageHeader.Dmxpik8H.css.br b/apps/dashboard/build/_app/immutable/assets/PageHeader.Dmxpik8H.css.br deleted file mode 100644 index 417d6c0..0000000 Binary files a/apps/dashboard/build/_app/immutable/assets/PageHeader.Dmxpik8H.css.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/assets/PageHeader.Dmxpik8H.css.gz b/apps/dashboard/build/_app/immutable/assets/PageHeader.Dmxpik8H.css.gz deleted file mode 100644 index fb7c3df..0000000 Binary files a/apps/dashboard/build/_app/immutable/assets/PageHeader.Dmxpik8H.css.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/60_R_Vbt.js b/apps/dashboard/build/_app/immutable/chunks/60_R_Vbt.js deleted file mode 100644 index df354f9..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/60_R_Vbt.js +++ /dev/null @@ -1,2 +0,0 @@ -import{D as z,q as dr,aD as sr,G as A,L as U,N as gr,T as pr,g as m,U as hr,W as _r,X as x,K,M as G,I as Er,aE as Ar,aF as y,w as Sr,aG as b,$ as q,aH as Tr,a1 as Nr,ao as br,z as Ir,aI as V,aJ as Mr,aK as Or,ac as Cr,aL as rr,aM as Rr,Y as ur,_ as or,aN as P,R as lr,aO as Lr,aP as wr,aQ as Hr,Z as kr,J as Dr,aR as Fr,aS as Gr,aT as zr,aU as Ur,aV as tr,aW as Kr}from"./wpu9U-D0.js";function jr(r,e){return e}function Pr(r,e,f){for(var a=[],n=e.length,s,u=e.length,v=0;v{if(s){if(s.pending.delete(g),s.done.add(g),s.pending.size===0){var t=r.outrogroups;B(V(s.done)),t.delete(s),t.size===0&&(r.outrogroups=null)}}else u-=1},!1)}if(u===0){var o=a.length===0&&f!==null;if(o){var d=f,l=d.parentNode;Hr(l),l.append(d),r.items.clear()}B(e,!o)}else s={pending:new Set(e),done:new Set},(r.outrogroups??(r.outrogroups=new Set)).add(s)}function B(r,e=!0){for(var f=0;f{var c=f();return Ir(c)?c:c==null?[]:V(c)}),t,p=!0;function S(){i.fallback=l,Yr(i,t,u,e,a),l!==null&&(t.length===0?(l.f&b)===0?ur(l):(l.f^=b,k(l,null,u)):or(l,()=>{l=null}))}var T=dr(()=>{t=m(g);var c=t.length;let C=!1;if(A){var R=hr(u)===_r;R!==(c===0)&&(u=x(),U(u),K(!1),C=!0)}for(var _=new Set,M=Sr,L=Nr(),h=0;hs(u)):(l=q(()=>s(er??(er=z()))),l.f|=b)),c>_.size&&Tr(),A&&c>0&&U(x()),!p)if(L){for(const[D,F]of v)_.has(D)||M.skip_effect(F.e);M.oncommit(S),M.ondiscard(()=>{})}else S();C&&K(!0),m(g)}),i={effect:T,items:v,outrogroups:null,fallback:l};p=!1,A&&(u=G)}function H(r){for(;r!==null&&(r.f&Lr)===0;)r=r.next;return r}function Yr(r,e,f,a,n){var E,D,F,X,J,W,$,Q,Z;var s=(a&wr)!==0,u=e.length,v=r.items,o=H(r.effect.first),d,l=null,g,t=[],p=[],S,T,i,c;if(s)for(c=0;c0){var w=(a&sr)!==0&&u===0?f:null;if(s){for(c=0;c{var N,j;if(g!==void 0)for(i of g)(j=(N=i.nodes)==null?void 0:N.a)==null||j.apply()})}function qr(r,e,f,a,n,s,u,v){var o=(u&Mr)!==0?(u&Or)===0?Cr(f,!1,!1):rr(f):null,d=(u&Rr)!==0?rr(n):null;return{v:o,i:d,e:q(()=>(s(e,o??f,d??n,v),()=>{r.delete(a)}))}}function k(r,e,f){if(r.nodes)for(var a=r.nodes.start,n=r.nodes.end,s=e&&(e.f&b)===0?e.nodes.start:f;a!==null;){var u=Dr(a);if(s.before(a),a===n)return;a=u}}function I(r,e,f){e===null?r.effect.first=f:e.next=f,f===null?r.effect.last=e:f.prev=e}function cr(r){var e,f,a="";if(typeof r=="string"||typeof r=="number")a+=r;else if(typeof r=="object")if(Array.isArray(r)){var n=r.length;for(e=0;e=0;){var v=u+s;(u===0||fr.includes(a[u-1]))&&(v===a.length||fr.includes(a[v]))?a=(u===0?"":a.substring(0,u))+a.substring(v+1):u=v}}return a===""?null:a}function ar(r,e=!1){var f=e?" !important;":";",a="";for(var n of Object.keys(r)){var s=r[n];s!=null&&s!==""&&(a+=" "+n+": "+s+f)}return a}function Y(r){return r[0]!=="-"||r[1]!=="-"?r.toLowerCase():r}function yr(r,e){if(e){var f="",a,n;if(Array.isArray(e)?(a=e[0],n=e[1]):a=e,r){r=String(r).replaceAll(/\s*\/\*.*?\*\/\s*/g,"").trim();var s=!1,u=0,v=!1,o=[];a&&o.push(...Object.keys(a).map(Y)),n&&o.push(...Object.keys(n).map(Y));var d=0,l=-1;const T=r.length;for(var g=0;g{if(!e){if(e=!0,r.hasAttribute("value")){var a=r.value;ir(r,"value",null),r.value=a}if(r.hasAttribute("checked")){var n=r.checked;ir(r,"checked",null),r.checked=n}}};r.__on_r=f,lr(f),Kr()}}function fe(r,e){var f=vr(r);f.value===(f.value=e??void 0)||r.value===e&&(e!==0||r.nodeName!==$r)||(r.value=e??"")}function ir(r,e,f,a){var n=vr(r);A&&(n[e]=r.getAttribute(e),e==="src"||e==="srcset"||e==="href"&&r.nodeName===Wr)||n[e]!==(n[e]=f)&&(e==="loading"&&(r[Fr]=f),f==null?r.removeAttribute(e):typeof f!="string"&&Qr(r).includes(e)?r[e]=f:r.setAttribute(e,f))}function vr(r){return r.__attributes??(r.__attributes={[Xr]:r.nodeName.includes("-"),[Jr]:r.namespaceURI===Gr})}var nr=new Map;function Qr(r){var e=r.getAttribute("is")||r.nodeName,f=nr.get(e);if(f)return f;nr.set(e,f=[]);for(var a,n=r,s=Element.prototype;s!==n;){a=Ur(n);for(var u in a)a[u].set&&f.push(u);n=zr(n)}return f}export{ir as a,fe as b,xr as c,mr as e,jr as i,ee as r,re as s,yr as t}; diff --git a/apps/dashboard/build/_app/immutable/chunks/60_R_Vbt.js.br b/apps/dashboard/build/_app/immutable/chunks/60_R_Vbt.js.br deleted file mode 100644 index 8810358..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/60_R_Vbt.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/60_R_Vbt.js.gz b/apps/dashboard/build/_app/immutable/chunks/60_R_Vbt.js.gz deleted file mode 100644 index 2795bf5..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/60_R_Vbt.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BHDZZvku.js b/apps/dashboard/build/_app/immutable/chunks/BHDZZvku.js deleted file mode 100644 index 0cc8663..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/BHDZZvku.js +++ /dev/null @@ -1,2 +0,0 @@ -import"./Bzak7iHL.js";import{t as h,a as o,j as f,e as a,r as s,h as c}from"./wpu9U-D0.js";import{s as p}from"./D8mhvFt8.js";import{s as k}from"./LDOJP_6N.js";import{i as _}from"./DKve45Wd.js";import{s as I}from"./60_R_Vbt.js";import{p as P}from"./ByYB047u.js";import{I as H}from"./D7A-gG4Z.js";var q=f('

'),A=f('
'),B=f('

');function M(u,e){let v=P(e,"accent",3,"synapse");var l=B(),m=a(l),i=a(m),b=a(i);H(b,{get name(){return e.icon},size:22,draw:!0}),s(i);var x=c(i,2),d=a(x),g=a(d,!0);s(d);var z=c(d,2);{var w=t=>{var r=q(),n=a(r,!0);s(r),h(()=>p(n,e.subtitle)),o(t,r)};_(z,t=>{e.subtitle&&t(w)})}s(x),s(m);var y=c(m,2);{var j=t=>{var r=A(),n=a(r);k(n,()=>e.children),s(r),o(t,r)};_(y,t=>{e.children&&t(j)})}s(l),h(()=>{I(i,1,`header-tile relative flex items-center justify-center w-11 h-11 rounded-xl shrink-0 - bg-${v()??""}/12 border border-${v()??""}/25 text-${v()??""}-glow`,"svelte-162svzm"),p(g,e.title)}),o(u,l)}export{M as P}; diff --git a/apps/dashboard/build/_app/immutable/chunks/BHDZZvku.js.br b/apps/dashboard/build/_app/immutable/chunks/BHDZZvku.js.br deleted file mode 100644 index 3474420..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/BHDZZvku.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BHDZZvku.js.gz b/apps/dashboard/build/_app/immutable/chunks/BHDZZvku.js.gz deleted file mode 100644 index 13f4a03..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/BHDZZvku.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BKuqSeVd.js b/apps/dashboard/build/_app/immutable/chunks/BKuqSeVd.js new file mode 100644 index 0000000..e8f917c --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/BKuqSeVd.js @@ -0,0 +1,2 @@ +const e=[...` +\r\f \v\uFEFF`];function o(t,f,u){var n=t==null?"":""+t;if(f&&(n=n?n+" "+f:f),u){for(var s of Object.keys(u))if(u[s])n=n?n+" "+s:s;else if(n.length)for(var i=s.length,l=0;(l=n.indexOf(s,l))>=0;){var r=l+i;(l===0||e.includes(n[l-1]))&&(r===n.length||e.includes(n[r]))?n=(l===0?"":n.substring(0,l))+n.substring(r+1):l=r}}return n===""?null:n}function c(t,f){return t==null?null:String(t)}export{c as a,o as t}; diff --git a/apps/dashboard/build/_app/immutable/chunks/BKuqSeVd.js.br b/apps/dashboard/build/_app/immutable/chunks/BKuqSeVd.js.br new file mode 100644 index 0000000..262d659 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/BKuqSeVd.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BKuqSeVd.js.gz b/apps/dashboard/build/_app/immutable/chunks/BKuqSeVd.js.gz new file mode 100644 index 0000000..967efc3 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/BKuqSeVd.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BLadwbF7.js.br b/apps/dashboard/build/_app/immutable/chunks/BLadwbF7.js.br deleted file mode 100644 index 3022c57..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/BLadwbF7.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BLadwbF7.js.gz b/apps/dashboard/build/_app/immutable/chunks/BLadwbF7.js.gz deleted file mode 100644 index 7691181..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/BLadwbF7.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BWk3o_TN.js b/apps/dashboard/build/_app/immutable/chunks/BWk3o_TN.js deleted file mode 100644 index cd19933..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/BWk3o_TN.js +++ /dev/null @@ -1 +0,0 @@ -var B=Object.defineProperty;var g=i=>{throw TypeError(i)};var F=(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)=>F(i,typeof e!="symbol"?e+"":e,s),M=(i,e,s)=>e.has(i)||g("Cannot "+s);var t=(i,e,s)=>(M(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),y=(i,e,s,a)=>(M(i,e,"write to private field"),a?a.call(i,s):e.set(i,s),s);import{w as D,Y as C,Z as k,_ as G,D as x,$ as A,G as S,M as Y,a0 as Z,a1 as $}from"./wpu9U-D0.js";var r,n,h,u,p,_,v;class z{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=D;if(t(this,r).has(e)){var s=t(this,r).get(e),a=t(this,n).get(s);if(a)C(a),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),a=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();Z(o,b),b.append(x()),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)||!a?(t(this,u).add(f),G(o,d,!1)):d()}}});l(this,v,e=>{t(this,r).delete(e);const s=Array.from(t(this,r).values());for(const[a,c]of t(this,h))s.includes(a)||(k(c.effect),t(this,h).delete(a))});this.anchor=e,y(this,p,s)}ensure(e,s){var a=D,c=$();if(s&&!t(this,n).has(e)&&!t(this,h).has(e))if(c){var f=document.createDocumentFragment(),o=x();f.append(o),t(this,h).set(e,{effect:A(()=>s(o)),fragment:f})}else t(this,n).set(e,A(()=>s(this.anchor)));if(t(this,r).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,h))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=Y),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{z as B}; diff --git a/apps/dashboard/build/_app/immutable/chunks/BWk3o_TN.js.br b/apps/dashboard/build/_app/immutable/chunks/BWk3o_TN.js.br deleted file mode 100644 index e3163e0..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/BWk3o_TN.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BWk3o_TN.js.gz b/apps/dashboard/build/_app/immutable/chunks/BWk3o_TN.js.gz deleted file mode 100644 index 1de713b..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/BWk3o_TN.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BZQzXWp7.js b/apps/dashboard/build/_app/immutable/chunks/BZQzXWp7.js deleted file mode 100644 index 1e262da..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/BZQzXWp7.js +++ /dev/null @@ -1,5 +0,0 @@ -import{TSL as t}from"./CfobEeQC.js";/** - * @license - * Copyright 2010-2024 Three.js Authors - * SPDX-License-Identifier: MIT - */const e=t.BRDF_GGX,n=t.BRDF_Lambert,s=t.BasicShadowFilter,r=t.Break,c=t.Continue,a=t.DFGApprox,i=t.D_GGX,l=t.Discard,m=t.EPSILON,p=t.F_Schlick,d=t.Fn,u=t.INFINITY,g=t.If,h=t.Loop,f=t.NodeShaderStage,x=t.NodeType,b=t.NodeUpdateType,w=t.NodeAccess,v=t.PCFShadowFilter,S=t.PCFSoftShadowFilter,T=t.PI,_=t.PI2,V=t.Return,y=t.Schlick_to_F0,D=t.ScriptableNodeResources,M=t.ShaderNode,F=t.TBNViewMatrix,C=t.VSMShadowFilter,I=t.V_GGX_SmithCorrelated,N=t.abs,P=t.acesFilmicToneMapping,R=t.acos,B=t.add,L=t.addNodeElement,k=t.agxToneMapping,A=t.all,G=t.alphaT,O=t.and,W=t.anisotropy,j=t.anisotropyB,U=t.anisotropyT,q=t.any,z=t.append,E=t.arrayBuffer,Z=t.asin,X=t.assign,Y=t.atan,H=t.atan2,J=t.atomicAdd,K=t.atomicAnd,Q=t.atomicFunc,$=t.atomicMax,tt=t.atomicMin,ot=t.atomicOr,et=t.atomicStore,nt=t.atomicSub,st=t.atomicXor,rt=t.attenuationColor,ct=t.attenuationDistance,at=t.attribute,it=t.attributeArray,lt=t.backgroundBlurriness,mt=t.backgroundIntensity,pt=t.backgroundRotation,dt=t.batch,ut=t.billboarding,gt=t.bitAnd,ht=t.bitNot,ft=t.bitOr,xt=t.bitXor,bt=t.bitangentGeometry,wt=t.bitangentLocal,vt=t.bitangentView,St=t.bitangentWorld,Tt=t.bitcast,_t=t.blendBurn,Vt=t.blendColor,yt=t.blendDodge,Dt=t.blendOverlay,Mt=t.blendScreen,Ft=t.blur,Ct=t.bool,It=t.buffer,Nt=t.bufferAttribute,Pt=t.bumpMap,Rt=t.burn,Bt=t.bvec2,Lt=t.bvec3,kt=t.bvec4,At=t.bypass,Gt=t.cache,Ot=t.call,Wt=t.cameraFar,jt=t.cameraNear,Ut=t.cameraNormalMatrix,qt=t.cameraPosition,zt=t.cameraProjectionMatrix,Et=t.cameraProjectionMatrixInverse,Zt=t.cameraViewMatrix,Xt=t.cameraWorldMatrix,Yt=t.cbrt,Ht=t.cdl,Jt=t.ceil,Kt=t.checker,Qt=t.cineonToneMapping,$t=t.clamp,to=t.clearcoat,oo=t.clearcoatRoughness,eo=t.code,no=t.color,so=t.colorSpaceToWorking,ro=t.colorToDirection,co=t.compute,ao=t.cond,io=t.context,lo=t.convert,mo=t.convertColorSpace,po=t.convertToTexture,uo=t.cos,go=t.cross,ho=t.cubeTexture,fo=t.dFdx,xo=t.dFdy,bo=t.dashSize,wo=t.defaultBuildStages,vo=t.defaultShaderStages,So=t.defined,To=t.degrees,_o=t.deltaTime,Vo=t.densityFog,yo=t.densityFogFactor,Do=t.depth,Mo=t.depthPass,Fo=t.difference,Co=t.diffuseColor,Io=t.directPointLight,No=t.directionToColor,Po=t.dispersion,Ro=t.distance,Bo=t.div,Lo=t.dodge,ko=t.dot,Ao=t.drawIndex,Go=t.dynamicBufferAttribute,Oo=t.element,Wo=t.emissive,jo=t.equal,Uo=t.equals,qo=t.equirectUV,zo=t.exp,Eo=t.exp2,Zo=t.expression,Xo=t.faceDirection,Yo=t.faceForward,Ho=t.faceforward,Jo=t.float,Ko=t.floor,Qo=t.fog,$o=t.fract,te=t.frameGroup,oe=t.frameId,ee=t.frontFacing,ne=t.fwidth,se=t.gain,re=t.gapSize,ce=t.getConstNodeType,ae=t.getCurrentStack,ie=t.getDirection,le=t.getDistanceAttenuation,me=t.getGeometryRoughness,pe=t.getNormalFromDepth,de=t.getParallaxCorrectNormal,ue=t.getRoughness,ge=t.getScreenPosition,he=t.getShIrradianceAt,fe=t.getTextureIndex,xe=t.getViewPosition,be=t.glsl,we=t.glslFn,ve=t.grayscale,Se=t.greaterThan,Te=t.greaterThanEqual,_e=t.hash,Ve=t.highpModelNormalViewMatrix,ye=t.highpModelViewMatrix,De=t.hue,Me=t.instance,Fe=t.instanceIndex,Ce=t.instancedArray,Ie=t.instancedBufferAttribute,Ne=t.instancedDynamicBufferAttribute,Pe=t.instancedMesh,Re=t.int,Be=t.inverseSqrt,Le=t.inversesqrt,ke=t.invocationLocalIndex,Ae=t.invocationSubgroupIndex,Ge=t.ior,Oe=t.iridescence,We=t.iridescenceIOR,je=t.iridescenceThickness,Ue=t.ivec2,qe=t.ivec3,ze=t.ivec4,Ee=t.js,Ze=t.label,Xe=t.length,Ye=t.lengthSq,He=t.lessThan,Je=t.lessThanEqual,Ke=t.lightPosition,Qe=t.lightTargetDirection,$e=t.lightTargetPosition,tn=t.lightViewPosition,on=t.lightingContext,en=t.lights,nn=t.linearDepth,sn=t.linearToneMapping,rn=t.localId,cn=t.log,an=t.log2,ln=t.logarithmicDepthToViewZ,mn=t.loop,pn=t.luminance,dn=t.mediumpModelViewMatrix,un=t.mat2,gn=t.mat3,hn=t.mat4,fn=t.matcapUV,xn=t.materialAO,bn=t.materialAlphaTest,wn=t.materialAnisotropy,vn=t.materialAnisotropyVector,Sn=t.materialAttenuationColor,Tn=t.materialAttenuationDistance,_n=t.materialClearcoat,Vn=t.materialClearcoatNormal,yn=t.materialClearcoatRoughness,Dn=t.materialColor,Mn=t.materialDispersion,Fn=t.materialEmissive,Cn=t.materialIOR,In=t.materialIridescence,Nn=t.materialIridescenceIOR,Pn=t.materialIridescenceThickness,Rn=t.materialLightMap,Bn=t.materialLineDashOffset,Ln=t.materialLineDashSize,kn=t.materialLineGapSize,An=t.materialLineScale,Gn=t.materialLineWidth,On=t.materialMetalness,Wn=t.materialNormal,jn=t.materialOpacity,Un=t.materialPointWidth,qn=t.materialReference,zn=t.materialReflectivity,En=t.materialRefractionRatio,Zn=t.materialRotation,Xn=t.materialRoughness,Yn=t.materialSheen,Hn=t.materialSheenRoughness,Jn=t.materialShininess,Kn=t.materialSpecular,Qn=t.materialSpecularColor,$n=t.materialSpecularIntensity,ts=t.materialSpecularStrength,os=t.materialThickness,es=t.materialTransmission,ns=t.max,ss=t.maxMipLevel,rs=t.metalness,cs=t.min,as=t.mix,is=t.mixElement,ls=t.mod,ms=t.modInt,ps=t.modelDirection,ds=t.modelNormalMatrix,us=t.modelPosition,gs=t.modelScale,hs=t.modelViewMatrix,fs=t.modelViewPosition,xs=t.modelViewProjection,bs=t.modelWorldMatrix,ws=t.modelWorldMatrixInverse,vs=t.morphReference,Ss=t.mrt,Ts=t.mul,_s=t.mx_aastep,Vs=t.mx_cell_noise_float,ys=t.mx_contrast,Ds=t.mx_fractal_noise_float,Ms=t.mx_fractal_noise_vec2,Fs=t.mx_fractal_noise_vec3,Cs=t.mx_fractal_noise_vec4,Is=t.mx_hsvtorgb,Ns=t.mx_noise_float,Ps=t.mx_noise_vec3,Rs=t.mx_noise_vec4,Bs=t.mx_ramplr,Ls=t.mx_ramptb,ks=t.mx_rgbtohsv,As=t.mx_safepower,Gs=t.mx_splitlr,Os=t.mx_splittb,Ws=t.mx_srgb_texture_to_lin_rec709,js=t.mx_transform_uv,Us=t.mx_worley_noise_float,qs=t.mx_worley_noise_vec2,zs=t.mx_worley_noise_vec3,Es=t.negate,Zs=t.neutralToneMapping,Xs=t.nodeArray,Ys=t.nodeImmutable,Hs=t.nodeObject,Js=t.nodeObjects,Ks=t.nodeProxy,Qs=t.normalFlat,$s=t.normalGeometry,tr=t.normalLocal,or=t.normalMap,er=t.normalView,nr=t.normalWorld,sr=t.normalize,rr=t.not,cr=t.notEqual,ar=t.numWorkgroups,ir=t.objectDirection,lr=t.objectGroup,mr=t.objectPosition,pr=t.objectScale,dr=t.objectViewPosition,ur=t.objectWorldMatrix,gr=t.oneMinus,hr=t.or,fr=t.orthographicDepthToViewZ,xr=t.oscSawtooth,br=t.oscSine,wr=t.oscSquare,vr=t.oscTriangle,Sr=t.output,Tr=t.outputStruct,_r=t.overlay,Vr=t.overloadingFn,yr=t.parabola,Dr=t.parallaxDirection,Mr=t.parallaxUV,Fr=t.parameter,Cr=t.pass,Ir=t.passTexture,Nr=t.pcurve,Pr=t.perspectiveDepthToViewZ,Rr=t.pmremTexture,Br=t.pointUV,Lr=t.pointWidth,kr=t.positionGeometry,Ar=t.positionLocal,Gr=t.positionPrevious,Or=t.positionView,Wr=t.positionViewDirection,jr=t.positionWorld,Ur=t.positionWorldDirection,qr=t.posterize,zr=t.pow,Er=t.pow2,Zr=t.pow3,Xr=t.pow4,Yr=t.property,Hr=t.radians,Jr=t.rand,Kr=t.range,Qr=t.rangeFog,$r=t.rangeFogFactor,tc=t.reciprocal,oc=t.reference,ec=t.referenceBuffer,nc=t.reflect,sc=t.reflectVector,rc=t.reflectView,cc=t.reflector,ac=t.refract,ic=t.refractVector,lc=t.refractView,mc=t.reinhardToneMapping,pc=t.remainder,dc=t.remap,uc=t.remapClamp,gc=t.renderGroup,hc=t.renderOutput,fc=t.rendererReference,xc=t.rotate,bc=t.rotateUV,wc=t.roughness,vc=t.round,Sc=t.rtt,Tc=t.sRGBTransferEOTF,_c=t.sRGBTransferOETF,Vc=t.sampler,yc=t.saturate,Dc=t.saturation,Mc=t.screen,Fc=t.screenCoordinate,Cc=t.screenSize,Ic=t.screenUV,Nc=t.scriptable,Pc=t.scriptableValue,Rc=t.select,Bc=t.setCurrentStack,Lc=t.shaderStages,kc=t.shadow,Ac=t.shadowPositionWorld,Gc=t.sharedUniformGroup,Oc=t.sheen,Wc=t.sheenRoughness,jc=t.shiftLeft,Uc=t.shiftRight,qc=t.shininess,zc=t.sign,Ec=t.sin,Zc=t.sinc,Xc=t.skinning,Yc=t.skinningReference,Hc=t.smoothstep,Jc=t.smoothstepElement,Kc=t.specularColor,Qc=t.specularF90,$c=t.spherizeUV,ta=t.split,oa=t.spritesheetUV,ea=t.sqrt,na=t.stack,sa=t.step,ra=t.storage,ca=t.storageBarrier,aa=t.storageObject,ia=t.storageTexture,la=t.string,ma=t.sub,pa=t.subgroupIndex,da=t.subgroupSize,ua=t.tan,ga=t.tangentGeometry,ha=t.tangentLocal,fa=t.tangentView,xa=t.tangentWorld,ba=t.temp,wa=t.texture,va=t.texture3D,Sa=t.textureBarrier,Ta=t.textureBicubic,_a=t.textureCubeUV,Va=t.textureLoad,ya=t.textureSize,Da=t.textureStore,Ma=t.thickness,Fa=t.threshold,Ca=t.time,Ia=t.timerDelta,Na=t.timerGlobal,Pa=t.timerLocal,Ra=t.toOutputColorSpace,Ba=t.toWorkingColorSpace,La=t.toneMapping,ka=t.toneMappingExposure,Aa=t.toonOutlinePass,Ga=t.transformDirection,Oa=t.transformNormal,Wa=t.transformNormalToView,ja=t.transformedBentNormalView,Ua=t.transformedBitangentView,qa=t.transformedBitangentWorld,za=t.transformedClearcoatNormalView,Ea=t.transformedNormalView,Za=t.transformedNormalWorld,Xa=t.transformedTangentView,Ya=t.transformedTangentWorld,Ha=t.transmission,Ja=t.transpose,Ka=t.tri,Qa=t.tri3,$a=t.triNoise3D,ti=t.triplanarTexture,oi=t.triplanarTextures,ei=t.trunc,ni=t.tslFn,si=t.uint,ri=t.uniform,ci=t.uniformArray,ai=t.uniformGroup,ii=t.uniforms,li=t.userData,mi=t.uv,pi=t.uvec2,di=t.uvec3,ui=t.uvec4,gi=t.varying,hi=t.varyingProperty,fi=t.vec2,xi=t.vec3,bi=t.vec4,wi=t.vectorComponents,vi=t.velocity,Si=t.vertexColor,Ti=t.vertexIndex,_i=t.vibrance,Vi=t.viewZToLogarithmicDepth,yi=t.viewZToOrthographicDepth,Di=t.viewZToPerspectiveDepth,Mi=t.viewport,Fi=t.viewportBottomLeft,Ci=t.viewportCoordinate,Ii=t.viewportDepthTexture,Ni=t.viewportLinearDepth,Pi=t.viewportMipTexture,Ri=t.viewportResolution,Bi=t.viewportSafeUV,Li=t.viewportSharedTexture,ki=t.viewportSize,Ai=t.viewportTexture,Gi=t.viewportTopLeft,Oi=t.viewportUV,Wi=t.wgsl,ji=t.wgslFn,Ui=t.workgroupArray,qi=t.workgroupBarrier,zi=t.workgroupId,Ei=t.workingToColorSpace,Zi=t.xor;export{e as BRDF_GGX,n as BRDF_Lambert,s as BasicShadowFilter,r as Break,c as Continue,a as DFGApprox,i as D_GGX,l as Discard,m as EPSILON,p as F_Schlick,d as Fn,u as INFINITY,g as If,h as Loop,w as NodeAccess,f as NodeShaderStage,x as NodeType,b as NodeUpdateType,v as PCFShadowFilter,S as PCFSoftShadowFilter,T as PI,_ as PI2,V as Return,y as Schlick_to_F0,D as ScriptableNodeResources,M as ShaderNode,F as TBNViewMatrix,C as VSMShadowFilter,I as V_GGX_SmithCorrelated,N as abs,P as acesFilmicToneMapping,R as acos,B as add,L as addNodeElement,k as agxToneMapping,A as all,G as alphaT,O as and,W as anisotropy,j as anisotropyB,U as anisotropyT,q as any,z as append,E as arrayBuffer,Z as asin,X as assign,Y as atan,H as atan2,J as atomicAdd,K as atomicAnd,Q as atomicFunc,$ as atomicMax,tt as atomicMin,ot as atomicOr,et as atomicStore,nt as atomicSub,st as atomicXor,rt as attenuationColor,ct as attenuationDistance,at as attribute,it as attributeArray,lt as backgroundBlurriness,mt as backgroundIntensity,pt as backgroundRotation,dt as batch,ut as billboarding,gt as bitAnd,ht as bitNot,ft as bitOr,xt as bitXor,bt as bitangentGeometry,wt as bitangentLocal,vt as bitangentView,St as bitangentWorld,Tt as bitcast,_t as blendBurn,Vt as blendColor,yt as blendDodge,Dt as blendOverlay,Mt as blendScreen,Ft as blur,Ct as bool,It as buffer,Nt as bufferAttribute,Pt as bumpMap,Rt as burn,Bt as bvec2,Lt as bvec3,kt as bvec4,At as bypass,Gt as cache,Ot as call,Wt as cameraFar,jt as cameraNear,Ut as cameraNormalMatrix,qt as cameraPosition,zt as cameraProjectionMatrix,Et as cameraProjectionMatrixInverse,Zt as cameraViewMatrix,Xt as cameraWorldMatrix,Yt as cbrt,Ht as cdl,Jt as ceil,Kt as checker,Qt as cineonToneMapping,$t as clamp,to as clearcoat,oo as clearcoatRoughness,eo as code,no as color,so as colorSpaceToWorking,ro as colorToDirection,co as compute,ao as cond,io as context,lo as convert,mo as convertColorSpace,po as convertToTexture,uo as cos,go as cross,ho as cubeTexture,fo as dFdx,xo as dFdy,bo as dashSize,wo as defaultBuildStages,vo as defaultShaderStages,So as defined,To as degrees,_o as deltaTime,Vo as densityFog,yo as densityFogFactor,Do as depth,Mo as depthPass,Fo as difference,Co as diffuseColor,Io as directPointLight,No as directionToColor,Po as dispersion,Ro as distance,Bo as div,Lo as dodge,ko as dot,Ao as drawIndex,Go as dynamicBufferAttribute,Oo as element,Wo as emissive,jo as equal,Uo as equals,qo as equirectUV,zo as exp,Eo as exp2,Zo as expression,Xo as faceDirection,Yo as faceForward,Ho as faceforward,Jo as float,Ko as floor,Qo as fog,$o as fract,te as frameGroup,oe as frameId,ee as frontFacing,ne as fwidth,se as gain,re as gapSize,ce as getConstNodeType,ae as getCurrentStack,ie as getDirection,le as getDistanceAttenuation,me as getGeometryRoughness,pe as getNormalFromDepth,de as getParallaxCorrectNormal,ue as getRoughness,ge as getScreenPosition,he as getShIrradianceAt,fe as getTextureIndex,xe as getViewPosition,be as glsl,we as glslFn,ve as grayscale,Se as greaterThan,Te as greaterThanEqual,_e as hash,Ve as highpModelNormalViewMatrix,ye as highpModelViewMatrix,De as hue,Me as instance,Fe as instanceIndex,Ce as instancedArray,Ie as instancedBufferAttribute,Ne as instancedDynamicBufferAttribute,Pe as instancedMesh,Re as int,Be as inverseSqrt,Le as inversesqrt,ke as invocationLocalIndex,Ae as invocationSubgroupIndex,Ge as ior,Oe as iridescence,We as iridescenceIOR,je as iridescenceThickness,Ue as ivec2,qe as ivec3,ze as ivec4,Ee as js,Ze as label,Xe as length,Ye as lengthSq,He as lessThan,Je as lessThanEqual,Ke as lightPosition,Qe as lightTargetDirection,$e as lightTargetPosition,tn as lightViewPosition,on as lightingContext,en as lights,nn as linearDepth,sn as linearToneMapping,rn as localId,cn as log,an as log2,ln as logarithmicDepthToViewZ,mn as loop,pn as luminance,un as mat2,gn as mat3,hn as mat4,fn as matcapUV,xn as materialAO,bn as materialAlphaTest,wn as materialAnisotropy,vn as materialAnisotropyVector,Sn as materialAttenuationColor,Tn as materialAttenuationDistance,_n as materialClearcoat,Vn as materialClearcoatNormal,yn as materialClearcoatRoughness,Dn as materialColor,Mn as materialDispersion,Fn as materialEmissive,Cn as materialIOR,In as materialIridescence,Nn as materialIridescenceIOR,Pn as materialIridescenceThickness,Rn as materialLightMap,Bn as materialLineDashOffset,Ln as materialLineDashSize,kn as materialLineGapSize,An as materialLineScale,Gn as materialLineWidth,On as materialMetalness,Wn as materialNormal,jn as materialOpacity,Un as materialPointWidth,qn as materialReference,zn as materialReflectivity,En as materialRefractionRatio,Zn as materialRotation,Xn as materialRoughness,Yn as materialSheen,Hn as materialSheenRoughness,Jn as materialShininess,Kn as materialSpecular,Qn as materialSpecularColor,$n as materialSpecularIntensity,ts as materialSpecularStrength,os as materialThickness,es as materialTransmission,ns as max,ss as maxMipLevel,dn as mediumpModelViewMatrix,rs as metalness,cs as min,as as mix,is as mixElement,ls as mod,ms as modInt,ps as modelDirection,ds as modelNormalMatrix,us as modelPosition,gs as modelScale,hs as modelViewMatrix,fs as modelViewPosition,xs as modelViewProjection,bs as modelWorldMatrix,ws as modelWorldMatrixInverse,vs as morphReference,Ss as mrt,Ts as mul,_s as mx_aastep,Vs as mx_cell_noise_float,ys as mx_contrast,Ds as mx_fractal_noise_float,Ms as mx_fractal_noise_vec2,Fs as mx_fractal_noise_vec3,Cs as mx_fractal_noise_vec4,Is as mx_hsvtorgb,Ns as mx_noise_float,Ps as mx_noise_vec3,Rs as mx_noise_vec4,Bs as mx_ramplr,Ls as mx_ramptb,ks as mx_rgbtohsv,As as mx_safepower,Gs as mx_splitlr,Os as mx_splittb,Ws as mx_srgb_texture_to_lin_rec709,js as mx_transform_uv,Us as mx_worley_noise_float,qs as mx_worley_noise_vec2,zs as mx_worley_noise_vec3,Es as negate,Zs as neutralToneMapping,Xs as nodeArray,Ys as nodeImmutable,Hs as nodeObject,Js as nodeObjects,Ks as nodeProxy,Qs as normalFlat,$s as normalGeometry,tr as normalLocal,or as normalMap,er as normalView,nr as normalWorld,sr as normalize,rr as not,cr as notEqual,ar as numWorkgroups,ir as objectDirection,lr as objectGroup,mr as objectPosition,pr as objectScale,dr as objectViewPosition,ur as objectWorldMatrix,gr as oneMinus,hr as or,fr as orthographicDepthToViewZ,xr as oscSawtooth,br as oscSine,wr as oscSquare,vr as oscTriangle,Sr as output,Tr as outputStruct,_r as overlay,Vr as overloadingFn,yr as parabola,Dr as parallaxDirection,Mr as parallaxUV,Fr as parameter,Cr as pass,Ir as passTexture,Nr as pcurve,Pr as perspectiveDepthToViewZ,Rr as pmremTexture,Br as pointUV,Lr as pointWidth,kr as positionGeometry,Ar as positionLocal,Gr as positionPrevious,Or as positionView,Wr as positionViewDirection,jr as positionWorld,Ur as positionWorldDirection,qr as posterize,zr as pow,Er as pow2,Zr as pow3,Xr as pow4,Yr as property,Hr as radians,Jr as rand,Kr as range,Qr as rangeFog,$r as rangeFogFactor,tc as reciprocal,oc as reference,ec as referenceBuffer,nc as reflect,sc as reflectVector,rc as reflectView,cc as reflector,ac as refract,ic as refractVector,lc as refractView,mc as reinhardToneMapping,pc as remainder,dc as remap,uc as remapClamp,gc as renderGroup,hc as renderOutput,fc as rendererReference,xc as rotate,bc as rotateUV,wc as roughness,vc as round,Sc as rtt,Tc as sRGBTransferEOTF,_c as sRGBTransferOETF,Vc as sampler,yc as saturate,Dc as saturation,Mc as screen,Fc as screenCoordinate,Cc as screenSize,Ic as screenUV,Nc as scriptable,Pc as scriptableValue,Rc as select,Bc as setCurrentStack,Lc as shaderStages,kc as shadow,Ac as shadowPositionWorld,Gc as sharedUniformGroup,Oc as sheen,Wc as sheenRoughness,jc as shiftLeft,Uc as shiftRight,qc as shininess,zc as sign,Ec as sin,Zc as sinc,Xc as skinning,Yc as skinningReference,Hc as smoothstep,Jc as smoothstepElement,Kc as specularColor,Qc as specularF90,$c as spherizeUV,ta as split,oa as spritesheetUV,ea as sqrt,na as stack,sa as step,ra as storage,ca as storageBarrier,aa as storageObject,ia as storageTexture,la as string,ma as sub,pa as subgroupIndex,da as subgroupSize,ua as tan,ga as tangentGeometry,ha as tangentLocal,fa as tangentView,xa as tangentWorld,ba as temp,wa as texture,va as texture3D,Sa as textureBarrier,Ta as textureBicubic,_a as textureCubeUV,Va as textureLoad,ya as textureSize,Da as textureStore,Ma as thickness,Fa as threshold,Ca as time,Ia as timerDelta,Na as timerGlobal,Pa as timerLocal,Ra as toOutputColorSpace,Ba as toWorkingColorSpace,La as toneMapping,ka as toneMappingExposure,Aa as toonOutlinePass,Ga as transformDirection,Oa as transformNormal,Wa as transformNormalToView,ja as transformedBentNormalView,Ua as transformedBitangentView,qa as transformedBitangentWorld,za as transformedClearcoatNormalView,Ea as transformedNormalView,Za as transformedNormalWorld,Xa as transformedTangentView,Ya as transformedTangentWorld,Ha as transmission,Ja as transpose,Ka as tri,Qa as tri3,$a as triNoise3D,ti as triplanarTexture,oi as triplanarTextures,ei as trunc,ni as tslFn,si as uint,ri as uniform,ci as uniformArray,ai as uniformGroup,ii as uniforms,li as userData,mi as uv,pi as uvec2,di as uvec3,ui as uvec4,gi as varying,hi as varyingProperty,fi as vec2,xi as vec3,bi as vec4,wi as vectorComponents,vi as velocity,Si as vertexColor,Ti as vertexIndex,_i as vibrance,Vi as viewZToLogarithmicDepth,yi as viewZToOrthographicDepth,Di as viewZToPerspectiveDepth,Mi as viewport,Fi as viewportBottomLeft,Ci as viewportCoordinate,Ii as viewportDepthTexture,Ni as viewportLinearDepth,Pi as viewportMipTexture,Ri as viewportResolution,Bi as viewportSafeUV,Li as viewportSharedTexture,ki as viewportSize,Ai as viewportTexture,Gi as viewportTopLeft,Oi as viewportUV,Wi as wgsl,ji as wgslFn,Ui as workgroupArray,qi as workgroupBarrier,zi as workgroupId,Ei as workingToColorSpace,Zi as xor}; diff --git a/apps/dashboard/build/_app/immutable/chunks/BZQzXWp7.js.br b/apps/dashboard/build/_app/immutable/chunks/BZQzXWp7.js.br deleted file mode 100644 index 84b2088..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/BZQzXWp7.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BZQzXWp7.js.gz b/apps/dashboard/build/_app/immutable/chunks/BZQzXWp7.js.gz deleted file mode 100644 index f4670dd..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/BZQzXWp7.js.gz and /dev/null 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..476614c 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/BhIgFntf.js b/apps/dashboard/build/_app/immutable/chunks/BhIgFntf.js deleted file mode 100644 index 0f8a968..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/BhIgFntf.js +++ /dev/null @@ -1 +0,0 @@ -import{b as c,w as g}from"./D8mhvFt8.js";const v=200;function $(){const{subscribe:e,set:n,update:t}=g({connected:!1,reconnecting:!1,events:[],lastHeartbeat:null,error:null});let o=null,s=null,d=0;function m(i){const l=i||(window.location.port==="5173"?`ws://${window.location.hostname}:3927/ws`:`ws://${window.location.host}/ws`);if((o==null?void 0:o.readyState)!==WebSocket.OPEN)try{o=new WebSocket(l),o.onopen=()=>{d=0,t(r=>({...r,connected:!0,reconnecting:!1,error:null}))},o.onmessage=r=>{try{const u=JSON.parse(r.data);t(f=>{if(u.type==="Heartbeat")return{...f,lastHeartbeat:u};const w=[u,...f.events].slice(0,v);return{...f,events:w}})}catch(u){console.warn("[vestige] Failed to parse WebSocket message:",u)}},o.onclose=()=>{t(r=>({...r,connected:!1})),p(l)},o.onerror=()=>{t(r=>({...r,error:"WebSocket connection failed"}))}}catch(r){t(u=>({...u,error:String(r)}))}}function p(i){s&&clearTimeout(s),t(r=>({...r,reconnecting:!0}));const l=Math.min(1e3*2**d,3e4);d++,s=setTimeout(()=>m(i),l)}function b(){s&&clearTimeout(s),o==null||o.close(),o=null,n({connected:!1,reconnecting:!1,events:[],lastHeartbeat:null,error:null})}function y(){t(i=>({...i,events:[]}))}function h(i){t(l=>{const r=[i,...l.events].slice(0,v);return{...l,events:r}})}return{subscribe:e,connect:m,disconnect:b,clearEvents:y,injectEvent:h}}const a=$(),S=c(a,e=>e.connected),H=c(a,e=>e.reconnecting),T=c(a,e=>e.events);c(a,e=>e.lastHeartbeat);const M=c(a,e=>{var n,t;return((t=(n=e.lastHeartbeat)==null?void 0:n.data)==null?void 0:t.memory_count)??0}),k=c(a,e=>{var n,t;return((t=(n=e.lastHeartbeat)==null?void 0:n.data)==null?void 0:t.avg_retention)??0}),_=c(a,e=>{var n,t;return((t=(n=e.lastHeartbeat)==null?void 0:n.data)==null?void 0:t.suppressed_count)??0}),W=c(a,e=>{var n,t;return((t=(n=e.lastHeartbeat)==null?void 0:n.data)==null?void 0:t.uptime_secs)??0}),N=c(a,e=>e.events.filter(n=>n.type==="TraceEvent")),P=c(a,e=>{var t;const n=e.events.find(o=>o.type==="TraceEvent");return((t=n==null?void 0:n.data)==null?void 0:t.run_id)??null}),R=c(a,e=>e.events.find(n=>n.type==="TraceEvent")??null),C=c(a,e=>e.events.filter(n=>n.type==="MemoryPrOpened"||n.type==="MemoryPrDecided"));function F(e){if(!Number.isFinite(e)||e<0)return"—";const n=Math.floor(e/86400),t=Math.floor(e%86400/3600),o=Math.floor(e%3600/60),s=Math.floor(e%60);return n>0?t>0?`${n}d ${t}h`:`${n}d`:t>0?o>0?`${t}h ${o}m`:`${t}h`:o>0?s>0?`${o}m ${s}s`:`${o}m`:`${s}s`}export{k as a,M as b,H as c,P as d,T as e,F as f,S as i,R as l,C as m,_ as s,N as t,W as u,a as w}; diff --git a/apps/dashboard/build/_app/immutable/chunks/BhIgFntf.js.br b/apps/dashboard/build/_app/immutable/chunks/BhIgFntf.js.br deleted file mode 100644 index 56deb73..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/BhIgFntf.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BhIgFntf.js.gz b/apps/dashboard/build/_app/immutable/chunks/BhIgFntf.js.gz deleted file mode 100644 index 1102382..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/BhIgFntf.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/Bhad70Ss.js b/apps/dashboard/build/_app/immutable/chunks/Bhad70Ss.js new file mode 100644 index 0000000..9cdeb4b --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/Bhad70Ss.js @@ -0,0 +1 @@ +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..dc811ec 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/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..332d14a 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/Bxs5UR9-.js b/apps/dashboard/build/_app/immutable/chunks/Bxs5UR9-.js deleted file mode 100644 index 4ee383e..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/Bxs5UR9-.js +++ /dev/null @@ -1 +0,0 @@ -var x=t=>{throw TypeError(t)};var B=(t,e,n)=>e.has(t)||x("Cannot "+n);var a=(t,e,n)=>(B(t,e,"read from private field"),n?n.call(t):e.get(t)),c=(t,e,n)=>e.has(t)?x("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(t):e.set(t,n);import{o as I}from"./TZu9D97Z.js";import{s as u,g as f,i as d}from"./wpu9U-D0.js";import{w as G}from"./D8mhvFt8.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"&&w.delete(U(t)),z(t,e));const w=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&&w.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(w.size>0){const r=U(t,n),s=w.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 b,_,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,b,u({}));c(this,_,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,b))}set data(e){d(a(this,b),e)}get form(){return f(a(this,_))}set form(e){d(a(this,_),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)}},b=new WeakMap,_=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{we as H,be 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,_e 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/Bxs5UR9-.js.br b/apps/dashboard/build/_app/immutable/chunks/Bxs5UR9-.js.br deleted file mode 100644 index 11713a3..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/Bxs5UR9-.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/Bxs5UR9-.js.gz b/apps/dashboard/build/_app/immutable/chunks/Bxs5UR9-.js.gz deleted file mode 100644 index 6acb963..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/Bxs5UR9-.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/ByYB047u.js b/apps/dashboard/build/_app/immutable/chunks/ByYB047u.js deleted file mode 100644 index 1776b5d..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/ByYB047u.js +++ /dev/null @@ -1 +0,0 @@ -import{ab as R,ac as y,g as o,y as N,ad as U,i as A,ae as B,af as M,ag as Y,d as $,ah as h,ai as q,aj as w,ak as x,P as j,al as p,am as z,an as C,a7 as G,ao as Z,ap as F,S as H,aq as J}from"./wpu9U-D0.js";import{c as K,g as Q}from"./D8mhvFt8.js";let v=!1,g=Symbol();function k(e,r,i){const n=i[r]??(i[r]={store:null,source:y(void 0),unsubscribe:R});if(n.store!==e&&!(g in i))if(n.unsubscribe(),n.store=e??null,e==null)n.source.v=void 0,n.unsubscribe=R;else{var u=!0;n.unsubscribe=K(e,t=>{u?n.source.v=t:A(n.source,t)}),u=!1}return e&&g in i?Q(e):o(n.source)}function ee(){const e={};function r(){N(()=>{for(var i in e)e[i].unsubscribe();U(e,g,{enumerable:!1,value:!0})})}return[e,r]}function V(e){var r=v;try{return v=!1,[e(),v]}finally{v=r}}function re(e,r,i,n){var E;var u=!p||(i&z)!==0,t=(i&x)!==0,T=(i&F)!==0,f=n,b=!0,P=()=>(b&&(b=!1,f=T?j(n):n),f),d;if(t){var m=H in e||J in e;d=((E=B(e,r))==null?void 0:E.set)??(m&&r in e?a=>e[r]=a:void 0)}var _,I=!1;t?[_,I]=V(()=>e[r]):_=e[r],_===void 0&&n!==void 0&&(_=P(),d&&(u&&M(),d(_)));var s;if(u?s=()=>{var a=e[r];return a===void 0?P():(b=!0,a)}:s=()=>{var a=e[r];return a!==void 0&&(f=void 0),a===void 0?f:a},u&&(i&Y)===0)return s;if(d){var D=e.$$legacy;return(function(a,c){return arguments.length>0?((!u||!c||D||I)&&d(c?s():a),a):s()})}var S=!1,l=((i&C)!==0?G:Z)(()=>(S=!1,s()));t&&o(l);var L=q;return(function(a,c){if(arguments.length>0){const O=c?o(l):u&&t?$(a):a;return A(l,O),S=!0,f!==void 0&&(f=O),a}return h&&S||(L.f&w)!==0?l.v:o(l)})}export{ee as a,re as p,k as s}; diff --git a/apps/dashboard/build/_app/immutable/chunks/ByYB047u.js.br b/apps/dashboard/build/_app/immutable/chunks/ByYB047u.js.br deleted file mode 100644 index 372bba1..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/ByYB047u.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/ByYB047u.js.gz b/apps/dashboard/build/_app/immutable/chunks/ByYB047u.js.gz deleted file mode 100644 index cbf9823..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/ByYB047u.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BLadwbF7.js b/apps/dashboard/build/_app/immutable/chunks/Bz1l2A_1.js similarity index 76% rename from apps/dashboard/build/_app/immutable/chunks/BLadwbF7.js rename to apps/dashboard/build/_app/immutable/chunks/Bz1l2A_1.js index 5a1ac32..d247327 100644 --- a/apps/dashboard/build/_app/immutable/chunks/BLadwbF7.js +++ b/apps/dashboard/build/_app/immutable/chunks/Bz1l2A_1.js @@ -1 +1 @@ -import{a2 as g,a3 as d,o as c,P as m,a4 as i,a5 as b,g as p,a6 as v,a7 as h,a8 as k}from"./wpu9U-D0.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}; +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..6ede0fd 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/Bzak7iHL.js.gz b/apps/dashboard/build/_app/immutable/chunks/Bzak7iHL.js.gz index f429ed1..0311328 100644 Binary files a/apps/dashboard/build/_app/immutable/chunks/Bzak7iHL.js.gz and b/apps/dashboard/build/_app/immutable/chunks/Bzak7iHL.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/C-SOZ1Oi.js b/apps/dashboard/build/_app/immutable/chunks/C-SOZ1Oi.js deleted file mode 100644 index 27a1889..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/C-SOZ1Oi.js +++ /dev/null @@ -1,4118 +0,0 @@ -const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./Ma4NfFrG.js","./Dp1pzeXC.js"])))=>i.map(i=>d[i]); -var ff=Object.defineProperty;var pf=(r,e,t)=>e in r?ff(r,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):r[e]=t;var ke=(r,e,t)=>pf(r,typeof e!="symbol"?e+"":e,t);import"./Bzak7iHL.js";import{o as od,a as Ac}from"./TZu9D97Z.js";import{s as Qe,d as Al,i as le,g as L,p as sr,o as Ga,a as ot,b as rr,j as ut,e as xe,h as Ee,n as Or,r as fe,t as Pt,u as ei,c as mf,f as no,m as gf,l as _f}from"./wpu9U-D0.js";import{s as ct,d as Cc,a as Ut}from"./D8mhvFt8.js";import{i as It}from"./DKve45Wd.js";import{e as Pr,i as Wa,a as Rt,r as Br,s as Ns,c as vf}from"./60_R_Vbt.js";import{s as zr}from"./EqHb-9AZ.js";import{b as Cl,a as ph}from"./CnZzd20v.js";import{p as Dr,a as xf,s as yf}from"./ByYB047u.js";import{b as mh}from"./Bxs5UR9-.js";import{b as Rl}from"./g5OnrUYZ.js";import{N as Il}from"./CcUbQ_Wl.js";import{b as Mf}from"./DrafHjYM.js";import{i as bf}from"./BLadwbF7.js";import{_ as Sf}from"./Dp1pzeXC.js";import{I as ps}from"./D7A-gG4Z.js";import{D as wf}from"./CmbJHhgy.js";import{a as ms}from"./CZfHMhLI.js";import{e as Ef}from"./BhIgFntf.js";/** - * @license - * Copyright 2010-2024 Three.js Authors - * SPDX-License-Identifier: MIT - */const Rc="172",Xs={LEFT:0,MIDDLE:1,RIGHT:2,ROTATE:0,DOLLY:1,PAN:2},Os={ROTATE:0,PAN:1,DOLLY_PAN:2,DOLLY_ROTATE:3},Tf=0,gh=1,Af=2,sS=3,rS=0,ld=1,Cf=2,Qn=3,Ci=0,fn=1,Bn=2,ii=0,qs=1,Wt=2,_h=3,vh=4,Rf=5,Qi=100,If=101,Pf=102,Df=103,Lf=104,Uf=200,Nf=201,Ff=202,Of=203,Pl=204,Dl=205,Bf=206,zf=207,kf=208,Vf=209,Hf=210,Gf=211,Wf=212,Xf=213,qf=214,Ll=0,Ul=1,Nl=2,Ks=3,Fl=4,Ol=5,Bl=6,zl=7,bo=0,Yf=1,Zf=2,Ti=0,Jf=1,Kf=2,$f=3,cd=4,jf=5,Qf=6,ep=7,xh="attached",tp="detached",Ic=300,Ri=301,os=302,io=303,so=304,qr=306,ro=1e3,Nn=1001,ao=1002,Qt=1003,hd=1004,aS=1004,Cr=1005,oS=1005,Vt=1006,Xa=1007,lS=1007,ni=1008,cS=1008,li=1009,ud=1010,dd=1011,kr=1012,Pc=1013,Ii=1014,Mn=1015,si=1016,Dc=1017,Lc=1018,$s=1020,fd=35902,pd=1021,md=1022,dn=1023,gd=1024,_d=1025,Ys=1026,js=1027,Uc=1028,So=1029,vd=1030,Nc=1031,hS=1032,Fc=1033,qa=33776,Ya=33777,Za=33778,Ja=33779,kl=35840,Vl=35841,Hl=35842,Gl=35843,Wl=36196,Xl=37492,ql=37496,Yl=37808,Zl=37809,Jl=37810,Kl=37811,$l=37812,jl=37813,Ql=37814,ec=37815,tc=37816,nc=37817,ic=37818,sc=37819,rc=37820,ac=37821,Ka=36492,oc=36494,lc=36495,xd=36283,cc=36284,hc=36285,uc=36286,np=2200,ip=2201,sp=2202,oo=2300,dc=2301,Fo=2302,Bs=2400,zs=2401,lo=2402,Oc=2500,yd=2501,uS=0,dS=1,fS=2,rp=3200,ap=3201,pS=3202,mS=3203,cs=0,op=1,wi="",xn="srgb",Qs="srgb-linear",co="linear",vt="srgb",gS=0,gs=7680,_S=7681,vS=7682,xS=7683,yS=34055,MS=34056,bS=5386,SS=512,wS=513,ES=514,TS=515,AS=516,CS=517,RS=518,yh=519,lp=512,cp=513,hp=514,Md=515,up=516,dp=517,fp=518,pp=519,ho=35044,IS=35048,PS=35040,DS=35045,LS=35049,US=35041,NS=35046,FS=35050,OS=35042,BS="100",Mh="300 es",zn=2e3,uo=2001;class ci{addEventListener(e,t){this._listeners===void 0&&(this._listeners={});const n=this._listeners;n[e]===void 0&&(n[e]=[]),n[e].indexOf(t)===-1&&n[e].push(t)}hasEventListener(e,t){if(this._listeners===void 0)return!1;const n=this._listeners;return n[e]!==void 0&&n[e].indexOf(t)!==-1}removeEventListener(e,t){if(this._listeners===void 0)return;const i=this._listeners[e];if(i!==void 0){const s=i.indexOf(t);s!==-1&&i.splice(s,1)}}dispatchEvent(e){if(this._listeners===void 0)return;const n=this._listeners[e.type];if(n!==void 0){e.target=this;const i=n.slice(0);for(let s=0,a=i.length;s>8&255]+Jt[r>>16&255]+Jt[r>>24&255]+"-"+Jt[e&255]+Jt[e>>8&255]+"-"+Jt[e>>16&15|64]+Jt[e>>24&255]+"-"+Jt[t&63|128]+Jt[t>>8&255]+"-"+Jt[t>>16&255]+Jt[t>>24&255]+Jt[n&255]+Jt[n>>8&255]+Jt[n>>16&255]+Jt[n>>24&255]).toLowerCase()}function et(r,e,t){return Math.max(e,Math.min(t,r))}function Bc(r,e){return(r%e+e)%e}function mp(r,e,t,n,i){return n+(r-e)*(i-n)/(t-e)}function gp(r,e,t){return r!==e?(t-r)/(e-r):0}function Lr(r,e,t){return(1-t)*r+t*e}function _p(r,e,t,n){return Lr(r,e,1-Math.exp(-t*n))}function vp(r,e=1){return e-Math.abs(Bc(r,e*2)-e)}function xp(r,e,t){return r<=e?0:r>=t?1:(r=(r-e)/(t-e),r*r*(3-2*r))}function yp(r,e,t){return r<=e?0:r>=t?1:(r=(r-e)/(t-e),r*r*r*(r*(r*6-15)+10))}function Mp(r,e){return r+Math.floor(Math.random()*(e-r+1))}function bp(r,e){return r+Math.random()*(e-r)}function Sp(r){return r*(.5-Math.random())}function wp(r){r!==void 0&&(bh=r);let e=bh+=1831565813;return e=Math.imul(e^e>>>15,e|1),e^=e+Math.imul(e^e>>>7,e|61),((e^e>>>14)>>>0)/4294967296}function Ep(r){return r*ss}function Tp(r){return r*er}function Ap(r){return(r&r-1)===0&&r!==0}function Cp(r){return Math.pow(2,Math.ceil(Math.log(r)/Math.LN2))}function Rp(r){return Math.pow(2,Math.floor(Math.log(r)/Math.LN2))}function Ip(r,e,t,n,i){const s=Math.cos,a=Math.sin,o=s(t/2),l=a(t/2),c=s((e+n)/2),h=a((e+n)/2),u=s((e-n)/2),f=a((e-n)/2),d=s((n-e)/2),p=a((n-e)/2);switch(i){case"XYX":r.set(o*h,l*u,l*f,o*c);break;case"YZY":r.set(l*f,o*h,l*u,o*c);break;case"ZXZ":r.set(l*u,l*f,o*h,o*c);break;case"XZX":r.set(o*h,l*p,l*d,o*c);break;case"YXY":r.set(l*d,o*h,l*p,o*c);break;case"ZYZ":r.set(l*p,l*d,o*h,o*c);break;default:console.warn("THREE.MathUtils: .setQuaternionFromProperEuler() encountered an unknown order: "+i)}}function sn(r,e){switch(e.constructor){case Float32Array:return r;case Uint32Array:return r/4294967295;case Uint16Array:return r/65535;case Uint8Array:return r/255;case Int32Array:return Math.max(r/2147483647,-1);case Int16Array:return Math.max(r/32767,-1);case Int8Array:return Math.max(r/127,-1);default:throw new Error("Invalid component type.")}}function nt(r,e){switch(e.constructor){case Float32Array:return r;case Uint32Array:return Math.round(r*4294967295);case Uint16Array:return Math.round(r*65535);case Uint8Array:return Math.round(r*255);case Int32Array:return Math.round(r*2147483647);case Int16Array:return Math.round(r*32767);case Int8Array:return Math.round(r*127);default:throw new Error("Invalid component type.")}}const bd={DEG2RAD:ss,RAD2DEG:er,generateUUID:bn,clamp:et,euclideanModulo:Bc,mapLinear:mp,inverseLerp:gp,lerp:Lr,damp:_p,pingpong:vp,smoothstep:xp,smootherstep:yp,randInt:Mp,randFloat:bp,randFloatSpread:Sp,seededRandom:wp,degToRad:Ep,radToDeg:Tp,isPowerOfTwo:Ap,ceilPowerOfTwo:Cp,floorPowerOfTwo:Rp,setQuaternionFromProperEuler:Ip,normalize:nt,denormalize:sn};class j{constructor(e=0,t=0){j.prototype.isVector2=!0,this.x=e,this.y=t}get width(){return this.x}set width(e){this.x=e}get height(){return this.y}set height(e){this.y=e}set(e,t){return this.x=e,this.y=t,this}setScalar(e){return this.x=e,this.y=e,this}setX(e){return this.x=e,this}setY(e){return this.y=e,this}setComponent(e,t){switch(e){case 0:this.x=t;break;case 1:this.y=t;break;default:throw new Error("index is out of range: "+e)}return this}getComponent(e){switch(e){case 0:return this.x;case 1:return this.y;default:throw new Error("index is out of range: "+e)}}clone(){return new this.constructor(this.x,this.y)}copy(e){return this.x=e.x,this.y=e.y,this}add(e){return this.x+=e.x,this.y+=e.y,this}addScalar(e){return this.x+=e,this.y+=e,this}addVectors(e,t){return this.x=e.x+t.x,this.y=e.y+t.y,this}addScaledVector(e,t){return this.x+=e.x*t,this.y+=e.y*t,this}sub(e){return this.x-=e.x,this.y-=e.y,this}subScalar(e){return this.x-=e,this.y-=e,this}subVectors(e,t){return this.x=e.x-t.x,this.y=e.y-t.y,this}multiply(e){return this.x*=e.x,this.y*=e.y,this}multiplyScalar(e){return this.x*=e,this.y*=e,this}divide(e){return this.x/=e.x,this.y/=e.y,this}divideScalar(e){return this.multiplyScalar(1/e)}applyMatrix3(e){const t=this.x,n=this.y,i=e.elements;return this.x=i[0]*t+i[3]*n+i[6],this.y=i[1]*t+i[4]*n+i[7],this}min(e){return this.x=Math.min(this.x,e.x),this.y=Math.min(this.y,e.y),this}max(e){return this.x=Math.max(this.x,e.x),this.y=Math.max(this.y,e.y),this}clamp(e,t){return this.x=et(this.x,e.x,t.x),this.y=et(this.y,e.y,t.y),this}clampScalar(e,t){return this.x=et(this.x,e,t),this.y=et(this.y,e,t),this}clampLength(e,t){const n=this.length();return this.divideScalar(n||1).multiplyScalar(et(n,e,t))}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(e){return this.x*e.x+this.y*e.y}cross(e){return this.x*e.y-this.y*e.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(e){const t=Math.sqrt(this.lengthSq()*e.lengthSq());if(t===0)return Math.PI/2;const n=this.dot(e)/t;return Math.acos(et(n,-1,1))}distanceTo(e){return Math.sqrt(this.distanceToSquared(e))}distanceToSquared(e){const t=this.x-e.x,n=this.y-e.y;return t*t+n*n}manhattanDistanceTo(e){return Math.abs(this.x-e.x)+Math.abs(this.y-e.y)}setLength(e){return this.normalize().multiplyScalar(e)}lerp(e,t){return this.x+=(e.x-this.x)*t,this.y+=(e.y-this.y)*t,this}lerpVectors(e,t,n){return this.x=e.x+(t.x-e.x)*n,this.y=e.y+(t.y-e.y)*n,this}equals(e){return e.x===this.x&&e.y===this.y}fromArray(e,t=0){return this.x=e[t],this.y=e[t+1],this}toArray(e=[],t=0){return e[t]=this.x,e[t+1]=this.y,e}fromBufferAttribute(e,t){return this.x=e.getX(t),this.y=e.getY(t),this}rotateAround(e,t){const n=Math.cos(t),i=Math.sin(t),s=this.x-e.x,a=this.y-e.y;return this.x=s*n-a*i+e.x,this.y=s*i+a*n+e.y,this}random(){return this.x=Math.random(),this.y=Math.random(),this}*[Symbol.iterator](){yield this.x,yield this.y}}class tt{constructor(e,t,n,i,s,a,o,l,c){tt.prototype.isMatrix3=!0,this.elements=[1,0,0,0,1,0,0,0,1],e!==void 0&&this.set(e,t,n,i,s,a,o,l,c)}set(e,t,n,i,s,a,o,l,c){const h=this.elements;return h[0]=e,h[1]=i,h[2]=o,h[3]=t,h[4]=s,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(e){const t=this.elements,n=e.elements;return t[0]=n[0],t[1]=n[1],t[2]=n[2],t[3]=n[3],t[4]=n[4],t[5]=n[5],t[6]=n[6],t[7]=n[7],t[8]=n[8],this}extractBasis(e,t,n){return e.setFromMatrix3Column(this,0),t.setFromMatrix3Column(this,1),n.setFromMatrix3Column(this,2),this}setFromMatrix4(e){const t=e.elements;return this.set(t[0],t[4],t[8],t[1],t[5],t[9],t[2],t[6],t[10]),this}multiply(e){return this.multiplyMatrices(this,e)}premultiply(e){return this.multiplyMatrices(e,this)}multiplyMatrices(e,t){const n=e.elements,i=t.elements,s=this.elements,a=n[0],o=n[3],l=n[6],c=n[1],h=n[4],u=n[7],f=n[2],d=n[5],p=n[8],_=i[0],g=i[3],m=i[6],y=i[1],v=i[4],x=i[7],w=i[2],T=i[5],C=i[8];return s[0]=a*_+o*y+l*w,s[3]=a*g+o*v+l*T,s[6]=a*m+o*x+l*C,s[1]=c*_+h*y+u*w,s[4]=c*g+h*v+u*T,s[7]=c*m+h*x+u*C,s[2]=f*_+d*y+p*w,s[5]=f*g+d*v+p*T,s[8]=f*m+d*x+p*C,this}multiplyScalar(e){const t=this.elements;return t[0]*=e,t[3]*=e,t[6]*=e,t[1]*=e,t[4]*=e,t[7]*=e,t[2]*=e,t[5]*=e,t[8]*=e,this}determinant(){const e=this.elements,t=e[0],n=e[1],i=e[2],s=e[3],a=e[4],o=e[5],l=e[6],c=e[7],h=e[8];return t*a*h-t*o*c-n*s*h+n*o*l+i*s*c-i*a*l}invert(){const e=this.elements,t=e[0],n=e[1],i=e[2],s=e[3],a=e[4],o=e[5],l=e[6],c=e[7],h=e[8],u=h*a-o*c,f=o*l-h*s,d=c*s-a*l,p=t*u+n*f+i*d;if(p===0)return this.set(0,0,0,0,0,0,0,0,0);const _=1/p;return e[0]=u*_,e[1]=(i*c-h*n)*_,e[2]=(o*n-i*a)*_,e[3]=f*_,e[4]=(h*t-i*l)*_,e[5]=(i*s-o*t)*_,e[6]=d*_,e[7]=(n*l-c*t)*_,e[8]=(a*t-n*s)*_,this}transpose(){let e;const t=this.elements;return e=t[1],t[1]=t[3],t[3]=e,e=t[2],t[2]=t[6],t[6]=e,e=t[5],t[5]=t[7],t[7]=e,this}getNormalMatrix(e){return this.setFromMatrix4(e).invert().transpose()}transposeIntoArray(e){const t=this.elements;return e[0]=t[0],e[1]=t[3],e[2]=t[6],e[3]=t[1],e[4]=t[4],e[5]=t[7],e[6]=t[2],e[7]=t[5],e[8]=t[8],this}setUvTransform(e,t,n,i,s,a,o){const l=Math.cos(s),c=Math.sin(s);return this.set(n*l,n*c,-n*(l*a+c*o)+a+e,-i*c,i*l,-i*(-c*a+l*o)+o+t,0,0,1),this}scale(e,t){return this.premultiply(Oo.makeScale(e,t)),this}rotate(e){return this.premultiply(Oo.makeRotation(-e)),this}translate(e,t){return this.premultiply(Oo.makeTranslation(e,t)),this}makeTranslation(e,t){return e.isVector2?this.set(1,0,e.x,0,1,e.y,0,0,1):this.set(1,0,e,0,1,t,0,0,1),this}makeRotation(e){const t=Math.cos(e),n=Math.sin(e);return this.set(t,-n,0,n,t,0,0,0,1),this}makeScale(e,t){return this.set(e,0,0,0,t,0,0,0,1),this}equals(e){const t=this.elements,n=e.elements;for(let i=0;i<9;i++)if(t[i]!==n[i])return!1;return!0}fromArray(e,t=0){for(let n=0;n<9;n++)this.elements[n]=e[n+t];return this}toArray(e=[],t=0){const n=this.elements;return e[t]=n[0],e[t+1]=n[1],e[t+2]=n[2],e[t+3]=n[3],e[t+4]=n[4],e[t+5]=n[5],e[t+6]=n[6],e[t+7]=n[7],e[t+8]=n[8],e}clone(){return new this.constructor().fromArray(this.elements)}}const Oo=new tt;function Sd(r){for(let e=r.length-1;e>=0;--e)if(r[e]>=65535)return!0;return!1}const Pp={Int8Array,Uint8Array,Uint8ClampedArray,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array};function ks(r,e){return new Pp[r](e)}function Vr(r){return document.createElementNS("http://www.w3.org/1999/xhtml",r)}function Dp(){const r=Vr("canvas");return r.style.display="block",r}const Sh={};function Fs(r){r in Sh||(Sh[r]=!0,console.warn(r))}function Lp(r,e,t){return new Promise(function(n,i){function s(){switch(r.clientWaitSync(e,r.SYNC_FLUSH_COMMANDS_BIT,0)){case r.WAIT_FAILED:i();break;case r.TIMEOUT_EXPIRED:setTimeout(s,t);break;default:n()}}setTimeout(s,t)})}function Up(r){const e=r.elements;e[2]=.5*e[2]+.5*e[3],e[6]=.5*e[6]+.5*e[7],e[10]=.5*e[10]+.5*e[11],e[14]=.5*e[14]+.5*e[15]}function Np(r){const e=r.elements;e[11]===-1?(e[10]=-e[10]-1,e[14]=-e[14]):(e[10]=-e[10],e[14]=-e[14]+1)}const wh=new tt().set(.4123908,.3575843,.1804808,.212639,.7151687,.0721923,.0193308,.1191948,.9505322),Eh=new tt().set(3.2409699,-1.5373832,-.4986108,-.9692436,1.8759675,.0415551,.0556301,-.203977,1.0569715);function Fp(){const r={enabled:!0,workingColorSpace:Qs,spaces:{},convert:function(i,s,a){return this.enabled===!1||s===a||!s||!a||(this.spaces[s].transfer===vt&&(i.r=ri(i.r),i.g=ri(i.g),i.b=ri(i.b)),this.spaces[s].primaries!==this.spaces[a].primaries&&(i.applyMatrix3(this.spaces[s].toXYZ),i.applyMatrix3(this.spaces[a].fromXYZ)),this.spaces[a].transfer===vt&&(i.r=Zs(i.r),i.g=Zs(i.g),i.b=Zs(i.b))),i},fromWorkingColorSpace:function(i,s){return this.convert(i,this.workingColorSpace,s)},toWorkingColorSpace:function(i,s){return this.convert(i,s,this.workingColorSpace)},getPrimaries:function(i){return this.spaces[i].primaries},getTransfer:function(i){return i===wi?co:this.spaces[i].transfer},getLuminanceCoefficients:function(i,s=this.workingColorSpace){return i.fromArray(this.spaces[s].luminanceCoefficients)},define:function(i){Object.assign(this.spaces,i)},_getMatrix:function(i,s,a){return i.copy(this.spaces[s].toXYZ).multiply(this.spaces[a].fromXYZ)},_getDrawingBufferColorSpace:function(i){return this.spaces[i].outputColorSpaceConfig.drawingBufferColorSpace},_getUnpackColorSpace:function(i=this.workingColorSpace){return this.spaces[i].workingColorSpaceConfig.unpackColorSpace}},e=[.64,.33,.3,.6,.15,.06],t=[.2126,.7152,.0722],n=[.3127,.329];return r.define({[Qs]:{primaries:e,whitePoint:n,transfer:co,toXYZ:wh,fromXYZ:Eh,luminanceCoefficients:t,workingColorSpaceConfig:{unpackColorSpace:xn},outputColorSpaceConfig:{drawingBufferColorSpace:xn}},[xn]:{primaries:e,whitePoint:n,transfer:vt,toXYZ:wh,fromXYZ:Eh,luminanceCoefficients:t,outputColorSpaceConfig:{drawingBufferColorSpace:xn}}}),r}const ht=Fp();function ri(r){return r<.04045?r*.0773993808:Math.pow(r*.9478672986+.0521327014,2.4)}function Zs(r){return r<.0031308?r*12.92:1.055*Math.pow(r,.41666)-.055}let _s;class Op{static getDataURL(e){if(/^data:/i.test(e.src)||typeof HTMLCanvasElement>"u")return e.src;let t;if(e instanceof HTMLCanvasElement)t=e;else{_s===void 0&&(_s=Vr("canvas")),_s.width=e.width,_s.height=e.height;const n=_s.getContext("2d");e instanceof ImageData?n.putImageData(e,0,0):n.drawImage(e,0,0,e.width,e.height),t=_s}return t.width>2048||t.height>2048?(console.warn("THREE.ImageUtils.getDataURL: Image converted to jpg for performance reasons",e),t.toDataURL("image/jpeg",.6)):t.toDataURL("image/png")}static sRGBToLinear(e){if(typeof HTMLImageElement<"u"&&e instanceof HTMLImageElement||typeof HTMLCanvasElement<"u"&&e instanceof HTMLCanvasElement||typeof ImageBitmap<"u"&&e instanceof ImageBitmap){const t=Vr("canvas");t.width=e.width,t.height=e.height;const n=t.getContext("2d");n.drawImage(e,0,0,e.width,e.height);const i=n.getImageData(0,0,e.width,e.height),s=i.data;for(let a=0;a0&&(n.userData=this.userData),t||(e.textures[this.uuid]=n),n}dispose(){this.dispatchEvent({type:"dispose"})}transformUv(e){if(this.mapping!==Ic)return e;if(e.applyMatrix3(this.matrix),e.x<0||e.x>1)switch(this.wrapS){case ro:e.x=e.x-Math.floor(e.x);break;case Nn:e.x=e.x<0?0:1;break;case ao:Math.abs(Math.floor(e.x)%2)===1?e.x=Math.ceil(e.x)-e.x:e.x=e.x-Math.floor(e.x);break}if(e.y<0||e.y>1)switch(this.wrapT){case ro:e.y=e.y-Math.floor(e.y);break;case Nn:e.y=e.y<0?0:1;break;case ao:Math.abs(Math.floor(e.y)%2)===1?e.y=Math.ceil(e.y)-e.y:e.y=e.y-Math.floor(e.y);break}return this.flipY&&(e.y=1-e.y),e}set needsUpdate(e){e===!0&&(this.version++,this.source.needsUpdate=!0)}set needsPMREMUpdate(e){e===!0&&this.pmremVersion++}}Et.DEFAULT_IMAGE=null;Et.DEFAULT_MAPPING=Ic;Et.DEFAULT_ANISOTROPY=1;class pt{constructor(e=0,t=0,n=0,i=1){pt.prototype.isVector4=!0,this.x=e,this.y=t,this.z=n,this.w=i}get width(){return this.z}set width(e){this.z=e}get height(){return this.w}set height(e){this.w=e}set(e,t,n,i){return this.x=e,this.y=t,this.z=n,this.w=i,this}setScalar(e){return this.x=e,this.y=e,this.z=e,this.w=e,this}setX(e){return this.x=e,this}setY(e){return this.y=e,this}setZ(e){return this.z=e,this}setW(e){return this.w=e,this}setComponent(e,t){switch(e){case 0:this.x=t;break;case 1:this.y=t;break;case 2:this.z=t;break;case 3:this.w=t;break;default:throw new Error("index is out of range: "+e)}return this}getComponent(e){switch(e){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: "+e)}}clone(){return new this.constructor(this.x,this.y,this.z,this.w)}copy(e){return this.x=e.x,this.y=e.y,this.z=e.z,this.w=e.w!==void 0?e.w:1,this}add(e){return this.x+=e.x,this.y+=e.y,this.z+=e.z,this.w+=e.w,this}addScalar(e){return this.x+=e,this.y+=e,this.z+=e,this.w+=e,this}addVectors(e,t){return this.x=e.x+t.x,this.y=e.y+t.y,this.z=e.z+t.z,this.w=e.w+t.w,this}addScaledVector(e,t){return this.x+=e.x*t,this.y+=e.y*t,this.z+=e.z*t,this.w+=e.w*t,this}sub(e){return this.x-=e.x,this.y-=e.y,this.z-=e.z,this.w-=e.w,this}subScalar(e){return this.x-=e,this.y-=e,this.z-=e,this.w-=e,this}subVectors(e,t){return this.x=e.x-t.x,this.y=e.y-t.y,this.z=e.z-t.z,this.w=e.w-t.w,this}multiply(e){return this.x*=e.x,this.y*=e.y,this.z*=e.z,this.w*=e.w,this}multiplyScalar(e){return this.x*=e,this.y*=e,this.z*=e,this.w*=e,this}applyMatrix4(e){const t=this.x,n=this.y,i=this.z,s=this.w,a=e.elements;return this.x=a[0]*t+a[4]*n+a[8]*i+a[12]*s,this.y=a[1]*t+a[5]*n+a[9]*i+a[13]*s,this.z=a[2]*t+a[6]*n+a[10]*i+a[14]*s,this.w=a[3]*t+a[7]*n+a[11]*i+a[15]*s,this}divide(e){return this.x/=e.x,this.y/=e.y,this.z/=e.z,this.w/=e.w,this}divideScalar(e){return this.multiplyScalar(1/e)}setAxisAngleFromQuaternion(e){this.w=2*Math.acos(e.w);const t=Math.sqrt(1-e.w*e.w);return t<1e-4?(this.x=1,this.y=0,this.z=0):(this.x=e.x/t,this.y=e.y/t,this.z=e.z/t),this}setAxisAngleFromRotationMatrix(e){let t,n,i,s;const l=e.elements,c=l[0],h=l[4],u=l[8],f=l[1],d=l[5],p=l[9],_=l[2],g=l[6],m=l[10];if(Math.abs(h-f)<.01&&Math.abs(u-_)<.01&&Math.abs(p-g)<.01){if(Math.abs(h+f)<.1&&Math.abs(u+_)<.1&&Math.abs(p+g)<.1&&Math.abs(c+d+m-3)<.1)return this.set(1,0,0,0),this;t=Math.PI;const v=(c+1)/2,x=(d+1)/2,w=(m+1)/2,T=(h+f)/4,C=(u+_)/4,P=(p+g)/4;return v>x&&v>w?v<.01?(n=0,i=.707106781,s=.707106781):(n=Math.sqrt(v),i=T/n,s=C/n):x>w?x<.01?(n=.707106781,i=0,s=.707106781):(i=Math.sqrt(x),n=T/i,s=P/i):w<.01?(n=.707106781,i=.707106781,s=0):(s=Math.sqrt(w),n=C/s,i=P/s),this.set(n,i,s,t),this}let y=Math.sqrt((g-p)*(g-p)+(u-_)*(u-_)+(f-h)*(f-h));return Math.abs(y)<.001&&(y=1),this.x=(g-p)/y,this.y=(u-_)/y,this.z=(f-h)/y,this.w=Math.acos((c+d+m-1)/2),this}setFromMatrixPosition(e){const t=e.elements;return this.x=t[12],this.y=t[13],this.z=t[14],this.w=t[15],this}min(e){return this.x=Math.min(this.x,e.x),this.y=Math.min(this.y,e.y),this.z=Math.min(this.z,e.z),this.w=Math.min(this.w,e.w),this}max(e){return this.x=Math.max(this.x,e.x),this.y=Math.max(this.y,e.y),this.z=Math.max(this.z,e.z),this.w=Math.max(this.w,e.w),this}clamp(e,t){return this.x=et(this.x,e.x,t.x),this.y=et(this.y,e.y,t.y),this.z=et(this.z,e.z,t.z),this.w=et(this.w,e.w,t.w),this}clampScalar(e,t){return this.x=et(this.x,e,t),this.y=et(this.y,e,t),this.z=et(this.z,e,t),this.w=et(this.w,e,t),this}clampLength(e,t){const n=this.length();return this.divideScalar(n||1).multiplyScalar(et(n,e,t))}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(e){return this.x*e.x+this.y*e.y+this.z*e.z+this.w*e.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(e){return this.normalize().multiplyScalar(e)}lerp(e,t){return this.x+=(e.x-this.x)*t,this.y+=(e.y-this.y)*t,this.z+=(e.z-this.z)*t,this.w+=(e.w-this.w)*t,this}lerpVectors(e,t,n){return this.x=e.x+(t.x-e.x)*n,this.y=e.y+(t.y-e.y)*n,this.z=e.z+(t.z-e.z)*n,this.w=e.w+(t.w-e.w)*n,this}equals(e){return e.x===this.x&&e.y===this.y&&e.z===this.z&&e.w===this.w}fromArray(e,t=0){return this.x=e[t],this.y=e[t+1],this.z=e[t+2],this.w=e[t+3],this}toArray(e=[],t=0){return e[t]=this.x,e[t+1]=this.y,e[t+2]=this.z,e[t+3]=this.w,e}fromBufferAttribute(e,t){return this.x=e.getX(t),this.y=e.getY(t),this.z=e.getZ(t),this.w=e.getW(t),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 zc extends ci{constructor(e=1,t=1,n={}){super(),this.isRenderTarget=!0,this.width=e,this.height=t,this.depth=1,this.scissor=new pt(0,0,e,t),this.scissorTest=!1,this.viewport=new pt(0,0,e,t);const i={width:e,height:t,depth:1};n=Object.assign({generateMipmaps:!1,internalFormat:null,minFilter:Vt,depthBuffer:!0,stencilBuffer:!1,resolveDepthBuffer:!0,resolveStencilBuffer:!0,depthTexture:null,samples:0,count:1},n);const s=new Et(i,n.mapping,n.wrapS,n.wrapT,n.magFilter,n.minFilter,n.format,n.type,n.anisotropy,n.colorSpace);s.flipY=!1,s.generateMipmaps=n.generateMipmaps,s.internalFormat=n.internalFormat,this.textures=[];const a=n.count;for(let o=0;o=0?1:-1,v=1-m*m;if(v>Number.EPSILON){const w=Math.sqrt(v),T=Math.atan2(w,m*y);g=Math.sin(g*T)/w,o=Math.sin(o*T)/w}const x=o*y;if(l=l*g+f*x,c=c*g+d*x,h=h*g+p*x,u=u*g+_*x,g===1-o){const w=1/Math.sqrt(l*l+c*c+h*h+u*u);l*=w,c*=w,h*=w,u*=w}}e[t]=l,e[t+1]=c,e[t+2]=h,e[t+3]=u}static multiplyQuaternionsFlat(e,t,n,i,s,a){const o=n[i],l=n[i+1],c=n[i+2],h=n[i+3],u=s[a],f=s[a+1],d=s[a+2],p=s[a+3];return e[t]=o*p+h*u+l*d-c*f,e[t+1]=l*p+h*f+c*u-o*d,e[t+2]=c*p+h*d+o*f-l*u,e[t+3]=h*p-o*u-l*f-c*d,e}get x(){return this._x}set x(e){this._x=e,this._onChangeCallback()}get y(){return this._y}set y(e){this._y=e,this._onChangeCallback()}get z(){return this._z}set z(e){this._z=e,this._onChangeCallback()}get w(){return this._w}set w(e){this._w=e,this._onChangeCallback()}set(e,t,n,i){return this._x=e,this._y=t,this._z=n,this._w=i,this._onChangeCallback(),this}clone(){return new this.constructor(this._x,this._y,this._z,this._w)}copy(e){return this._x=e.x,this._y=e.y,this._z=e.z,this._w=e.w,this._onChangeCallback(),this}setFromEuler(e,t=!0){const n=e._x,i=e._y,s=e._z,a=e._order,o=Math.cos,l=Math.sin,c=o(n/2),h=o(i/2),u=o(s/2),f=l(n/2),d=l(i/2),p=l(s/2);switch(a){case"XYZ":this._x=f*h*u+c*d*p,this._y=c*d*u-f*h*p,this._z=c*h*p+f*d*u,this._w=c*h*u-f*d*p;break;case"YXZ":this._x=f*h*u+c*d*p,this._y=c*d*u-f*h*p,this._z=c*h*p-f*d*u,this._w=c*h*u+f*d*p;break;case"ZXY":this._x=f*h*u-c*d*p,this._y=c*d*u+f*h*p,this._z=c*h*p+f*d*u,this._w=c*h*u-f*d*p;break;case"ZYX":this._x=f*h*u-c*d*p,this._y=c*d*u+f*h*p,this._z=c*h*p-f*d*u,this._w=c*h*u+f*d*p;break;case"YZX":this._x=f*h*u+c*d*p,this._y=c*d*u+f*h*p,this._z=c*h*p-f*d*u,this._w=c*h*u-f*d*p;break;case"XZY":this._x=f*h*u-c*d*p,this._y=c*d*u-f*h*p,this._z=c*h*p+f*d*u,this._w=c*h*u+f*d*p;break;default:console.warn("THREE.Quaternion: .setFromEuler() encountered an unknown order: "+a)}return t===!0&&this._onChangeCallback(),this}setFromAxisAngle(e,t){const n=t/2,i=Math.sin(n);return this._x=e.x*i,this._y=e.y*i,this._z=e.z*i,this._w=Math.cos(n),this._onChangeCallback(),this}setFromRotationMatrix(e){const t=e.elements,n=t[0],i=t[4],s=t[8],a=t[1],o=t[5],l=t[9],c=t[2],h=t[6],u=t[10],f=n+o+u;if(f>0){const d=.5/Math.sqrt(f+1);this._w=.25/d,this._x=(h-l)*d,this._y=(s-c)*d,this._z=(a-i)*d}else if(n>o&&n>u){const d=2*Math.sqrt(1+n-o-u);this._w=(h-l)/d,this._x=.25*d,this._y=(i+a)/d,this._z=(s+c)/d}else if(o>u){const d=2*Math.sqrt(1+o-n-u);this._w=(s-c)/d,this._x=(i+a)/d,this._y=.25*d,this._z=(l+h)/d}else{const d=2*Math.sqrt(1+u-n-o);this._w=(a-i)/d,this._x=(s+c)/d,this._y=(l+h)/d,this._z=.25*d}return this._onChangeCallback(),this}setFromUnitVectors(e,t){let n=e.dot(t)+1;return nMath.abs(e.z)?(this._x=-e.y,this._y=e.x,this._z=0,this._w=n):(this._x=0,this._y=-e.z,this._z=e.y,this._w=n)):(this._x=e.y*t.z-e.z*t.y,this._y=e.z*t.x-e.x*t.z,this._z=e.x*t.y-e.y*t.x,this._w=n),this.normalize()}angleTo(e){return 2*Math.acos(Math.abs(et(this.dot(e),-1,1)))}rotateTowards(e,t){const n=this.angleTo(e);if(n===0)return this;const i=Math.min(1,t/n);return this.slerp(e,i),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(e){return this._x*e._x+this._y*e._y+this._z*e._z+this._w*e._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 e=this.length();return e===0?(this._x=0,this._y=0,this._z=0,this._w=1):(e=1/e,this._x=this._x*e,this._y=this._y*e,this._z=this._z*e,this._w=this._w*e),this._onChangeCallback(),this}multiply(e){return this.multiplyQuaternions(this,e)}premultiply(e){return this.multiplyQuaternions(e,this)}multiplyQuaternions(e,t){const n=e._x,i=e._y,s=e._z,a=e._w,o=t._x,l=t._y,c=t._z,h=t._w;return this._x=n*h+a*o+i*c-s*l,this._y=i*h+a*l+s*o-n*c,this._z=s*h+a*c+n*l-i*o,this._w=a*h-n*o-i*l-s*c,this._onChangeCallback(),this}slerp(e,t){if(t===0)return this;if(t===1)return this.copy(e);const n=this._x,i=this._y,s=this._z,a=this._w;let o=a*e._w+n*e._x+i*e._y+s*e._z;if(o<0?(this._w=-e._w,this._x=-e._x,this._y=-e._y,this._z=-e._z,o=-o):this.copy(e),o>=1)return this._w=a,this._x=n,this._y=i,this._z=s,this;const l=1-o*o;if(l<=Number.EPSILON){const d=1-t;return this._w=d*a+t*this._w,this._x=d*n+t*this._x,this._y=d*i+t*this._y,this._z=d*s+t*this._z,this.normalize(),this}const c=Math.sqrt(l),h=Math.atan2(c,o),u=Math.sin((1-t)*h)/c,f=Math.sin(t*h)/c;return this._w=a*u+this._w*f,this._x=n*u+this._x*f,this._y=i*u+this._y*f,this._z=s*u+this._z*f,this._onChangeCallback(),this}slerpQuaternions(e,t,n){return this.copy(e).slerp(t,n)}random(){const e=2*Math.PI*Math.random(),t=2*Math.PI*Math.random(),n=Math.random(),i=Math.sqrt(1-n),s=Math.sqrt(n);return this.set(i*Math.sin(e),i*Math.cos(e),s*Math.sin(t),s*Math.cos(t))}equals(e){return e._x===this._x&&e._y===this._y&&e._z===this._z&&e._w===this._w}fromArray(e,t=0){return this._x=e[t],this._y=e[t+1],this._z=e[t+2],this._w=e[t+3],this._onChangeCallback(),this}toArray(e=[],t=0){return e[t]=this._x,e[t+1]=this._y,e[t+2]=this._z,e[t+3]=this._w,e}fromBufferAttribute(e,t){return this._x=e.getX(t),this._y=e.getY(t),this._z=e.getZ(t),this._w=e.getW(t),this._onChangeCallback(),this}toJSON(){return this.toArray()}_onChange(e){return this._onChangeCallback=e,this}_onChangeCallback(){}*[Symbol.iterator](){yield this._x,yield this._y,yield this._z,yield this._w}}class A{constructor(e=0,t=0,n=0){A.prototype.isVector3=!0,this.x=e,this.y=t,this.z=n}set(e,t,n){return n===void 0&&(n=this.z),this.x=e,this.y=t,this.z=n,this}setScalar(e){return this.x=e,this.y=e,this.z=e,this}setX(e){return this.x=e,this}setY(e){return this.y=e,this}setZ(e){return this.z=e,this}setComponent(e,t){switch(e){case 0:this.x=t;break;case 1:this.y=t;break;case 2:this.z=t;break;default:throw new Error("index is out of range: "+e)}return this}getComponent(e){switch(e){case 0:return this.x;case 1:return this.y;case 2:return this.z;default:throw new Error("index is out of range: "+e)}}clone(){return new this.constructor(this.x,this.y,this.z)}copy(e){return this.x=e.x,this.y=e.y,this.z=e.z,this}add(e){return this.x+=e.x,this.y+=e.y,this.z+=e.z,this}addScalar(e){return this.x+=e,this.y+=e,this.z+=e,this}addVectors(e,t){return this.x=e.x+t.x,this.y=e.y+t.y,this.z=e.z+t.z,this}addScaledVector(e,t){return this.x+=e.x*t,this.y+=e.y*t,this.z+=e.z*t,this}sub(e){return this.x-=e.x,this.y-=e.y,this.z-=e.z,this}subScalar(e){return this.x-=e,this.y-=e,this.z-=e,this}subVectors(e,t){return this.x=e.x-t.x,this.y=e.y-t.y,this.z=e.z-t.z,this}multiply(e){return this.x*=e.x,this.y*=e.y,this.z*=e.z,this}multiplyScalar(e){return this.x*=e,this.y*=e,this.z*=e,this}multiplyVectors(e,t){return this.x=e.x*t.x,this.y=e.y*t.y,this.z=e.z*t.z,this}applyEuler(e){return this.applyQuaternion(Th.setFromEuler(e))}applyAxisAngle(e,t){return this.applyQuaternion(Th.setFromAxisAngle(e,t))}applyMatrix3(e){const t=this.x,n=this.y,i=this.z,s=e.elements;return this.x=s[0]*t+s[3]*n+s[6]*i,this.y=s[1]*t+s[4]*n+s[7]*i,this.z=s[2]*t+s[5]*n+s[8]*i,this}applyNormalMatrix(e){return this.applyMatrix3(e).normalize()}applyMatrix4(e){const t=this.x,n=this.y,i=this.z,s=e.elements,a=1/(s[3]*t+s[7]*n+s[11]*i+s[15]);return this.x=(s[0]*t+s[4]*n+s[8]*i+s[12])*a,this.y=(s[1]*t+s[5]*n+s[9]*i+s[13])*a,this.z=(s[2]*t+s[6]*n+s[10]*i+s[14])*a,this}applyQuaternion(e){const t=this.x,n=this.y,i=this.z,s=e.x,a=e.y,o=e.z,l=e.w,c=2*(a*i-o*n),h=2*(o*t-s*i),u=2*(s*n-a*t);return this.x=t+l*c+a*u-o*h,this.y=n+l*h+o*c-s*u,this.z=i+l*u+s*h-a*c,this}project(e){return this.applyMatrix4(e.matrixWorldInverse).applyMatrix4(e.projectionMatrix)}unproject(e){return this.applyMatrix4(e.projectionMatrixInverse).applyMatrix4(e.matrixWorld)}transformDirection(e){const t=this.x,n=this.y,i=this.z,s=e.elements;return this.x=s[0]*t+s[4]*n+s[8]*i,this.y=s[1]*t+s[5]*n+s[9]*i,this.z=s[2]*t+s[6]*n+s[10]*i,this.normalize()}divide(e){return this.x/=e.x,this.y/=e.y,this.z/=e.z,this}divideScalar(e){return this.multiplyScalar(1/e)}min(e){return this.x=Math.min(this.x,e.x),this.y=Math.min(this.y,e.y),this.z=Math.min(this.z,e.z),this}max(e){return this.x=Math.max(this.x,e.x),this.y=Math.max(this.y,e.y),this.z=Math.max(this.z,e.z),this}clamp(e,t){return this.x=et(this.x,e.x,t.x),this.y=et(this.y,e.y,t.y),this.z=et(this.z,e.z,t.z),this}clampScalar(e,t){return this.x=et(this.x,e,t),this.y=et(this.y,e,t),this.z=et(this.z,e,t),this}clampLength(e,t){const n=this.length();return this.divideScalar(n||1).multiplyScalar(et(n,e,t))}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(e){return this.x*e.x+this.y*e.y+this.z*e.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(e){return this.normalize().multiplyScalar(e)}lerp(e,t){return this.x+=(e.x-this.x)*t,this.y+=(e.y-this.y)*t,this.z+=(e.z-this.z)*t,this}lerpVectors(e,t,n){return this.x=e.x+(t.x-e.x)*n,this.y=e.y+(t.y-e.y)*n,this.z=e.z+(t.z-e.z)*n,this}cross(e){return this.crossVectors(this,e)}crossVectors(e,t){const n=e.x,i=e.y,s=e.z,a=t.x,o=t.y,l=t.z;return this.x=i*l-s*o,this.y=s*a-n*l,this.z=n*o-i*a,this}projectOnVector(e){const t=e.lengthSq();if(t===0)return this.set(0,0,0);const n=e.dot(this)/t;return this.copy(e).multiplyScalar(n)}projectOnPlane(e){return zo.copy(this).projectOnVector(e),this.sub(zo)}reflect(e){return this.sub(zo.copy(e).multiplyScalar(2*this.dot(e)))}angleTo(e){const t=Math.sqrt(this.lengthSq()*e.lengthSq());if(t===0)return Math.PI/2;const n=this.dot(e)/t;return Math.acos(et(n,-1,1))}distanceTo(e){return Math.sqrt(this.distanceToSquared(e))}distanceToSquared(e){const t=this.x-e.x,n=this.y-e.y,i=this.z-e.z;return t*t+n*n+i*i}manhattanDistanceTo(e){return Math.abs(this.x-e.x)+Math.abs(this.y-e.y)+Math.abs(this.z-e.z)}setFromSpherical(e){return this.setFromSphericalCoords(e.radius,e.phi,e.theta)}setFromSphericalCoords(e,t,n){const i=Math.sin(t)*e;return this.x=i*Math.sin(n),this.y=Math.cos(t)*e,this.z=i*Math.cos(n),this}setFromCylindrical(e){return this.setFromCylindricalCoords(e.radius,e.theta,e.y)}setFromCylindricalCoords(e,t,n){return this.x=e*Math.sin(t),this.y=n,this.z=e*Math.cos(t),this}setFromMatrixPosition(e){const t=e.elements;return this.x=t[12],this.y=t[13],this.z=t[14],this}setFromMatrixScale(e){const t=this.setFromMatrixColumn(e,0).length(),n=this.setFromMatrixColumn(e,1).length(),i=this.setFromMatrixColumn(e,2).length();return this.x=t,this.y=n,this.z=i,this}setFromMatrixColumn(e,t){return this.fromArray(e.elements,t*4)}setFromMatrix3Column(e,t){return this.fromArray(e.elements,t*3)}setFromEuler(e){return this.x=e._x,this.y=e._y,this.z=e._z,this}setFromColor(e){return this.x=e.r,this.y=e.g,this.z=e.b,this}equals(e){return e.x===this.x&&e.y===this.y&&e.z===this.z}fromArray(e,t=0){return this.x=e[t],this.y=e[t+1],this.z=e[t+2],this}toArray(e=[],t=0){return e[t]=this.x,e[t+1]=this.y,e[t+2]=this.z,e}fromBufferAttribute(e,t){return this.x=e.getX(t),this.y=e.getY(t),this.z=e.getZ(t),this}random(){return this.x=Math.random(),this.y=Math.random(),this.z=Math.random(),this}randomDirection(){const e=Math.random()*Math.PI*2,t=Math.random()*2-1,n=Math.sqrt(1-t*t);return this.x=n*Math.cos(e),this.y=t,this.z=n*Math.sin(e),this}*[Symbol.iterator](){yield this.x,yield this.y,yield this.z}}const zo=new A,Th=new rn;class pn{constructor(e=new A(1/0,1/0,1/0),t=new A(-1/0,-1/0,-1/0)){this.isBox3=!0,this.min=e,this.max=t}set(e,t){return this.min.copy(e),this.max.copy(t),this}setFromArray(e){this.makeEmpty();for(let t=0,n=e.length;t=this.min.x&&e.x<=this.max.x&&e.y>=this.min.y&&e.y<=this.max.y&&e.z>=this.min.z&&e.z<=this.max.z}containsBox(e){return this.min.x<=e.min.x&&e.max.x<=this.max.x&&this.min.y<=e.min.y&&e.max.y<=this.max.y&&this.min.z<=e.min.z&&e.max.z<=this.max.z}getParameter(e,t){return t.set((e.x-this.min.x)/(this.max.x-this.min.x),(e.y-this.min.y)/(this.max.y-this.min.y),(e.z-this.min.z)/(this.max.z-this.min.z))}intersectsBox(e){return e.max.x>=this.min.x&&e.min.x<=this.max.x&&e.max.y>=this.min.y&&e.min.y<=this.max.y&&e.max.z>=this.min.z&&e.min.z<=this.max.z}intersectsSphere(e){return this.clampPoint(e.center,Dn),Dn.distanceToSquared(e.center)<=e.radius*e.radius}intersectsPlane(e){let t,n;return e.normal.x>0?(t=e.normal.x*this.min.x,n=e.normal.x*this.max.x):(t=e.normal.x*this.max.x,n=e.normal.x*this.min.x),e.normal.y>0?(t+=e.normal.y*this.min.y,n+=e.normal.y*this.max.y):(t+=e.normal.y*this.max.y,n+=e.normal.y*this.min.y),e.normal.z>0?(t+=e.normal.z*this.min.z,n+=e.normal.z*this.max.z):(t+=e.normal.z*this.max.z,n+=e.normal.z*this.min.z),t<=-e.constant&&n>=-e.constant}intersectsTriangle(e){if(this.isEmpty())return!1;this.getCenter(mr),$r.subVectors(this.max,mr),vs.subVectors(e.a,mr),xs.subVectors(e.b,mr),ys.subVectors(e.c,mr),mi.subVectors(xs,vs),gi.subVectors(ys,xs),Fi.subVectors(vs,ys);let t=[0,-mi.z,mi.y,0,-gi.z,gi.y,0,-Fi.z,Fi.y,mi.z,0,-mi.x,gi.z,0,-gi.x,Fi.z,0,-Fi.x,-mi.y,mi.x,0,-gi.y,gi.x,0,-Fi.y,Fi.x,0];return!ko(t,vs,xs,ys,$r)||(t=[1,0,0,0,1,0,0,0,1],!ko(t,vs,xs,ys,$r))?!1:(jr.crossVectors(mi,gi),t=[jr.x,jr.y,jr.z],ko(t,vs,xs,ys,$r))}clampPoint(e,t){return t.copy(e).clamp(this.min,this.max)}distanceToPoint(e){return this.clampPoint(e,Dn).distanceTo(e)}getBoundingSphere(e){return this.isEmpty()?e.makeEmpty():(this.getCenter(e.center),e.radius=this.getSize(Dn).length()*.5),e}intersect(e){return this.min.max(e.min),this.max.min(e.max),this.isEmpty()&&this.makeEmpty(),this}union(e){return this.min.min(e.min),this.max.max(e.max),this}applyMatrix4(e){return this.isEmpty()?this:(qn[0].set(this.min.x,this.min.y,this.min.z).applyMatrix4(e),qn[1].set(this.min.x,this.min.y,this.max.z).applyMatrix4(e),qn[2].set(this.min.x,this.max.y,this.min.z).applyMatrix4(e),qn[3].set(this.min.x,this.max.y,this.max.z).applyMatrix4(e),qn[4].set(this.max.x,this.min.y,this.min.z).applyMatrix4(e),qn[5].set(this.max.x,this.min.y,this.max.z).applyMatrix4(e),qn[6].set(this.max.x,this.max.y,this.min.z).applyMatrix4(e),qn[7].set(this.max.x,this.max.y,this.max.z).applyMatrix4(e),this.setFromPoints(qn),this)}translate(e){return this.min.add(e),this.max.add(e),this}equals(e){return e.min.equals(this.min)&&e.max.equals(this.max)}}const qn=[new A,new A,new A,new A,new A,new A,new A,new A],Dn=new A,Kr=new pn,vs=new A,xs=new A,ys=new A,mi=new A,gi=new A,Fi=new A,mr=new A,$r=new A,jr=new A,Oi=new A;function ko(r,e,t,n,i){for(let s=0,a=r.length-3;s<=a;s+=3){Oi.fromArray(r,s);const o=i.x*Math.abs(Oi.x)+i.y*Math.abs(Oi.y)+i.z*Math.abs(Oi.z),l=e.dot(Oi),c=t.dot(Oi),h=n.dot(Oi);if(Math.max(-Math.max(l,c,h),Math.min(l,c,h))>o)return!1}return!0}const kp=new pn,gr=new A,Vo=new A;class an{constructor(e=new A,t=-1){this.isSphere=!0,this.center=e,this.radius=t}set(e,t){return this.center.copy(e),this.radius=t,this}setFromPoints(e,t){const n=this.center;t!==void 0?n.copy(t):kp.setFromPoints(e).getCenter(n);let i=0;for(let s=0,a=e.length;sthis.radius*this.radius&&(t.sub(this.center).normalize(),t.multiplyScalar(this.radius).add(this.center)),t}getBoundingBox(e){return this.isEmpty()?(e.makeEmpty(),e):(e.set(this.center,this.center),e.expandByScalar(this.radius),e)}applyMatrix4(e){return this.center.applyMatrix4(e),this.radius=this.radius*e.getMaxScaleOnAxis(),this}translate(e){return this.center.add(e),this}expandByPoint(e){if(this.isEmpty())return this.center.copy(e),this.radius=0,this;gr.subVectors(e,this.center);const t=gr.lengthSq();if(t>this.radius*this.radius){const n=Math.sqrt(t),i=(n-this.radius)*.5;this.center.addScaledVector(gr,i/n),this.radius+=i}return this}union(e){return e.isEmpty()?this:this.isEmpty()?(this.copy(e),this):(this.center.equals(e.center)===!0?this.radius=Math.max(this.radius,e.radius):(Vo.subVectors(e.center,this.center).setLength(e.radius),this.expandByPoint(gr.copy(e.center).add(Vo)),this.expandByPoint(gr.copy(e.center).sub(Vo))),this)}equals(e){return e.center.equals(this.center)&&e.radius===this.radius}clone(){return new this.constructor().copy(this)}}const Yn=new A,Ho=new A,Qr=new A,_i=new A,Go=new A,ea=new A,Wo=new A;class ar{constructor(e=new A,t=new A(0,0,-1)){this.origin=e,this.direction=t}set(e,t){return this.origin.copy(e),this.direction.copy(t),this}copy(e){return this.origin.copy(e.origin),this.direction.copy(e.direction),this}at(e,t){return t.copy(this.origin).addScaledVector(this.direction,e)}lookAt(e){return this.direction.copy(e).sub(this.origin).normalize(),this}recast(e){return this.origin.copy(this.at(e,Yn)),this}closestPointToPoint(e,t){t.subVectors(e,this.origin);const n=t.dot(this.direction);return n<0?t.copy(this.origin):t.copy(this.origin).addScaledVector(this.direction,n)}distanceToPoint(e){return Math.sqrt(this.distanceSqToPoint(e))}distanceSqToPoint(e){const t=Yn.subVectors(e,this.origin).dot(this.direction);return t<0?this.origin.distanceToSquared(e):(Yn.copy(this.origin).addScaledVector(this.direction,t),Yn.distanceToSquared(e))}distanceSqToSegment(e,t,n,i){Ho.copy(e).add(t).multiplyScalar(.5),Qr.copy(t).sub(e).normalize(),_i.copy(this.origin).sub(Ho);const s=e.distanceTo(t)*.5,a=-this.direction.dot(Qr),o=_i.dot(this.direction),l=-_i.dot(Qr),c=_i.lengthSq(),h=Math.abs(1-a*a);let u,f,d,p;if(h>0)if(u=a*l-o,f=a*o-l,p=s*h,u>=0)if(f>=-p)if(f<=p){const _=1/h;u*=_,f*=_,d=u*(u+a*f+2*o)+f*(a*u+f+2*l)+c}else f=s,u=Math.max(0,-(a*f+o)),d=-u*u+f*(f+2*l)+c;else f=-s,u=Math.max(0,-(a*f+o)),d=-u*u+f*(f+2*l)+c;else f<=-p?(u=Math.max(0,-(-a*s+o)),f=u>0?-s:Math.min(Math.max(-s,-l),s),d=-u*u+f*(f+2*l)+c):f<=p?(u=0,f=Math.min(Math.max(-s,-l),s),d=f*(f+2*l)+c):(u=Math.max(0,-(a*s+o)),f=u>0?s:Math.min(Math.max(-s,-l),s),d=-u*u+f*(f+2*l)+c);else f=a>0?-s:s,u=Math.max(0,-(a*f+o)),d=-u*u+f*(f+2*l)+c;return n&&n.copy(this.origin).addScaledVector(this.direction,u),i&&i.copy(Ho).addScaledVector(Qr,f),d}intersectSphere(e,t){Yn.subVectors(e.center,this.origin);const n=Yn.dot(this.direction),i=Yn.dot(Yn)-n*n,s=e.radius*e.radius;if(i>s)return null;const a=Math.sqrt(s-i),o=n-a,l=n+a;return l<0?null:o<0?this.at(l,t):this.at(o,t)}intersectsSphere(e){return this.distanceSqToPoint(e.center)<=e.radius*e.radius}distanceToPlane(e){const t=e.normal.dot(this.direction);if(t===0)return e.distanceToPoint(this.origin)===0?0:null;const n=-(this.origin.dot(e.normal)+e.constant)/t;return n>=0?n:null}intersectPlane(e,t){const n=this.distanceToPlane(e);return n===null?null:this.at(n,t)}intersectsPlane(e){const t=e.distanceToPoint(this.origin);return t===0||e.normal.dot(this.direction)*t<0}intersectBox(e,t){let n,i,s,a,o,l;const c=1/this.direction.x,h=1/this.direction.y,u=1/this.direction.z,f=this.origin;return c>=0?(n=(e.min.x-f.x)*c,i=(e.max.x-f.x)*c):(n=(e.max.x-f.x)*c,i=(e.min.x-f.x)*c),h>=0?(s=(e.min.y-f.y)*h,a=(e.max.y-f.y)*h):(s=(e.max.y-f.y)*h,a=(e.min.y-f.y)*h),n>a||s>i||((s>n||isNaN(n))&&(n=s),(a=0?(o=(e.min.z-f.z)*u,l=(e.max.z-f.z)*u):(o=(e.max.z-f.z)*u,l=(e.min.z-f.z)*u),n>l||o>i)||((o>n||n!==n)&&(n=o),(l=0?n:i,t)}intersectsBox(e){return this.intersectBox(e,Yn)!==null}intersectTriangle(e,t,n,i,s){Go.subVectors(t,e),ea.subVectors(n,e),Wo.crossVectors(Go,ea);let a=this.direction.dot(Wo),o;if(a>0){if(i)return null;o=1}else if(a<0)o=-1,a=-a;else return null;_i.subVectors(this.origin,e);const l=o*this.direction.dot(ea.crossVectors(_i,ea));if(l<0)return null;const c=o*this.direction.dot(Go.cross(_i));if(c<0||l+c>a)return null;const h=-o*_i.dot(Wo);return h<0?null:this.at(h/a,s)}applyMatrix4(e){return this.origin.applyMatrix4(e),this.direction.transformDirection(e),this}equals(e){return e.origin.equals(this.origin)&&e.direction.equals(this.direction)}clone(){return new this.constructor().copy(this)}}class Ze{constructor(e,t,n,i,s,a,o,l,c,h,u,f,d,p,_,g){Ze.prototype.isMatrix4=!0,this.elements=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],e!==void 0&&this.set(e,t,n,i,s,a,o,l,c,h,u,f,d,p,_,g)}set(e,t,n,i,s,a,o,l,c,h,u,f,d,p,_,g){const m=this.elements;return m[0]=e,m[4]=t,m[8]=n,m[12]=i,m[1]=s,m[5]=a,m[9]=o,m[13]=l,m[2]=c,m[6]=h,m[10]=u,m[14]=f,m[3]=d,m[7]=p,m[11]=_,m[15]=g,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 Ze().fromArray(this.elements)}copy(e){const t=this.elements,n=e.elements;return t[0]=n[0],t[1]=n[1],t[2]=n[2],t[3]=n[3],t[4]=n[4],t[5]=n[5],t[6]=n[6],t[7]=n[7],t[8]=n[8],t[9]=n[9],t[10]=n[10],t[11]=n[11],t[12]=n[12],t[13]=n[13],t[14]=n[14],t[15]=n[15],this}copyPosition(e){const t=this.elements,n=e.elements;return t[12]=n[12],t[13]=n[13],t[14]=n[14],this}setFromMatrix3(e){const t=e.elements;return this.set(t[0],t[3],t[6],0,t[1],t[4],t[7],0,t[2],t[5],t[8],0,0,0,0,1),this}extractBasis(e,t,n){return e.setFromMatrixColumn(this,0),t.setFromMatrixColumn(this,1),n.setFromMatrixColumn(this,2),this}makeBasis(e,t,n){return this.set(e.x,t.x,n.x,0,e.y,t.y,n.y,0,e.z,t.z,n.z,0,0,0,0,1),this}extractRotation(e){const t=this.elements,n=e.elements,i=1/Ms.setFromMatrixColumn(e,0).length(),s=1/Ms.setFromMatrixColumn(e,1).length(),a=1/Ms.setFromMatrixColumn(e,2).length();return t[0]=n[0]*i,t[1]=n[1]*i,t[2]=n[2]*i,t[3]=0,t[4]=n[4]*s,t[5]=n[5]*s,t[6]=n[6]*s,t[7]=0,t[8]=n[8]*a,t[9]=n[9]*a,t[10]=n[10]*a,t[11]=0,t[12]=0,t[13]=0,t[14]=0,t[15]=1,this}makeRotationFromEuler(e){const t=this.elements,n=e.x,i=e.y,s=e.z,a=Math.cos(n),o=Math.sin(n),l=Math.cos(i),c=Math.sin(i),h=Math.cos(s),u=Math.sin(s);if(e.order==="XYZ"){const f=a*h,d=a*u,p=o*h,_=o*u;t[0]=l*h,t[4]=-l*u,t[8]=c,t[1]=d+p*c,t[5]=f-_*c,t[9]=-o*l,t[2]=_-f*c,t[6]=p+d*c,t[10]=a*l}else if(e.order==="YXZ"){const f=l*h,d=l*u,p=c*h,_=c*u;t[0]=f+_*o,t[4]=p*o-d,t[8]=a*c,t[1]=a*u,t[5]=a*h,t[9]=-o,t[2]=d*o-p,t[6]=_+f*o,t[10]=a*l}else if(e.order==="ZXY"){const f=l*h,d=l*u,p=c*h,_=c*u;t[0]=f-_*o,t[4]=-a*u,t[8]=p+d*o,t[1]=d+p*o,t[5]=a*h,t[9]=_-f*o,t[2]=-a*c,t[6]=o,t[10]=a*l}else if(e.order==="ZYX"){const f=a*h,d=a*u,p=o*h,_=o*u;t[0]=l*h,t[4]=p*c-d,t[8]=f*c+_,t[1]=l*u,t[5]=_*c+f,t[9]=d*c-p,t[2]=-c,t[6]=o*l,t[10]=a*l}else if(e.order==="YZX"){const f=a*l,d=a*c,p=o*l,_=o*c;t[0]=l*h,t[4]=_-f*u,t[8]=p*u+d,t[1]=u,t[5]=a*h,t[9]=-o*h,t[2]=-c*h,t[6]=d*u+p,t[10]=f-_*u}else if(e.order==="XZY"){const f=a*l,d=a*c,p=o*l,_=o*c;t[0]=l*h,t[4]=-u,t[8]=c*h,t[1]=f*u+_,t[5]=a*h,t[9]=d*u-p,t[2]=p*u-d,t[6]=o*h,t[10]=_*u+f}return t[3]=0,t[7]=0,t[11]=0,t[12]=0,t[13]=0,t[14]=0,t[15]=1,this}makeRotationFromQuaternion(e){return this.compose(Vp,e,Hp)}lookAt(e,t,n){const i=this.elements;return _n.subVectors(e,t),_n.lengthSq()===0&&(_n.z=1),_n.normalize(),vi.crossVectors(n,_n),vi.lengthSq()===0&&(Math.abs(n.z)===1?_n.x+=1e-4:_n.z+=1e-4,_n.normalize(),vi.crossVectors(n,_n)),vi.normalize(),ta.crossVectors(_n,vi),i[0]=vi.x,i[4]=ta.x,i[8]=_n.x,i[1]=vi.y,i[5]=ta.y,i[9]=_n.y,i[2]=vi.z,i[6]=ta.z,i[10]=_n.z,this}multiply(e){return this.multiplyMatrices(this,e)}premultiply(e){return this.multiplyMatrices(e,this)}multiplyMatrices(e,t){const n=e.elements,i=t.elements,s=this.elements,a=n[0],o=n[4],l=n[8],c=n[12],h=n[1],u=n[5],f=n[9],d=n[13],p=n[2],_=n[6],g=n[10],m=n[14],y=n[3],v=n[7],x=n[11],w=n[15],T=i[0],C=i[4],P=i[8],S=i[12],M=i[1],D=i[5],G=i[9],F=i[13],V=i[2],J=i[6],q=i[10],se=i[14],Z=i[3],_e=i[7],be=i[11],Re=i[15];return s[0]=a*T+o*M+l*V+c*Z,s[4]=a*C+o*D+l*J+c*_e,s[8]=a*P+o*G+l*q+c*be,s[12]=a*S+o*F+l*se+c*Re,s[1]=h*T+u*M+f*V+d*Z,s[5]=h*C+u*D+f*J+d*_e,s[9]=h*P+u*G+f*q+d*be,s[13]=h*S+u*F+f*se+d*Re,s[2]=p*T+_*M+g*V+m*Z,s[6]=p*C+_*D+g*J+m*_e,s[10]=p*P+_*G+g*q+m*be,s[14]=p*S+_*F+g*se+m*Re,s[3]=y*T+v*M+x*V+w*Z,s[7]=y*C+v*D+x*J+w*_e,s[11]=y*P+v*G+x*q+w*be,s[15]=y*S+v*F+x*se+w*Re,this}multiplyScalar(e){const t=this.elements;return t[0]*=e,t[4]*=e,t[8]*=e,t[12]*=e,t[1]*=e,t[5]*=e,t[9]*=e,t[13]*=e,t[2]*=e,t[6]*=e,t[10]*=e,t[14]*=e,t[3]*=e,t[7]*=e,t[11]*=e,t[15]*=e,this}determinant(){const e=this.elements,t=e[0],n=e[4],i=e[8],s=e[12],a=e[1],o=e[5],l=e[9],c=e[13],h=e[2],u=e[6],f=e[10],d=e[14],p=e[3],_=e[7],g=e[11],m=e[15];return p*(+s*l*u-i*c*u-s*o*f+n*c*f+i*o*d-n*l*d)+_*(+t*l*d-t*c*f+s*a*f-i*a*d+i*c*h-s*l*h)+g*(+t*c*u-t*o*d-s*a*u+n*a*d+s*o*h-n*c*h)+m*(-i*o*h-t*l*u+t*o*f+i*a*u-n*a*f+n*l*h)}transpose(){const e=this.elements;let t;return t=e[1],e[1]=e[4],e[4]=t,t=e[2],e[2]=e[8],e[8]=t,t=e[6],e[6]=e[9],e[9]=t,t=e[3],e[3]=e[12],e[12]=t,t=e[7],e[7]=e[13],e[13]=t,t=e[11],e[11]=e[14],e[14]=t,this}setPosition(e,t,n){const i=this.elements;return e.isVector3?(i[12]=e.x,i[13]=e.y,i[14]=e.z):(i[12]=e,i[13]=t,i[14]=n),this}invert(){const e=this.elements,t=e[0],n=e[1],i=e[2],s=e[3],a=e[4],o=e[5],l=e[6],c=e[7],h=e[8],u=e[9],f=e[10],d=e[11],p=e[12],_=e[13],g=e[14],m=e[15],y=u*g*c-_*f*c+_*l*d-o*g*d-u*l*m+o*f*m,v=p*f*c-h*g*c-p*l*d+a*g*d+h*l*m-a*f*m,x=h*_*c-p*u*c+p*o*d-a*_*d-h*o*m+a*u*m,w=p*u*l-h*_*l-p*o*f+a*_*f+h*o*g-a*u*g,T=t*y+n*v+i*x+s*w;if(T===0)return this.set(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);const C=1/T;return e[0]=y*C,e[1]=(_*f*s-u*g*s-_*i*d+n*g*d+u*i*m-n*f*m)*C,e[2]=(o*g*s-_*l*s+_*i*c-n*g*c-o*i*m+n*l*m)*C,e[3]=(u*l*s-o*f*s-u*i*c+n*f*c+o*i*d-n*l*d)*C,e[4]=v*C,e[5]=(h*g*s-p*f*s+p*i*d-t*g*d-h*i*m+t*f*m)*C,e[6]=(p*l*s-a*g*s-p*i*c+t*g*c+a*i*m-t*l*m)*C,e[7]=(a*f*s-h*l*s+h*i*c-t*f*c-a*i*d+t*l*d)*C,e[8]=x*C,e[9]=(p*u*s-h*_*s-p*n*d+t*_*d+h*n*m-t*u*m)*C,e[10]=(a*_*s-p*o*s+p*n*c-t*_*c-a*n*m+t*o*m)*C,e[11]=(h*o*s-a*u*s-h*n*c+t*u*c+a*n*d-t*o*d)*C,e[12]=w*C,e[13]=(h*_*i-p*u*i+p*n*f-t*_*f-h*n*g+t*u*g)*C,e[14]=(p*o*i-a*_*i-p*n*l+t*_*l+a*n*g-t*o*g)*C,e[15]=(a*u*i-h*o*i+h*n*l-t*u*l-a*n*f+t*o*f)*C,this}scale(e){const t=this.elements,n=e.x,i=e.y,s=e.z;return t[0]*=n,t[4]*=i,t[8]*=s,t[1]*=n,t[5]*=i,t[9]*=s,t[2]*=n,t[6]*=i,t[10]*=s,t[3]*=n,t[7]*=i,t[11]*=s,this}getMaxScaleOnAxis(){const e=this.elements,t=e[0]*e[0]+e[1]*e[1]+e[2]*e[2],n=e[4]*e[4]+e[5]*e[5]+e[6]*e[6],i=e[8]*e[8]+e[9]*e[9]+e[10]*e[10];return Math.sqrt(Math.max(t,n,i))}makeTranslation(e,t,n){return e.isVector3?this.set(1,0,0,e.x,0,1,0,e.y,0,0,1,e.z,0,0,0,1):this.set(1,0,0,e,0,1,0,t,0,0,1,n,0,0,0,1),this}makeRotationX(e){const t=Math.cos(e),n=Math.sin(e);return this.set(1,0,0,0,0,t,-n,0,0,n,t,0,0,0,0,1),this}makeRotationY(e){const t=Math.cos(e),n=Math.sin(e);return this.set(t,0,n,0,0,1,0,0,-n,0,t,0,0,0,0,1),this}makeRotationZ(e){const t=Math.cos(e),n=Math.sin(e);return this.set(t,-n,0,0,n,t,0,0,0,0,1,0,0,0,0,1),this}makeRotationAxis(e,t){const n=Math.cos(t),i=Math.sin(t),s=1-n,a=e.x,o=e.y,l=e.z,c=s*a,h=s*o;return this.set(c*a+n,c*o-i*l,c*l+i*o,0,c*o+i*l,h*o+n,h*l-i*a,0,c*l-i*o,h*l+i*a,s*l*l+n,0,0,0,0,1),this}makeScale(e,t,n){return this.set(e,0,0,0,0,t,0,0,0,0,n,0,0,0,0,1),this}makeShear(e,t,n,i,s,a){return this.set(1,n,s,0,e,1,a,0,t,i,1,0,0,0,0,1),this}compose(e,t,n){const i=this.elements,s=t._x,a=t._y,o=t._z,l=t._w,c=s+s,h=a+a,u=o+o,f=s*c,d=s*h,p=s*u,_=a*h,g=a*u,m=o*u,y=l*c,v=l*h,x=l*u,w=n.x,T=n.y,C=n.z;return i[0]=(1-(_+m))*w,i[1]=(d+x)*w,i[2]=(p-v)*w,i[3]=0,i[4]=(d-x)*T,i[5]=(1-(f+m))*T,i[6]=(g+y)*T,i[7]=0,i[8]=(p+v)*C,i[9]=(g-y)*C,i[10]=(1-(f+_))*C,i[11]=0,i[12]=e.x,i[13]=e.y,i[14]=e.z,i[15]=1,this}decompose(e,t,n){const i=this.elements;let s=Ms.set(i[0],i[1],i[2]).length();const a=Ms.set(i[4],i[5],i[6]).length(),o=Ms.set(i[8],i[9],i[10]).length();this.determinant()<0&&(s=-s),e.x=i[12],e.y=i[13],e.z=i[14],Ln.copy(this);const c=1/s,h=1/a,u=1/o;return Ln.elements[0]*=c,Ln.elements[1]*=c,Ln.elements[2]*=c,Ln.elements[4]*=h,Ln.elements[5]*=h,Ln.elements[6]*=h,Ln.elements[8]*=u,Ln.elements[9]*=u,Ln.elements[10]*=u,t.setFromRotationMatrix(Ln),n.x=s,n.y=a,n.z=o,this}makePerspective(e,t,n,i,s,a,o=zn){const l=this.elements,c=2*s/(t-e),h=2*s/(n-i),u=(t+e)/(t-e),f=(n+i)/(n-i);let d,p;if(o===zn)d=-(a+s)/(a-s),p=-2*a*s/(a-s);else if(o===uo)d=-a/(a-s),p=-a*s/(a-s);else throw new Error("THREE.Matrix4.makePerspective(): Invalid coordinate system: "+o);return l[0]=c,l[4]=0,l[8]=u,l[12]=0,l[1]=0,l[5]=h,l[9]=f,l[13]=0,l[2]=0,l[6]=0,l[10]=d,l[14]=p,l[3]=0,l[7]=0,l[11]=-1,l[15]=0,this}makeOrthographic(e,t,n,i,s,a,o=zn){const l=this.elements,c=1/(t-e),h=1/(n-i),u=1/(a-s),f=(t+e)*c,d=(n+i)*h;let p,_;if(o===zn)p=(a+s)*u,_=-2*u;else if(o===uo)p=s*u,_=-1*u;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]=-d,l[2]=0,l[6]=0,l[10]=_,l[14]=-p,l[3]=0,l[7]=0,l[11]=0,l[15]=1,this}equals(e){const t=this.elements,n=e.elements;for(let i=0;i<16;i++)if(t[i]!==n[i])return!1;return!0}fromArray(e,t=0){for(let n=0;n<16;n++)this.elements[n]=e[n+t];return this}toArray(e=[],t=0){const n=this.elements;return e[t]=n[0],e[t+1]=n[1],e[t+2]=n[2],e[t+3]=n[3],e[t+4]=n[4],e[t+5]=n[5],e[t+6]=n[6],e[t+7]=n[7],e[t+8]=n[8],e[t+9]=n[9],e[t+10]=n[10],e[t+11]=n[11],e[t+12]=n[12],e[t+13]=n[13],e[t+14]=n[14],e[t+15]=n[15],e}}const Ms=new A,Ln=new Ze,Vp=new A(0,0,0),Hp=new A(1,1,1),vi=new A,ta=new A,_n=new A,Ah=new Ze,Ch=new rn;class In{constructor(e=0,t=0,n=0,i=In.DEFAULT_ORDER){this.isEuler=!0,this._x=e,this._y=t,this._z=n,this._order=i}get x(){return this._x}set x(e){this._x=e,this._onChangeCallback()}get y(){return this._y}set y(e){this._y=e,this._onChangeCallback()}get z(){return this._z}set z(e){this._z=e,this._onChangeCallback()}get order(){return this._order}set order(e){this._order=e,this._onChangeCallback()}set(e,t,n,i=this._order){return this._x=e,this._y=t,this._z=n,this._order=i,this._onChangeCallback(),this}clone(){return new this.constructor(this._x,this._y,this._z,this._order)}copy(e){return this._x=e._x,this._y=e._y,this._z=e._z,this._order=e._order,this._onChangeCallback(),this}setFromRotationMatrix(e,t=this._order,n=!0){const i=e.elements,s=i[0],a=i[4],o=i[8],l=i[1],c=i[5],h=i[9],u=i[2],f=i[6],d=i[10];switch(t){case"XYZ":this._y=Math.asin(et(o,-1,1)),Math.abs(o)<.9999999?(this._x=Math.atan2(-h,d),this._z=Math.atan2(-a,s)):(this._x=Math.atan2(f,c),this._z=0);break;case"YXZ":this._x=Math.asin(-et(h,-1,1)),Math.abs(h)<.9999999?(this._y=Math.atan2(o,d),this._z=Math.atan2(l,c)):(this._y=Math.atan2(-u,s),this._z=0);break;case"ZXY":this._x=Math.asin(et(f,-1,1)),Math.abs(f)<.9999999?(this._y=Math.atan2(-u,d),this._z=Math.atan2(-a,c)):(this._y=0,this._z=Math.atan2(l,s));break;case"ZYX":this._y=Math.asin(-et(u,-1,1)),Math.abs(u)<.9999999?(this._x=Math.atan2(f,d),this._z=Math.atan2(l,s)):(this._x=0,this._z=Math.atan2(-a,c));break;case"YZX":this._z=Math.asin(et(l,-1,1)),Math.abs(l)<.9999999?(this._x=Math.atan2(-h,c),this._y=Math.atan2(-u,s)):(this._x=0,this._y=Math.atan2(o,d));break;case"XZY":this._z=Math.asin(-et(a,-1,1)),Math.abs(a)<.9999999?(this._x=Math.atan2(f,c),this._y=Math.atan2(o,s)):(this._x=Math.atan2(-h,d),this._y=0);break;default:console.warn("THREE.Euler: .setFromRotationMatrix() encountered an unknown order: "+t)}return this._order=t,n===!0&&this._onChangeCallback(),this}setFromQuaternion(e,t,n){return Ah.makeRotationFromQuaternion(e),this.setFromRotationMatrix(Ah,t,n)}setFromVector3(e,t=this._order){return this.set(e.x,e.y,e.z,t)}reorder(e){return Ch.setFromEuler(this),this.setFromQuaternion(Ch,e)}equals(e){return e._x===this._x&&e._y===this._y&&e._z===this._z&&e._order===this._order}fromArray(e){return this._x=e[0],this._y=e[1],this._z=e[2],e[3]!==void 0&&(this._order=e[3]),this._onChangeCallback(),this}toArray(e=[],t=0){return e[t]=this._x,e[t+1]=this._y,e[t+2]=this._z,e[t+3]=this._order,e}_onChange(e){return this._onChangeCallback=e,this}_onChangeCallback(){}*[Symbol.iterator](){yield this._x,yield this._y,yield this._z,yield this._order}}In.DEFAULT_ORDER="XYZ";class Vc{constructor(){this.mask=1}set(e){this.mask=(1<>>0}enable(e){this.mask|=1<1){for(let t=0;t1){for(let n=0;n0&&(i.userData=this.userData),i.layers=this.layers.mask,i.matrix=this.matrix.toArray(),i.up=this.up.toArray(),this.matrixAutoUpdate===!1&&(i.matrixAutoUpdate=!1),this.isInstancedMesh&&(i.type="InstancedMesh",i.count=this.count,i.instanceMatrix=this.instanceMatrix.toJSON(),this.instanceColor!==null&&(i.instanceColor=this.instanceColor.toJSON())),this.isBatchedMesh&&(i.type="BatchedMesh",i.perObjectFrustumCulled=this.perObjectFrustumCulled,i.sortObjects=this.sortObjects,i.drawRanges=this._drawRanges,i.reservedRanges=this._reservedRanges,i.visibility=this._visibility,i.active=this._active,i.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()})),i.maxInstanceCount=this._maxInstanceCount,i.maxVertexCount=this._maxVertexCount,i.maxIndexCount=this._maxIndexCount,i.geometryInitialized=this._geometryInitialized,i.geometryCount=this._geometryCount,i.matricesTexture=this._matricesTexture.toJSON(e),this._colorsTexture!==null&&(i.colorsTexture=this._colorsTexture.toJSON(e)),this.boundingSphere!==null&&(i.boundingSphere={center:i.boundingSphere.center.toArray(),radius:i.boundingSphere.radius}),this.boundingBox!==null&&(i.boundingBox={min:i.boundingBox.min.toArray(),max:i.boundingBox.max.toArray()}));function s(o,l){return o[l.uuid]===void 0&&(o[l.uuid]=l.toJSON(e)),l.uuid}if(this.isScene)this.background&&(this.background.isColor?i.background=this.background.toJSON():this.background.isTexture&&(i.background=this.background.toJSON(e).uuid)),this.environment&&this.environment.isTexture&&this.environment.isRenderTargetTexture!==!0&&(i.environment=this.environment.toJSON(e).uuid);else if(this.isMesh||this.isLine||this.isPoints){i.geometry=s(e.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){i.children=[];for(let o=0;o0){i.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),u.length>0&&(n.shapes=u),f.length>0&&(n.skeletons=f),d.length>0&&(n.animations=d),p.length>0&&(n.nodes=p)}return n.object=i,n;function a(o){const l=[];for(const c in o){const h=o[c];delete h.metadata,l.push(h)}return l}}clone(e){return new this.constructor().copy(this,e)}copy(e,t=!0){if(this.name=e.name,this.up.copy(e.up),this.position.copy(e.position),this.rotation.order=e.rotation.order,this.quaternion.copy(e.quaternion),this.scale.copy(e.scale),this.matrix.copy(e.matrix),this.matrixWorld.copy(e.matrixWorld),this.matrixAutoUpdate=e.matrixAutoUpdate,this.matrixWorldAutoUpdate=e.matrixWorldAutoUpdate,this.matrixWorldNeedsUpdate=e.matrixWorldNeedsUpdate,this.layers.mask=e.layers.mask,this.visible=e.visible,this.castShadow=e.castShadow,this.receiveShadow=e.receiveShadow,this.frustumCulled=e.frustumCulled,this.renderOrder=e.renderOrder,this.animations=e.animations.slice(),this.userData=JSON.parse(JSON.stringify(e.userData)),t===!0)for(let n=0;n0?i.multiplyScalar(1/Math.sqrt(s)):i.set(0,0,0)}static getBarycoord(e,t,n,i,s){Un.subVectors(i,t),Jn.subVectors(n,t),qo.subVectors(e,t);const a=Un.dot(Un),o=Un.dot(Jn),l=Un.dot(qo),c=Jn.dot(Jn),h=Jn.dot(qo),u=a*c-o*o;if(u===0)return s.set(0,0,0),null;const f=1/u,d=(c*l-o*h)*f,p=(a*h-o*l)*f;return s.set(1-d-p,p,d)}static containsPoint(e,t,n,i){return this.getBarycoord(e,t,n,i,Kn)===null?!1:Kn.x>=0&&Kn.y>=0&&Kn.x+Kn.y<=1}static getInterpolation(e,t,n,i,s,a,o,l){return this.getBarycoord(e,t,n,i,Kn)===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(s,Kn.x),l.addScaledVector(a,Kn.y),l.addScaledVector(o,Kn.z),l)}static getInterpolatedAttribute(e,t,n,i,s,a){return Ko.setScalar(0),$o.setScalar(0),jo.setScalar(0),Ko.fromBufferAttribute(e,t),$o.fromBufferAttribute(e,n),jo.fromBufferAttribute(e,i),a.setScalar(0),a.addScaledVector(Ko,s.x),a.addScaledVector($o,s.y),a.addScaledVector(jo,s.z),a}static isFrontFacing(e,t,n,i){return Un.subVectors(n,t),Jn.subVectors(e,t),Un.cross(Jn).dot(i)<0}set(e,t,n){return this.a.copy(e),this.b.copy(t),this.c.copy(n),this}setFromPointsAndIndices(e,t,n,i){return this.a.copy(e[t]),this.b.copy(e[n]),this.c.copy(e[i]),this}setFromAttributeAndIndices(e,t,n,i){return this.a.fromBufferAttribute(e,t),this.b.fromBufferAttribute(e,n),this.c.fromBufferAttribute(e,i),this}clone(){return new this.constructor().copy(this)}copy(e){return this.a.copy(e.a),this.b.copy(e.b),this.c.copy(e.c),this}getArea(){return Un.subVectors(this.c,this.b),Jn.subVectors(this.a,this.b),Un.cross(Jn).length()*.5}getMidpoint(e){return e.addVectors(this.a,this.b).add(this.c).multiplyScalar(1/3)}getNormal(e){return yn.getNormal(this.a,this.b,this.c,e)}getPlane(e){return e.setFromCoplanarPoints(this.a,this.b,this.c)}getBarycoord(e,t){return yn.getBarycoord(e,this.a,this.b,this.c,t)}getInterpolation(e,t,n,i,s){return yn.getInterpolation(e,this.a,this.b,this.c,t,n,i,s)}containsPoint(e){return yn.containsPoint(e,this.a,this.b,this.c)}isFrontFacing(e){return yn.isFrontFacing(this.a,this.b,this.c,e)}intersectsBox(e){return e.intersectsTriangle(this)}closestPointToPoint(e,t){const n=this.a,i=this.b,s=this.c;let a,o;ws.subVectors(i,n),Es.subVectors(s,n),Yo.subVectors(e,n);const l=ws.dot(Yo),c=Es.dot(Yo);if(l<=0&&c<=0)return t.copy(n);Zo.subVectors(e,i);const h=ws.dot(Zo),u=Es.dot(Zo);if(h>=0&&u<=h)return t.copy(i);const f=l*u-h*c;if(f<=0&&l>=0&&h<=0)return a=l/(l-h),t.copy(n).addScaledVector(ws,a);Jo.subVectors(e,s);const d=ws.dot(Jo),p=Es.dot(Jo);if(p>=0&&d<=p)return t.copy(s);const _=d*c-l*p;if(_<=0&&c>=0&&p<=0)return o=c/(c-p),t.copy(n).addScaledVector(Es,o);const g=h*p-d*u;if(g<=0&&u-h>=0&&d-p>=0)return Uh.subVectors(s,i),o=(u-h)/(u-h+(d-p)),t.copy(i).addScaledVector(Uh,o);const m=1/(g+_+f);return a=_*m,o=f*m,t.copy(n).addScaledVector(ws,a).addScaledVector(Es,o)}equals(e){return e.a.equals(this.a)&&e.b.equals(this.b)&&e.c.equals(this.c)}}const wd={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},xi={h:0,s:0,l:0},ia={h:0,s:0,l:0};function Qo(r,e,t){return t<0&&(t+=1),t>1&&(t-=1),t<1/6?r+(e-r)*6*t:t<1/2?e:t<2/3?r+(e-r)*6*(2/3-t):r}class ne{constructor(e,t,n){return this.isColor=!0,this.r=1,this.g=1,this.b=1,this.set(e,t,n)}set(e,t,n){if(t===void 0&&n===void 0){const i=e;i&&i.isColor?this.copy(i):typeof i=="number"?this.setHex(i):typeof i=="string"&&this.setStyle(i)}else this.setRGB(e,t,n);return this}setScalar(e){return this.r=e,this.g=e,this.b=e,this}setHex(e,t=xn){return e=Math.floor(e),this.r=(e>>16&255)/255,this.g=(e>>8&255)/255,this.b=(e&255)/255,ht.toWorkingColorSpace(this,t),this}setRGB(e,t,n,i=ht.workingColorSpace){return this.r=e,this.g=t,this.b=n,ht.toWorkingColorSpace(this,i),this}setHSL(e,t,n,i=ht.workingColorSpace){if(e=Bc(e,1),t=et(t,0,1),n=et(n,0,1),t===0)this.r=this.g=this.b=n;else{const s=n<=.5?n*(1+t):n+t-n*t,a=2*n-s;this.r=Qo(a,s,e+1/3),this.g=Qo(a,s,e),this.b=Qo(a,s,e-1/3)}return ht.toWorkingColorSpace(this,i),this}setStyle(e,t=xn){function n(s){s!==void 0&&parseFloat(s)<1&&console.warn("THREE.Color: Alpha component of "+e+" will be ignored.")}let i;if(i=/^(\w+)\(([^\)]*)\)/.exec(e)){let s;const a=i[1],o=i[2];switch(a){case"rgb":case"rgba":if(s=/^\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(o))return n(s[4]),this.setRGB(Math.min(255,parseInt(s[1],10))/255,Math.min(255,parseInt(s[2],10))/255,Math.min(255,parseInt(s[3],10))/255,t);if(s=/^\s*(\d+)\%\s*,\s*(\d+)\%\s*,\s*(\d+)\%\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(o))return n(s[4]),this.setRGB(Math.min(100,parseInt(s[1],10))/100,Math.min(100,parseInt(s[2],10))/100,Math.min(100,parseInt(s[3],10))/100,t);break;case"hsl":case"hsla":if(s=/^\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\%\s*,\s*(\d*\.?\d+)\%\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(o))return n(s[4]),this.setHSL(parseFloat(s[1])/360,parseFloat(s[2])/100,parseFloat(s[3])/100,t);break;default:console.warn("THREE.Color: Unknown color model "+e)}}else if(i=/^\#([A-Fa-f\d]+)$/.exec(e)){const s=i[1],a=s.length;if(a===3)return this.setRGB(parseInt(s.charAt(0),16)/15,parseInt(s.charAt(1),16)/15,parseInt(s.charAt(2),16)/15,t);if(a===6)return this.setHex(parseInt(s,16),t);console.warn("THREE.Color: Invalid hex color "+e)}else if(e&&e.length>0)return this.setColorName(e,t);return this}setColorName(e,t=xn){const n=wd[e.toLowerCase()];return n!==void 0?this.setHex(n,t):console.warn("THREE.Color: Unknown color "+e),this}clone(){return new this.constructor(this.r,this.g,this.b)}copy(e){return this.r=e.r,this.g=e.g,this.b=e.b,this}copySRGBToLinear(e){return this.r=ri(e.r),this.g=ri(e.g),this.b=ri(e.b),this}copyLinearToSRGB(e){return this.r=Zs(e.r),this.g=Zs(e.g),this.b=Zs(e.b),this}convertSRGBToLinear(){return this.copySRGBToLinear(this),this}convertLinearToSRGB(){return this.copyLinearToSRGB(this),this}getHex(e=xn){return ht.fromWorkingColorSpace(Kt.copy(this),e),Math.round(et(Kt.r*255,0,255))*65536+Math.round(et(Kt.g*255,0,255))*256+Math.round(et(Kt.b*255,0,255))}getHexString(e=xn){return("000000"+this.getHex(e).toString(16)).slice(-6)}getHSL(e,t=ht.workingColorSpace){ht.fromWorkingColorSpace(Kt.copy(this),t);const n=Kt.r,i=Kt.g,s=Kt.b,a=Math.max(n,i,s),o=Math.min(n,i,s);let l,c;const h=(o+a)/2;if(o===a)l=0,c=0;else{const u=a-o;switch(c=h<=.5?u/(a+o):u/(2-a-o),a){case n:l=(i-s)/u+(i0!=e>0&&this.version++,this._alphaTest=e}onBeforeRender(){}onBeforeCompile(){}customProgramCacheKey(){return this.onBeforeCompile.toString()}setValues(e){if(e!==void 0)for(const t in e){const n=e[t];if(n===void 0){console.warn(`THREE.Material: parameter '${t}' has value of undefined.`);continue}const i=this[t];if(i===void 0){console.warn(`THREE.Material: '${t}' is not a property of THREE.${this.type}.`);continue}i&&i.isColor?i.set(n):i&&i.isVector3&&n&&n.isVector3?i.copy(n):this[t]=n}}toJSON(e){const t=e===void 0||typeof e=="string";t&&(e={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(e).uuid),this.clearcoatRoughnessMap&&this.clearcoatRoughnessMap.isTexture&&(n.clearcoatRoughnessMap=this.clearcoatRoughnessMap.toJSON(e).uuid),this.clearcoatNormalMap&&this.clearcoatNormalMap.isTexture&&(n.clearcoatNormalMap=this.clearcoatNormalMap.toJSON(e).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(e).uuid),this.iridescenceThicknessMap&&this.iridescenceThicknessMap.isTexture&&(n.iridescenceThicknessMap=this.iridescenceThicknessMap.toJSON(e).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(e).uuid),this.map&&this.map.isTexture&&(n.map=this.map.toJSON(e).uuid),this.matcap&&this.matcap.isTexture&&(n.matcap=this.matcap.toJSON(e).uuid),this.alphaMap&&this.alphaMap.isTexture&&(n.alphaMap=this.alphaMap.toJSON(e).uuid),this.lightMap&&this.lightMap.isTexture&&(n.lightMap=this.lightMap.toJSON(e).uuid,n.lightMapIntensity=this.lightMapIntensity),this.aoMap&&this.aoMap.isTexture&&(n.aoMap=this.aoMap.toJSON(e).uuid,n.aoMapIntensity=this.aoMapIntensity),this.bumpMap&&this.bumpMap.isTexture&&(n.bumpMap=this.bumpMap.toJSON(e).uuid,n.bumpScale=this.bumpScale),this.normalMap&&this.normalMap.isTexture&&(n.normalMap=this.normalMap.toJSON(e).uuid,n.normalMapType=this.normalMapType,n.normalScale=this.normalScale.toArray()),this.displacementMap&&this.displacementMap.isTexture&&(n.displacementMap=this.displacementMap.toJSON(e).uuid,n.displacementScale=this.displacementScale,n.displacementBias=this.displacementBias),this.roughnessMap&&this.roughnessMap.isTexture&&(n.roughnessMap=this.roughnessMap.toJSON(e).uuid),this.metalnessMap&&this.metalnessMap.isTexture&&(n.metalnessMap=this.metalnessMap.toJSON(e).uuid),this.emissiveMap&&this.emissiveMap.isTexture&&(n.emissiveMap=this.emissiveMap.toJSON(e).uuid),this.specularMap&&this.specularMap.isTexture&&(n.specularMap=this.specularMap.toJSON(e).uuid),this.specularIntensityMap&&this.specularIntensityMap.isTexture&&(n.specularIntensityMap=this.specularIntensityMap.toJSON(e).uuid),this.specularColorMap&&this.specularColorMap.isTexture&&(n.specularColorMap=this.specularColorMap.toJSON(e).uuid),this.envMap&&this.envMap.isTexture&&(n.envMap=this.envMap.toJSON(e).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(e).uuid),this.transmission!==void 0&&(n.transmission=this.transmission),this.transmissionMap&&this.transmissionMap.isTexture&&(n.transmissionMap=this.transmissionMap.toJSON(e).uuid),this.thickness!==void 0&&(n.thickness=this.thickness),this.thicknessMap&&this.thicknessMap.isTexture&&(n.thicknessMap=this.thicknessMap.toJSON(e).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!==qs&&(n.blending=this.blending),this.side!==Ci&&(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!==Pl&&(n.blendSrc=this.blendSrc),this.blendDst!==Dl&&(n.blendDst=this.blendDst),this.blendEquation!==Qi&&(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!==Ks&&(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!==yh&&(n.stencilFunc=this.stencilFunc),this.stencilRef!==0&&(n.stencilRef=this.stencilRef),this.stencilFuncMask!==255&&(n.stencilFuncMask=this.stencilFuncMask),this.stencilFail!==gs&&(n.stencilFail=this.stencilFail),this.stencilZFail!==gs&&(n.stencilZFail=this.stencilZFail),this.stencilZPass!==gs&&(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 i(s){const a=[];for(const o in s){const l=s[o];delete l.metadata,a.push(l)}return a}if(t){const s=i(e.textures),a=i(e.images);s.length>0&&(n.textures=s),a.length>0&&(n.images=a)}return n}clone(){return new this.constructor().copy(this)}copy(e){this.name=e.name,this.blending=e.blending,this.side=e.side,this.vertexColors=e.vertexColors,this.opacity=e.opacity,this.transparent=e.transparent,this.blendSrc=e.blendSrc,this.blendDst=e.blendDst,this.blendEquation=e.blendEquation,this.blendSrcAlpha=e.blendSrcAlpha,this.blendDstAlpha=e.blendDstAlpha,this.blendEquationAlpha=e.blendEquationAlpha,this.blendColor.copy(e.blendColor),this.blendAlpha=e.blendAlpha,this.depthFunc=e.depthFunc,this.depthTest=e.depthTest,this.depthWrite=e.depthWrite,this.stencilWriteMask=e.stencilWriteMask,this.stencilFunc=e.stencilFunc,this.stencilRef=e.stencilRef,this.stencilFuncMask=e.stencilFuncMask,this.stencilFail=e.stencilFail,this.stencilZFail=e.stencilZFail,this.stencilZPass=e.stencilZPass,this.stencilWrite=e.stencilWrite;const t=e.clippingPlanes;let n=null;if(t!==null){const i=t.length;n=new Array(i);for(let s=0;s!==i;++s)n[s]=t[s].clone()}return this.clippingPlanes=n,this.clipIntersection=e.clipIntersection,this.clipShadows=e.clipShadows,this.shadowSide=e.shadowSide,this.colorWrite=e.colorWrite,this.precision=e.precision,this.polygonOffset=e.polygonOffset,this.polygonOffsetFactor=e.polygonOffsetFactor,this.polygonOffsetUnits=e.polygonOffsetUnits,this.dithering=e.dithering,this.alphaTest=e.alphaTest,this.alphaHash=e.alphaHash,this.alphaToCoverage=e.alphaToCoverage,this.premultipliedAlpha=e.premultipliedAlpha,this.forceSinglePass=e.forceSinglePass,this.visible=e.visible,this.toneMapped=e.toneMapped,this.userData=JSON.parse(JSON.stringify(e.userData)),this}dispose(){this.dispatchEvent({type:"dispose"})}set needsUpdate(e){e===!0&&this.version++}onBuild(){console.warn("Material: onBuild() has been removed.")}}class kn extends on{constructor(e){super(),this.isMeshBasicMaterial=!0,this.type="MeshBasicMaterial",this.color=new ne(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 In,this.combine=bo,this.reflectivity=1,this.refractionRatio=.98,this.wireframe=!1,this.wireframeLinewidth=1,this.wireframeLinecap="round",this.wireframeLinejoin="round",this.fog=!0,this.setValues(e)}copy(e){return super.copy(e),this.color.copy(e.color),this.map=e.map,this.lightMap=e.lightMap,this.lightMapIntensity=e.lightMapIntensity,this.aoMap=e.aoMap,this.aoMapIntensity=e.aoMapIntensity,this.specularMap=e.specularMap,this.alphaMap=e.alphaMap,this.envMap=e.envMap,this.envMapRotation.copy(e.envMapRotation),this.combine=e.combine,this.reflectivity=e.reflectivity,this.refractionRatio=e.refractionRatio,this.wireframe=e.wireframe,this.wireframeLinewidth=e.wireframeLinewidth,this.wireframeLinecap=e.wireframeLinecap,this.wireframeLinejoin=e.wireframeLinejoin,this.fog=e.fog,this}}const ti=Zp();function Zp(){const r=new ArrayBuffer(4),e=new Float32Array(r),t=new Uint32Array(r),n=new Uint32Array(512),i=new Uint32Array(512);for(let l=0;l<256;++l){const c=l-127;c<-27?(n[l]=0,n[l|256]=32768,i[l]=24,i[l|256]=24):c<-14?(n[l]=1024>>-c-14,n[l|256]=1024>>-c-14|32768,i[l]=-c-1,i[l|256]=-c-1):c<=15?(n[l]=c+15<<10,n[l|256]=c+15<<10|32768,i[l]=13,i[l|256]=13):c<128?(n[l]=31744,n[l|256]=64512,i[l]=24,i[l|256]=24):(n[l]=31744,n[l|256]=64512,i[l]=13,i[l|256]=13)}const s=new Uint32Array(2048),a=new Uint32Array(64),o=new Uint32Array(64);for(let l=1;l<1024;++l){let c=l<<13,h=0;for(;(c&8388608)===0;)c<<=1,h-=8388608;c&=-8388609,h+=947912704,s[l]=c|h}for(let l=1024;l<2048;++l)s[l]=939524096+(l-1024<<13);for(let l=1;l<31;++l)a[l]=l<<23;a[31]=1199570944,a[32]=2147483648;for(let l=33;l<63;++l)a[l]=2147483648+(l-32<<23);a[63]=3347054592;for(let l=1;l<64;++l)l!==32&&(o[l]=1024);return{floatView:e,uint32View:t,baseTable:n,shiftTable:i,mantissaTable:s,exponentTable:a,offsetTable:o}}function un(r){Math.abs(r)>65504&&console.warn("THREE.DataUtils.toHalfFloat(): Value out of range."),r=et(r,-65504,65504),ti.floatView[0]=r;const e=ti.uint32View[0],t=e>>23&511;return ti.baseTable[t]+((e&8388607)>>ti.shiftTable[t])}function Rr(r){const e=r>>10;return ti.uint32View[0]=ti.mantissaTable[ti.offsetTable[e]+(r&1023)]+ti.exponentTable[e],ti.floatView[0]}const VS={toHalfFloat:un,fromHalfFloat:Rr},Lt=new A,sa=new j;class rt{constructor(e,t,n=!1){if(Array.isArray(e))throw new TypeError("THREE.BufferAttribute: array should be a Typed Array.");this.isBufferAttribute=!0,this.name="",this.array=e,this.itemSize=t,this.count=e!==void 0?e.length/t:0,this.normalized=n,this.usage=ho,this.updateRanges=[],this.gpuType=Mn,this.version=0}onUploadCallback(){}set needsUpdate(e){e===!0&&this.version++}setUsage(e){return this.usage=e,this}addUpdateRange(e,t){this.updateRanges.push({start:e,count:t})}clearUpdateRanges(){this.updateRanges.length=0}copy(e){return this.name=e.name,this.array=new e.array.constructor(e.array),this.itemSize=e.itemSize,this.count=e.count,this.normalized=e.normalized,this.usage=e.usage,this.gpuType=e.gpuType,this}copyAt(e,t,n){e*=this.itemSize,n*=t.itemSize;for(let i=0,s=this.itemSize;it.count&&console.warn("THREE.BufferGeometry: Buffer size too small for points data. Use .dispose() and create a new geometry."),t.needsUpdate=!0}return this}computeBoundingBox(){this.boundingBox===null&&(this.boundingBox=new pn);const e=this.attributes.position,t=this.morphAttributes.position;if(e&&e.isGLBufferAttribute){console.error("THREE.BufferGeometry.computeBoundingBox(): GLBufferAttribute requires a manual bounding box.",this),this.boundingBox.set(new A(-1/0,-1/0,-1/0),new A(1/0,1/0,1/0));return}if(e!==void 0){if(this.boundingBox.setFromBufferAttribute(e),t)for(let n=0,i=t.length;n0&&(e.userData=this.userData),this.parameters!==void 0){const l=this.parameters;for(const c in l)l[c]!==void 0&&(e[c]=l[c]);return e}e.data={attributes:{}};const t=this.index;t!==null&&(e.data.index={type:t.array.constructor.name,array:Array.prototype.slice.call(t.array)});const n=this.attributes;for(const l in n){const c=n[l];e.data.attributes[l]=c.toJSON(e.data)}const i={};let s=!1;for(const l in this.morphAttributes){const c=this.morphAttributes[l],h=[];for(let u=0,f=c.length;u0&&(i[l]=h,s=!0)}s&&(e.data.morphAttributes=i,e.data.morphTargetsRelative=this.morphTargetsRelative);const a=this.groups;a.length>0&&(e.data.groups=JSON.parse(JSON.stringify(a)));const o=this.boundingSphere;return o!==null&&(e.data.boundingSphere={center:o.center.toArray(),radius:o.radius}),e}clone(){return new this.constructor().copy(this)}copy(e){this.index=null,this.attributes={},this.morphAttributes={},this.groups=[],this.boundingBox=null,this.boundingSphere=null;const t={};this.name=e.name;const n=e.index;n!==null&&this.setIndex(n.clone(t));const i=e.attributes;for(const c in i){const h=i[c];this.setAttribute(c,h.clone(t))}const s=e.morphAttributes;for(const c in s){const h=[],u=s[c];for(let f=0,d=u.length;f0){const i=t[n[0]];if(i!==void 0){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let s=0,a=i.length;s(e.far-e.near)**2))&&(Nh.copy(s).invert(),Bi.copy(e.ray).applyMatrix4(Nh),!(n.boundingBox!==null&&Bi.intersectsBox(n.boundingBox)===!1)&&this._computeIntersections(e,t,Bi)))}_computeIntersections(e,t,n){let i;const s=this.geometry,a=this.material,o=s.index,l=s.attributes.position,c=s.attributes.uv,h=s.attributes.uv1,u=s.attributes.normal,f=s.groups,d=s.drawRange;if(o!==null)if(Array.isArray(a))for(let p=0,_=f.length;p<_;p++){const g=f[p],m=a[g.materialIndex],y=Math.max(g.start,d.start),v=Math.min(o.count,Math.min(g.start+g.count,d.start+d.count));for(let x=y,w=v;xt.far?null:{distance:c,point:ha.clone(),object:r}}function ua(r,e,t,n,i,s,a,o,l,c){r.getVertexPosition(o,aa),r.getVertexPosition(l,oa),r.getVertexPosition(c,la);const h=Kp(r,e,t,n,aa,oa,la,Oh);if(h){const u=new A;yn.getBarycoord(Oh,aa,oa,la,u),i&&(h.uv=yn.getInterpolatedAttribute(i,o,l,c,u,new j)),s&&(h.uv1=yn.getInterpolatedAttribute(s,o,l,c,u,new j)),a&&(h.normal=yn.getInterpolatedAttribute(a,o,l,c,u,new A),h.normal.dot(n.direction)>0&&h.normal.multiplyScalar(-1));const f={a:o,b:l,c,normal:new A,materialIndex:0};yn.getNormal(aa,oa,la,f.normal),h.face=f,h.barycoord=u}return h}class or extends qe{constructor(e=1,t=1,n=1,i=1,s=1,a=1){super(),this.type="BoxGeometry",this.parameters={width:e,height:t,depth:n,widthSegments:i,heightSegments:s,depthSegments:a};const o=this;i=Math.floor(i),s=Math.floor(s),a=Math.floor(a);const l=[],c=[],h=[],u=[];let f=0,d=0;p("z","y","x",-1,-1,n,t,e,a,s,0),p("z","y","x",1,-1,n,t,-e,a,s,1),p("x","z","y",1,1,e,n,t,i,a,2),p("x","z","y",1,-1,e,n,-t,i,a,3),p("x","y","z",1,-1,e,t,n,i,s,4),p("x","y","z",-1,-1,e,t,-n,i,s,5),this.setIndex(l),this.setAttribute("position",new Fe(c,3)),this.setAttribute("normal",new Fe(h,3)),this.setAttribute("uv",new Fe(u,2));function p(_,g,m,y,v,x,w,T,C,P,S){const M=x/C,D=w/P,G=x/2,F=w/2,V=T/2,J=C+1,q=P+1;let se=0,Z=0;const _e=new A;for(let be=0;be0?1:-1,h.push(_e.x,_e.y,_e.z),u.push(Ge/C),u.push(1-be/P),se+=1}}for(let be=0;be0&&(t.defines=this.defines),t.vertexShader=this.vertexShader,t.fragmentShader=this.fragmentShader,t.lights=this.lights,t.clipping=this.clipping;const n={};for(const i in this.extensions)this.extensions[i]===!0&&(n[i]=!0);return Object.keys(n).length>0&&(t.extensions=n),t}}class Hc extends mt{constructor(){super(),this.isCamera=!0,this.type="Camera",this.matrixWorldInverse=new Ze,this.projectionMatrix=new Ze,this.projectionMatrixInverse=new Ze,this.coordinateSystem=zn}copy(e,t){return super.copy(e,t),this.matrixWorldInverse.copy(e.matrixWorldInverse),this.projectionMatrix.copy(e.projectionMatrix),this.projectionMatrixInverse.copy(e.projectionMatrixInverse),this.coordinateSystem=e.coordinateSystem,this}getWorldDirection(e){return super.getWorldDirection(e).negate()}updateMatrixWorld(e){super.updateMatrixWorld(e),this.matrixWorldInverse.copy(this.matrixWorld).invert()}updateWorldMatrix(e,t){super.updateWorldMatrix(e,t),this.matrixWorldInverse.copy(this.matrixWorld).invert()}clone(){return new this.constructor().copy(this)}}const yi=new A,Bh=new j,zh=new j;class kt extends Hc{constructor(e=50,t=1,n=.1,i=2e3){super(),this.isPerspectiveCamera=!0,this.type="PerspectiveCamera",this.fov=e,this.zoom=1,this.near=n,this.far=i,this.focus=10,this.aspect=t,this.view=null,this.filmGauge=35,this.filmOffset=0,this.updateProjectionMatrix()}copy(e,t){return super.copy(e,t),this.fov=e.fov,this.zoom=e.zoom,this.near=e.near,this.far=e.far,this.focus=e.focus,this.aspect=e.aspect,this.view=e.view===null?null:Object.assign({},e.view),this.filmGauge=e.filmGauge,this.filmOffset=e.filmOffset,this}setFocalLength(e){const t=.5*this.getFilmHeight()/e;this.fov=er*2*Math.atan(t),this.updateProjectionMatrix()}getFocalLength(){const e=Math.tan(ss*.5*this.fov);return .5*this.getFilmHeight()/e}getEffectiveFOV(){return er*2*Math.atan(Math.tan(ss*.5*this.fov)/this.zoom)}getFilmWidth(){return this.filmGauge*Math.min(this.aspect,1)}getFilmHeight(){return this.filmGauge/Math.max(this.aspect,1)}getViewBounds(e,t,n){yi.set(-1,-1,.5).applyMatrix4(this.projectionMatrixInverse),t.set(yi.x,yi.y).multiplyScalar(-e/yi.z),yi.set(1,1,.5).applyMatrix4(this.projectionMatrixInverse),n.set(yi.x,yi.y).multiplyScalar(-e/yi.z)}getViewSize(e,t){return this.getViewBounds(e,Bh,zh),t.subVectors(zh,Bh)}setViewOffset(e,t,n,i,s,a){this.aspect=e/t,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=e,this.view.fullHeight=t,this.view.offsetX=n,this.view.offsetY=i,this.view.width=s,this.view.height=a,this.updateProjectionMatrix()}clearViewOffset(){this.view!==null&&(this.view.enabled=!1),this.updateProjectionMatrix()}updateProjectionMatrix(){const e=this.near;let t=e*Math.tan(ss*.5*this.fov)/this.zoom,n=2*t,i=this.aspect*n,s=-.5*i;const a=this.view;if(this.view!==null&&this.view.enabled){const l=a.fullWidth,c=a.fullHeight;s+=a.offsetX*i/l,t-=a.offsetY*n/c,i*=a.width/l,n*=a.height/c}const o=this.filmOffset;o!==0&&(s+=e*o/this.getFilmWidth()),this.projectionMatrix.makePerspective(s,s+i,t,t-n,e,this.far,this.coordinateSystem),this.projectionMatrixInverse.copy(this.projectionMatrix).invert()}toJSON(e){const t=super.toJSON(e);return t.object.fov=this.fov,t.object.zoom=this.zoom,t.object.near=this.near,t.object.far=this.far,t.object.focus=this.focus,t.object.aspect=this.aspect,this.view!==null&&(t.object.view=Object.assign({},this.view)),t.object.filmGauge=this.filmGauge,t.object.filmOffset=this.filmOffset,t}}const As=-90,Cs=1;class em extends mt{constructor(e,t,n){super(),this.type="CubeCamera",this.renderTarget=n,this.coordinateSystem=null,this.activeMipmapLevel=0;const i=new kt(As,Cs,e,t);i.layers=this.layers,this.add(i);const s=new kt(As,Cs,e,t);s.layers=this.layers,this.add(s);const a=new kt(As,Cs,e,t);a.layers=this.layers,this.add(a);const o=new kt(As,Cs,e,t);o.layers=this.layers,this.add(o);const l=new kt(As,Cs,e,t);l.layers=this.layers,this.add(l);const c=new kt(As,Cs,e,t);c.layers=this.layers,this.add(c)}updateCoordinateSystem(){const e=this.coordinateSystem,t=this.children.concat(),[n,i,s,a,o,l]=t;for(const c of t)this.remove(c);if(e===zn)n.up.set(0,1,0),n.lookAt(1,0,0),i.up.set(0,1,0),i.lookAt(-1,0,0),s.up.set(0,0,-1),s.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(e===uo)n.up.set(0,-1,0),n.lookAt(-1,0,0),i.up.set(0,-1,0),i.lookAt(1,0,0),s.up.set(0,0,1),s.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: "+e);for(const c of t)this.add(c),c.updateMatrixWorld()}update(e,t){this.parent===null&&this.updateMatrixWorld();const{renderTarget:n,activeMipmapLevel:i}=this;this.coordinateSystem!==e.coordinateSystem&&(this.coordinateSystem=e.coordinateSystem,this.updateCoordinateSystem());const[s,a,o,l,c,h]=this.children,u=e.getRenderTarget(),f=e.getActiveCubeFace(),d=e.getActiveMipmapLevel(),p=e.xr.enabled;e.xr.enabled=!1;const _=n.texture.generateMipmaps;n.texture.generateMipmaps=!1,e.setRenderTarget(n,0,i),e.render(t,s),e.setRenderTarget(n,1,i),e.render(t,a),e.setRenderTarget(n,2,i),e.render(t,o),e.setRenderTarget(n,3,i),e.render(t,l),e.setRenderTarget(n,4,i),e.render(t,c),n.texture.generateMipmaps=_,e.setRenderTarget(n,5,i),e.render(t,h),e.setRenderTarget(u,f,d),e.xr.enabled=p,n.texture.needsPMREMUpdate=!0}}class Eo extends Et{constructor(e,t,n,i,s,a,o,l,c,h){e=e!==void 0?e:[],t=t!==void 0?t:Ri,super(e,t,n,i,s,a,o,l,c,h),this.isCubeTexture=!0,this.flipY=!1}get images(){return this.image}set images(e){this.image=e}}class tm extends Sn{constructor(e=1,t={}){super(e,e,t),this.isWebGLCubeRenderTarget=!0;const n={width:e,height:e,depth:1},i=[n,n,n,n,n,n];this.texture=new Eo(i,t.mapping,t.wrapS,t.wrapT,t.magFilter,t.minFilter,t.format,t.type,t.anisotropy,t.colorSpace),this.texture.isRenderTargetTexture=!0,this.texture.generateMipmaps=t.generateMipmaps!==void 0?t.generateMipmaps:!1,this.texture.minFilter=t.minFilter!==void 0?t.minFilter:Vt}fromEquirectangularTexture(e,t){this.texture.type=t.type,this.texture.colorSpace=t.colorSpace,this.texture.generateMipmaps=t.generateMipmaps,this.texture.minFilter=t.minFilter,this.texture.magFilter=t.magFilter;const n={uniforms:{tEquirect:{value:null}},vertexShader:` - - varying vec3 vWorldDirection; - - vec3 transformDirection( in vec3 dir, in mat4 matrix ) { - - return normalize( ( matrix * vec4( dir, 0.0 ) ).xyz ); - - } - - void main() { - - vWorldDirection = transformDirection( position, modelMatrix ); - - #include - #include - - } - `,fragmentShader:` - - uniform sampler2D tEquirect; - - varying vec3 vWorldDirection; - - #include - - void main() { - - vec3 direction = normalize( vWorldDirection ); - - vec2 sampleUV = equirectUv( direction ); - - gl_FragColor = texture2D( tEquirect, sampleUV ); - - } - `},i=new or(5,5,5),s=new Xt({name:"CubemapFromEquirect",uniforms:tr(n.uniforms),vertexShader:n.vertexShader,fragmentShader:n.fragmentShader,side:fn,blending:ii});s.uniforms.tEquirect.value=t;const a=new yt(i,s),o=t.minFilter;return t.minFilter===ni&&(t.minFilter=Vt),new em(1,10,this).update(e,a),t.minFilter=o,a.geometry.dispose(),a.material.dispose(),this}clear(e,t,n,i){const s=e.getRenderTarget();for(let a=0;a<6;a++)e.setRenderTarget(this,a),e.clear(t,n,i);e.setRenderTarget(s)}}class Yr{constructor(e,t=25e-5){this.isFogExp2=!0,this.name="",this.color=new ne(e),this.density=t}clone(){return new Yr(this.color,this.density)}toJSON(){return{type:"FogExp2",name:this.name,color:this.color.getHex(),density:this.density}}}class Gc{constructor(e,t=1,n=1e3){this.isFog=!0,this.name="",this.color=new ne(e),this.near=t,this.far=n}clone(){return new Gc(this.color,this.near,this.far)}toJSON(){return{type:"Fog",name:this.name,color:this.color.getHex(),near:this.near,far:this.far}}}class Cd extends mt{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 In,this.environmentIntensity=1,this.environmentRotation=new In,this.overrideMaterial=null,typeof __THREE_DEVTOOLS__<"u"&&__THREE_DEVTOOLS__.dispatchEvent(new CustomEvent("observe",{detail:this}))}copy(e,t){return super.copy(e,t),e.background!==null&&(this.background=e.background.clone()),e.environment!==null&&(this.environment=e.environment.clone()),e.fog!==null&&(this.fog=e.fog.clone()),this.backgroundBlurriness=e.backgroundBlurriness,this.backgroundIntensity=e.backgroundIntensity,this.backgroundRotation.copy(e.backgroundRotation),this.environmentIntensity=e.environmentIntensity,this.environmentRotation.copy(e.environmentRotation),e.overrideMaterial!==null&&(this.overrideMaterial=e.overrideMaterial.clone()),this.matrixAutoUpdate=e.matrixAutoUpdate,this}toJSON(e){const t=super.toJSON(e);return this.fog!==null&&(t.object.fog=this.fog.toJSON()),this.backgroundBlurriness>0&&(t.object.backgroundBlurriness=this.backgroundBlurriness),this.backgroundIntensity!==1&&(t.object.backgroundIntensity=this.backgroundIntensity),t.object.backgroundRotation=this.backgroundRotation.toArray(),this.environmentIntensity!==1&&(t.object.environmentIntensity=this.environmentIntensity),t.object.environmentRotation=this.environmentRotation.toArray(),t}}class Wc{constructor(e,t){this.isInterleavedBuffer=!0,this.array=e,this.stride=t,this.count=e!==void 0?e.length/t:0,this.usage=ho,this.updateRanges=[],this.version=0,this.uuid=bn()}onUploadCallback(){}set needsUpdate(e){e===!0&&this.version++}setUsage(e){return this.usage=e,this}addUpdateRange(e,t){this.updateRanges.push({start:e,count:t})}clearUpdateRanges(){this.updateRanges.length=0}copy(e){return this.array=new e.array.constructor(e.array),this.count=e.count,this.stride=e.stride,this.usage=e.usage,this}copyAt(e,t,n){e*=this.stride,n*=t.stride;for(let i=0,s=this.stride;ie.far||t.push({distance:l,point:xr.clone(),uv:yn.getInterpolation(xr,da,Mr,fa,kh,nl,Vh,new j),face:null,object:this})}copy(e,t){return super.copy(e,t),e.center!==void 0&&this.center.copy(e.center),this.material=e.material,this}}function pa(r,e,t,n,i,s){Ds.subVectors(r,t).addScalar(.5).multiply(n),i!==void 0?(yr.x=s*Ds.x-i*Ds.y,yr.y=i*Ds.x+s*Ds.y):yr.copy(Ds),r.copy(e),r.x+=yr.x,r.y+=yr.y,r.applyMatrix4(Rd)}const ma=new A,Hh=new A;class nm extends mt{constructor(){super(),this._currentLevel=0,this.type="LOD",Object.defineProperties(this,{levels:{enumerable:!0,value:[]},isLOD:{value:!0}}),this.autoUpdate=!0}copy(e){super.copy(e,!1);const t=e.levels;for(let n=0,i=t.length;n0){let n,i;for(n=1,i=t.length;n0){ma.setFromMatrixPosition(this.matrixWorld);const i=e.ray.origin.distanceTo(ma);this.getObjectForDistance(i).raycast(e,t)}}update(e){const t=this.levels;if(t.length>1){ma.setFromMatrixPosition(e.matrixWorld),Hh.setFromMatrixPosition(this.matrixWorld);const n=ma.distanceTo(Hh)/e.zoom;t[0].object.visible=!0;let i,s;for(i=1,s=t.length;i=a)t[i-1].object.visible=!1,t[i].object.visible=!0;else break}for(this._currentLevel=i-1;i1?null:t.copy(e.start).addScaledVector(n,s)}intersectsLine(e){const t=this.distanceToPoint(e.start),n=this.distanceToPoint(e.end);return t<0&&n>0||n<0&&t>0}intersectsBox(e){return e.intersectsPlane(this)}intersectsSphere(e){return e.intersectsPlane(this)}coplanarPoint(e){return e.copy(this.normal).multiplyScalar(-this.constant)}applyMatrix4(e,t){const n=t||lm.getNormalMatrix(e),i=this.coplanarPoint(rl).applyMatrix4(e),s=this.normal.applyMatrix3(n).normalize();return this.constant=-i.dot(s),this}translate(e){return this.constant-=e.dot(this.normal),this}equals(e){return e.normal.equals(this.normal)&&e.constant===this.constant}clone(){return new this.constructor().copy(this)}}const zi=new an,va=new A;class To{constructor(e=new Si,t=new Si,n=new Si,i=new Si,s=new Si,a=new Si){this.planes=[e,t,n,i,s,a]}set(e,t,n,i,s,a){const o=this.planes;return o[0].copy(e),o[1].copy(t),o[2].copy(n),o[3].copy(i),o[4].copy(s),o[5].copy(a),this}copy(e){const t=this.planes;for(let n=0;n<6;n++)t[n].copy(e.planes[n]);return this}setFromProjectionMatrix(e,t=zn){const n=this.planes,i=e.elements,s=i[0],a=i[1],o=i[2],l=i[3],c=i[4],h=i[5],u=i[6],f=i[7],d=i[8],p=i[9],_=i[10],g=i[11],m=i[12],y=i[13],v=i[14],x=i[15];if(n[0].setComponents(l-s,f-c,g-d,x-m).normalize(),n[1].setComponents(l+s,f+c,g+d,x+m).normalize(),n[2].setComponents(l+a,f+h,g+p,x+y).normalize(),n[3].setComponents(l-a,f-h,g-p,x-y).normalize(),n[4].setComponents(l-o,f-u,g-_,x-v).normalize(),t===zn)n[5].setComponents(l+o,f+u,g+_,x+v).normalize();else if(t===uo)n[5].setComponents(o,u,_,v).normalize();else throw new Error("THREE.Frustum.setFromProjectionMatrix(): Invalid coordinate system: "+t);return this}intersectsObject(e){if(e.boundingSphere!==void 0)e.boundingSphere===null&&e.computeBoundingSphere(),zi.copy(e.boundingSphere).applyMatrix4(e.matrixWorld);else{const t=e.geometry;t.boundingSphere===null&&t.computeBoundingSphere(),zi.copy(t.boundingSphere).applyMatrix4(e.matrixWorld)}return this.intersectsSphere(zi)}intersectsSprite(e){return zi.center.set(0,0,0),zi.radius=.7071067811865476,zi.applyMatrix4(e.matrixWorld),this.intersectsSphere(zi)}intersectsSphere(e){const t=this.planes,n=e.center,i=-e.radius;for(let s=0;s<6;s++)if(t[s].distanceToPoint(n)0?e.max.x:e.min.x,va.y=i.normal.y>0?e.max.y:e.min.y,va.z=i.normal.z>0?e.max.z:e.min.z,i.distanceToPoint(va)<0)return!1}return!0}containsPoint(e){const t=this.planes;for(let n=0;n<6;n++)if(t[n].distanceToPoint(e)<0)return!1;return!0}clone(){return new this.constructor().copy(this)}}function al(r,e){return r-e}function cm(r,e){return r.z-e.z}function hm(r,e){return e.z-r.z}class um{constructor(){this.index=0,this.pool=[],this.list=[]}push(e,t,n,i){const s=this.pool,a=this.list;this.index>=s.length&&s.push({start:-1,count:-1,z:-1,index:-1});const o=s[this.index];a.push(o),this.index++,o.start=e,o.count=t,o.z=n,o.index=i}reset(){this.list.length=0,this.index=0}}const cn=new Ze,dm=new ne(1,1,1),ol=new To,xa=new pn,ki=new an,wr=new A,$h=new A,fm=new A,ll=new um,$t=new yt,ya=[];function pm(r,e,t=0){const n=e.itemSize;if(r.isInterleavedBufferAttribute||r.array.constructor!==e.array.constructor){const i=r.count;for(let s=0;s65535?new Uint32Array(i):new Uint16Array(i);t.setIndex(new rt(s,1))}this._geometryInitialized=!0}}_validateGeometry(e){const t=this.geometry;if(!!e.getIndex()!=!!t.getIndex())throw new Error('THREE.BatchedMesh: All geometries must consistently have "index".');for(const n in t.attributes){if(!e.hasAttribute(n))throw new Error(`THREE.BatchedMesh: Added geometry missing "${n}". All geometries must have consistent attributes.`);const i=e.getAttribute(n),s=t.getAttribute(n);if(i.itemSize!==s.itemSize||i.normalized!==s.normalized)throw new Error("THREE.BatchedMesh: All attributes must have a consistent itemSize and normalized value.")}}validateInstanceId(e){const t=this._instanceInfo;if(e<0||e>=t.length||t[e].active===!1)throw new Error(`THREE.BatchedMesh: Invalid instanceId ${e}. Instance is either out of range or has been deleted.`)}validateGeometryId(e){const t=this._geometryInfo;if(e<0||e>=t.length||t[e].active===!1)throw new Error(`THREE.BatchedMesh: Invalid geometryId ${e}. Geometry is either out of range or has been deleted.`)}setCustomSort(e){return this.customSort=e,this}computeBoundingBox(){this.boundingBox===null&&(this.boundingBox=new pn);const e=this.boundingBox,t=this._instanceInfo;e.makeEmpty();for(let n=0,i=t.length;n=this.maxInstanceCount&&this._availableInstanceIds.length===0)throw new Error("THREE.BatchedMesh: Maximum item count reached.");const n={visible:!0,active:!0,geometryIndex:e};let i=null;this._availableInstanceIds.length>0?(this._availableInstanceIds.sort(al),i=this._availableInstanceIds.shift(),this._instanceInfo[i]=n):(i=this._instanceInfo.length,this._instanceInfo.push(n));const s=this._matricesTexture;cn.identity().toArray(s.image.data,i*16),s.needsUpdate=!0;const a=this._colorsTexture;return a&&(dm.toArray(a.image.data,i*4),a.needsUpdate=!0),this._visibilityChanged=!0,i}addGeometry(e,t=-1,n=-1){this._initializeGeometry(e),this._validateGeometry(e);const i={vertexStart:-1,vertexCount:-1,reservedVertexCount:-1,indexStart:-1,indexCount:-1,reservedIndexCount:-1,start:-1,count:-1,boundingBox:null,boundingSphere:null,active:!0},s=this._geometryInfo;i.vertexStart=this._nextVertexStart,i.reservedVertexCount=t===-1?e.getAttribute("position").count:t;const a=e.getIndex();if(a!==null&&(i.indexStart=this._nextIndexStart,i.reservedIndexCount=n===-1?a.count:n),i.indexStart!==-1&&i.indexStart+i.reservedIndexCount>this._maxIndexCount||i.vertexStart+i.reservedVertexCount>this._maxVertexCount)throw new Error("THREE.BatchedMesh: Reserved space request exceeds the maximum buffer size.");let l;return this._availableGeometryIds.length>0?(this._availableGeometryIds.sort(al),l=this._availableGeometryIds.shift(),s[l]=i):(l=this._geometryCount,this._geometryCount++,s.push(i)),this.setGeometryAt(l,e),this._nextIndexStart=i.indexStart+i.reservedIndexCount,this._nextVertexStart=i.vertexStart+i.reservedVertexCount,l}setGeometryAt(e,t){if(e>=this._geometryCount)throw new Error("THREE.BatchedMesh: Maximum geometry count reached.");this._validateGeometry(t);const n=this.geometry,i=n.getIndex()!==null,s=n.getIndex(),a=t.getIndex(),o=this._geometryInfo[e];if(i&&a.count>o.reservedIndexCount||t.attributes.position.count>o.reservedVertexCount)throw new Error("THREE.BatchedMesh: Reserved space not large enough for provided geometry.");const l=o.vertexStart,c=o.reservedVertexCount;o.vertexCount=t.getAttribute("position").count;for(const h in n.attributes){const u=t.getAttribute(h),f=n.getAttribute(h);pm(u,f,l);const d=u.itemSize;for(let p=u.count,_=c;p<_;p++){const g=l+p;for(let m=0;m=t.length||t[e].active===!1)return this;const n=this._instanceInfo;for(let i=0,s=n.length;io).sort((a,o)=>n[a].vertexStart-n[o].vertexStart),s=this.geometry;for(let a=0,o=n.length;a=this._geometryCount)return null;const n=this.geometry,i=this._geometryInfo[e];if(i.boundingBox===null){const s=new pn,a=n.index,o=n.attributes.position;for(let l=i.start,c=i.start+i.count;l=this._geometryCount)return null;const n=this.geometry,i=this._geometryInfo[e];if(i.boundingSphere===null){const s=new an;this.getBoundingBoxAt(e,xa),xa.getCenter(s.center);const a=n.index,o=n.attributes.position;let l=0;for(let c=i.start,h=i.start+i.count;co.active);if(Math.max(...n.map(o=>o.vertexStart+o.reservedVertexCount))>e)throw new Error(`BatchedMesh: Geometry vertex values are being used outside the range ${t}. Cannot shrink further.`);if(this.geometry.index&&Math.max(...n.map(l=>l.indexStart+l.reservedIndexCount))>t)throw new Error(`BatchedMesh: Geometry index values are being used outside the range ${t}. Cannot shrink further.`);const s=this.geometry;s.dispose(),this._maxVertexCount=e,this._maxIndexCount=t,this._geometryInitialized&&(this._geometryInitialized=!1,this.geometry=new qe,this._initializeGeometry(s));const a=this.geometry;s.index&&Vi(s.index.array,a.index.array);for(const o in s.attributes)Vi(s.attributes[o].array,a.attributes[o].array)}raycast(e,t){const n=this._instanceInfo,i=this._geometryInfo,s=this.matrixWorld,a=this.geometry;$t.material=this.material,$t.geometry.index=a.index,$t.geometry.attributes=a.attributes,$t.geometry.boundingBox===null&&($t.geometry.boundingBox=new pn),$t.geometry.boundingSphere===null&&($t.geometry.boundingSphere=new an);for(let o=0,l=n.length;o({...t,boundingBox:t.boundingBox!==null?t.boundingBox.clone():null,boundingSphere:t.boundingSphere!==null?t.boundingSphere.clone():null})),this._instanceInfo=e._instanceInfo.map(t=>({...t})),this._maxInstanceCount=e._maxInstanceCount,this._maxVertexCount=e._maxVertexCount,this._maxIndexCount=e._maxIndexCount,this._geometryInitialized=e._geometryInitialized,this._geometryCount=e._geometryCount,this._multiDrawCounts=e._multiDrawCounts.slice(),this._multiDrawStarts=e._multiDrawStarts.slice(),this._matricesTexture=e._matricesTexture.clone(),this._matricesTexture.image.data=this._matricesTexture.image.data.slice(),this._colorsTexture!==null&&(this._colorsTexture=e._colorsTexture.clone(),this._colorsTexture.image.data=this._colorsTexture.image.data.slice()),this}dispose(){return this.geometry.dispose(),this._matricesTexture.dispose(),this._matricesTexture=null,this._indirectTexture.dispose(),this._indirectTexture=null,this._colorsTexture!==null&&(this._colorsTexture.dispose(),this._colorsTexture=null),this}onBeforeRender(e,t,n,i,s){if(!this._visibilityChanged&&!this.perObjectFrustumCulled&&!this.sortObjects)return;const a=i.getIndex(),o=a===null?1:a.array.BYTES_PER_ELEMENT,l=this._instanceInfo,c=this._multiDrawStarts,h=this._multiDrawCounts,u=this._geometryInfo,f=this.perObjectFrustumCulled,d=this._indirectTexture,p=d.image.data;f&&(cn.multiplyMatrices(n.projectionMatrix,n.matrixWorldInverse).multiply(this.matrixWorld),ol.setFromProjectionMatrix(cn,e.coordinateSystem));let _=0;if(this.sortObjects){cn.copy(this.matrixWorld).invert(),wr.setFromMatrixPosition(n.matrixWorld).applyMatrix4(cn),$h.set(0,0,-1).transformDirection(n.matrixWorld).transformDirection(cn);for(let y=0,v=l.length;y0){const i=t[n[0]];if(i!==void 0){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let s=0,a=i.length;sn)return;cl.applyMatrix4(r.matrixWorld);const l=e.ray.origin.distanceTo(cl);if(!(le.far))return{distance:l,point:Qh.clone().applyMatrix4(r.matrixWorld),index:i,face:null,faceIndex:null,barycoord:null,object:r}}const eu=new A,tu=new A;class hi extends Vn{constructor(e,t){super(e,t),this.isLineSegments=!0,this.type="LineSegments"}computeLineDistances(){const e=this.geometry;if(e.index===null){const t=e.attributes.position,n=[];for(let i=0,s=t.count;i0){const i=t[n[0]];if(i!==void 0){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let s=0,a=i.length;si.far)return;s.push({distance:c,distanceToRay:Math.sqrt(o),point:l,index:e,face:null,faceIndex:null,barycoord:null,object:a})}}class ns extends mt{constructor(){super(),this.isGroup=!0,this.type="Group"}}class ZS extends Et{constructor(e,t,n,i,s,a,o,l,c){super(e,t,n,i,s,a,o,l,c),this.isVideoTexture=!0,this.minFilter=a!==void 0?a:Vt,this.magFilter=s!==void 0?s:Vt,this.generateMipmaps=!1;const h=this;function u(){h.needsUpdate=!0,e.requestVideoFrameCallback(u)}"requestVideoFrameCallback"in e&&e.requestVideoFrameCallback(u)}clone(){return new this.constructor(this.image).copy(this)}update(){const e=this.image;"requestVideoFrameCallback"in e===!1&&e.readyState>=e.HAVE_CURRENT_DATA&&(this.needsUpdate=!0)}}class JS extends Et{constructor(e,t){super({width:e,height:t}),this.isFramebufferTexture=!0,this.magFilter=Qt,this.minFilter=Qt,this.generateMipmaps=!1,this.needsUpdate=!0}}class qc extends Et{constructor(e,t,n,i,s,a,o,l,c,h,u,f){super(null,a,o,l,c,h,i,s,u,f),this.isCompressedTexture=!0,this.image={width:t,height:n},this.mipmaps=e,this.flipY=!1,this.generateMipmaps=!1}}class KS extends qc{constructor(e,t,n,i,s,a){super(e,t,n,s,a),this.isCompressedArrayTexture=!0,this.image.depth=i,this.wrapR=Nn,this.layerUpdates=new Set}addLayerUpdate(e){this.layerUpdates.add(e)}clearLayerUpdates(){this.layerUpdates.clear()}}class $S extends qc{constructor(e,t,n){super(void 0,e[0].width,e[0].height,t,n,Ri),this.isCompressedCubeTexture=!0,this.isCubeTexture=!0,this.image=e}}class Dd extends Et{constructor(e,t,n,i,s,a,o,l,c){super(e,t,n,i,s,a,o,l,c),this.isCanvasTexture=!0,this.needsUpdate=!0}}class Ld extends Et{constructor(e,t,n,i,s,a,o,l,c,h=Ys){if(h!==Ys&&h!==js)throw new Error("DepthTexture format must be either THREE.DepthFormat or THREE.DepthStencilFormat");n===void 0&&h===Ys&&(n=Ii),n===void 0&&h===js&&(n=$s),super(null,i,s,a,o,l,h,n,c),this.isDepthTexture=!0,this.image={width:e,height:t},this.magFilter=o!==void 0?o:Qt,this.minFilter=l!==void 0?l:Qt,this.flipY=!1,this.generateMipmaps=!1,this.compareFunction=null}copy(e){return super.copy(e),this.compareFunction=e.compareFunction,this}toJSON(e){const t=super.toJSON(e);return this.compareFunction!==null&&(t.compareFunction=this.compareFunction),t}}class Hn{constructor(){this.type="Curve",this.arcLengthDivisions=200}getPoint(){return console.warn("THREE.Curve: .getPoint() not implemented."),null}getPointAt(e,t){const n=this.getUtoTmapping(e);return this.getPoint(n,t)}getPoints(e=5){const t=[];for(let n=0;n<=e;n++)t.push(this.getPoint(n/e));return t}getSpacedPoints(e=5){const t=[];for(let n=0;n<=e;n++)t.push(this.getPointAt(n/e));return t}getLength(){const e=this.getLengths();return e[e.length-1]}getLengths(e=this.arcLengthDivisions){if(this.cacheArcLengths&&this.cacheArcLengths.length===e+1&&!this.needsUpdate)return this.cacheArcLengths;this.needsUpdate=!1;const t=[];let n,i=this.getPoint(0),s=0;t.push(0);for(let a=1;a<=e;a++)n=this.getPoint(a/e),s+=n.distanceTo(i),t.push(s),i=n;return this.cacheArcLengths=t,t}updateArcLengths(){this.needsUpdate=!0,this.getLengths()}getUtoTmapping(e,t){const n=this.getLengths();let i=0;const s=n.length;let a;t?a=t:a=e*n[s-1];let o=0,l=s-1,c;for(;o<=l;)if(i=Math.floor(o+(l-o)/2),c=n[i]-a,c<0)o=i+1;else if(c>0)l=i-1;else{l=i;break}if(i=l,n[i]===a)return i/(s-1);const h=n[i],f=n[i+1]-h,d=(a-h)/f;return(i+d)/(s-1)}getTangent(e,t){let i=e-1e-4,s=e+1e-4;i<0&&(i=0),s>1&&(s=1);const a=this.getPoint(i),o=this.getPoint(s),l=t||(a.isVector2?new j:new A);return l.copy(o).sub(a).normalize(),l}getTangentAt(e,t){const n=this.getUtoTmapping(e);return this.getTangent(n,t)}computeFrenetFrames(e,t){const n=new A,i=[],s=[],a=[],o=new A,l=new Ze;for(let d=0;d<=e;d++){const p=d/e;i[d]=this.getTangentAt(p,new A)}s[0]=new A,a[0]=new A;let c=Number.MAX_VALUE;const h=Math.abs(i[0].x),u=Math.abs(i[0].y),f=Math.abs(i[0].z);h<=c&&(c=h,n.set(1,0,0)),u<=c&&(c=u,n.set(0,1,0)),f<=c&&n.set(0,0,1),o.crossVectors(i[0],n).normalize(),s[0].crossVectors(i[0],o),a[0].crossVectors(i[0],s[0]);for(let d=1;d<=e;d++){if(s[d]=s[d-1].clone(),a[d]=a[d-1].clone(),o.crossVectors(i[d-1],i[d]),o.length()>Number.EPSILON){o.normalize();const p=Math.acos(et(i[d-1].dot(i[d]),-1,1));s[d].applyMatrix4(l.makeRotationAxis(o,p))}a[d].crossVectors(i[d],s[d])}if(t===!0){let d=Math.acos(et(s[0].dot(s[e]),-1,1));d/=e,i[0].dot(o.crossVectors(s[0],s[e]))>0&&(d=-d);for(let p=1;p<=e;p++)s[p].applyMatrix4(l.makeRotationAxis(i[p],d*p)),a[p].crossVectors(i[p],s[p])}return{tangents:i,normals:s,binormals:a}}clone(){return new this.constructor().copy(this)}copy(e){return this.arcLengthDivisions=e.arcLengthDivisions,this}toJSON(){const e={metadata:{version:4.6,type:"Curve",generator:"Curve.toJSON"}};return e.arcLengthDivisions=this.arcLengthDivisions,e.type=this.type,e}fromJSON(e){return this.arcLengthDivisions=e.arcLengthDivisions,this}}class Yc extends Hn{constructor(e=0,t=0,n=1,i=1,s=0,a=Math.PI*2,o=!1,l=0){super(),this.isEllipseCurve=!0,this.type="EllipseCurve",this.aX=e,this.aY=t,this.xRadius=n,this.yRadius=i,this.aStartAngle=s,this.aEndAngle=a,this.aClockwise=o,this.aRotation=l}getPoint(e,t=new j){const n=t,i=Math.PI*2;let s=this.aEndAngle-this.aStartAngle;const a=Math.abs(s)i;)s-=i;s0?0:(Math.floor(Math.abs(o)/s)+1)*s:l===0&&o===s-1&&(o=s-2,l=1);let c,h;this.closed||o>0?c=i[(o-1)%s]:(Ea.subVectors(i[0],i[1]).add(i[0]),c=Ea);const u=i[o%s],f=i[(o+1)%s];if(this.closed||o+2i.length-2?i.length-1:a+1],u=i[a>i.length-3?i.length-1:a+2];return n.set(su(o,l.x,c.x,h.x,u.x),su(o,l.y,c.y,h.y,u.y)),n}copy(e){super.copy(e),this.points=[];for(let t=0,n=e.points.length;t=n){const a=i[s]-n,o=this.curves[s],l=o.getLength(),c=l===0?0:1-a/l;return o.getPointAt(c,t)}s++}return null}getLength(){const e=this.getCurveLengths();return e[e.length-1]}updateArcLengths(){this.needsUpdate=!0,this.cacheLengths=null,this.getCurveLengths()}getCurveLengths(){if(this.cacheLengths&&this.cacheLengths.length===this.curves.length)return this.cacheLengths;const e=[];let t=0;for(let n=0,i=this.curves.length;n1&&!t[t.length-1].equals(t[0])&&t.push(t[0]),t}copy(e){super.copy(e),this.curves=[];for(let t=0,n=e.curves.length;t0){const u=c.getPoint(0);u.equals(this.currentPoint)||this.lineTo(u.x,u.y)}this.curves.push(c);const h=c.getPoint(1);return this.currentPoint.copy(h),this}copy(e){return super.copy(e),this.currentPoint.copy(e.currentPoint),this}toJSON(){const e=super.toJSON();return e.currentPoint=this.currentPoint.toArray(),e}fromJSON(e){return super.fromJSON(e),this.currentPoint.fromArray(e.currentPoint),this}}class Ao extends qe{constructor(e=[new j(0,-.5),new j(.5,0),new j(0,.5)],t=12,n=0,i=Math.PI*2){super(),this.type="LatheGeometry",this.parameters={points:e,segments:t,phiStart:n,phiLength:i},t=Math.floor(t),i=et(i,0,Math.PI*2);const s=[],a=[],o=[],l=[],c=[],h=1/t,u=new A,f=new j,d=new A,p=new A,_=new A;let g=0,m=0;for(let y=0;y<=e.length-1;y++)switch(y){case 0:g=e[y+1].x-e[y].x,m=e[y+1].y-e[y].y,d.x=m*1,d.y=-g,d.z=m*0,_.copy(d),d.normalize(),l.push(d.x,d.y,d.z);break;case e.length-1:l.push(_.x,_.y,_.z);break;default:g=e[y+1].x-e[y].x,m=e[y+1].y-e[y].y,d.x=m*1,d.y=-g,d.z=m*0,p.copy(d),d.x+=_.x,d.y+=_.y,d.z+=_.z,d.normalize(),l.push(d.x,d.y,d.z),_.copy(p)}for(let y=0;y<=t;y++){const v=n+y*h*i,x=Math.sin(v),w=Math.cos(v);for(let T=0;T<=e.length-1;T++){u.x=e[T].x*x,u.y=e[T].y,u.z=e[T].x*w,a.push(u.x,u.y,u.z),f.x=y/t,f.y=T/(e.length-1),o.push(f.x,f.y);const C=l[3*T+0]*x,P=l[3*T+1],S=l[3*T+0]*w;c.push(C,P,S)}}for(let y=0;y0&&v(!0),t>0&&v(!1)),this.setIndex(h),this.setAttribute("position",new Fe(u,3)),this.setAttribute("normal",new Fe(f,3)),this.setAttribute("uv",new Fe(d,2));function y(){const x=new A,w=new A;let T=0;const C=(t-e)/n;for(let P=0;P<=s;P++){const S=[],M=P/s,D=M*(t-e)+e;for(let G=0;G<=i;G++){const F=G/i,V=F*l+o,J=Math.sin(V),q=Math.cos(V);w.x=D*J,w.y=-M*n+g,w.z=D*q,u.push(w.x,w.y,w.z),x.set(J,C,q).normalize(),f.push(x.x,x.y,x.z),d.push(F,1-M),S.push(p++)}_.push(S)}for(let P=0;P0||S!==0)&&(h.push(M,D,F),T+=3),(t>0||S!==s-1)&&(h.push(D,G,F),T+=3)}c.addGroup(m,T,0),m+=T}function v(x){const w=p,T=new j,C=new A;let P=0;const S=x===!0?e:t,M=x===!0?1:-1;for(let G=1;G<=i;G++)u.push(0,g*M,0),f.push(0,M,0),d.push(.5,.5),p++;const D=p;for(let G=0;G<=i;G++){const V=G/i*l+o,J=Math.cos(V),q=Math.sin(V);C.x=S*q,C.y=g*M,C.z=S*J,u.push(C.x,C.y,C.z),f.push(0,M,0),T.x=J*.5+.5,T.y=q*.5*M+.5,d.push(T.x,T.y),p++}for(let G=0;G.9&&C<.1&&(v<.2&&(a[y+0]+=1),x<.2&&(a[y+2]+=1),w<.2&&(a[y+4]+=1))}}function f(y){s.push(y.x,y.y,y.z)}function d(y,v){const x=y*3;v.x=e[x+0],v.y=e[x+1],v.z=e[x+2]}function p(){const y=new A,v=new A,x=new A,w=new A,T=new j,C=new j,P=new j;for(let S=0,M=0;S80*t){o=c=r[0],l=h=r[1];for(let p=t;pc&&(c=u),f>h&&(h=f);d=Math.max(c-o,h-l),d=d!==0?32767/d:0}return Gr(s,a,t,o,l,d,0),a}};function zd(r,e,t,n,i){let s,a;if(i===qm(r,e,t,n)>0)for(s=e;s=e;s-=n)a=ru(s,r[s],r[s+1],a);return a&&Co(a,a.next)&&(Xr(a),a=a.next),a}function ls(r,e){if(!r)return r;e||(e=r);let t=r,n;do if(n=!1,!t.steiner&&(Co(t,t.next)||wt(t.prev,t,t.next)===0)){if(Xr(t),t=e=t.prev,t===t.next)break;n=!0}else t=t.next;while(n||t!==e);return e}function Gr(r,e,t,n,i,s,a){if(!r)return;!a&&s&&km(r,n,i,s);let o=r,l,c;for(;r.prev!==r.next;){if(l=r.prev,c=r.next,s?Dm(r,n,i,s):Pm(r)){e.push(l.i/t|0),e.push(r.i/t|0),e.push(c.i/t|0),Xr(r),r=c.next,o=c.next;continue}if(r=c,r===o){a?a===1?(r=Lm(ls(r),e,t),Gr(r,e,t,n,i,s,2)):a===2&&Um(r,e,t,n,i,s):Gr(ls(r),e,t,n,i,s,1);break}}}function Pm(r){const e=r.prev,t=r,n=r.next;if(wt(e,t,n)>=0)return!1;const i=e.x,s=t.x,a=n.x,o=e.y,l=t.y,c=n.y,h=is?i>a?i:a:s>a?s:a,d=o>l?o>c?o:c:l>c?l:c;let p=n.next;for(;p!==e;){if(p.x>=h&&p.x<=f&&p.y>=u&&p.y<=d&&Hs(i,o,s,l,a,c,p.x,p.y)&&wt(p.prev,p,p.next)>=0)return!1;p=p.next}return!0}function Dm(r,e,t,n){const i=r.prev,s=r,a=r.next;if(wt(i,s,a)>=0)return!1;const o=i.x,l=s.x,c=a.x,h=i.y,u=s.y,f=a.y,d=ol?o>c?o:c:l>c?l:c,g=h>u?h>f?h:f:u>f?u:f,m=pc(d,p,e,t,n),y=pc(_,g,e,t,n);let v=r.prevZ,x=r.nextZ;for(;v&&v.z>=m&&x&&x.z<=y;){if(v.x>=d&&v.x<=_&&v.y>=p&&v.y<=g&&v!==i&&v!==a&&Hs(o,h,l,u,c,f,v.x,v.y)&&wt(v.prev,v,v.next)>=0||(v=v.prevZ,x.x>=d&&x.x<=_&&x.y>=p&&x.y<=g&&x!==i&&x!==a&&Hs(o,h,l,u,c,f,x.x,x.y)&&wt(x.prev,x,x.next)>=0))return!1;x=x.nextZ}for(;v&&v.z>=m;){if(v.x>=d&&v.x<=_&&v.y>=p&&v.y<=g&&v!==i&&v!==a&&Hs(o,h,l,u,c,f,v.x,v.y)&&wt(v.prev,v,v.next)>=0)return!1;v=v.prevZ}for(;x&&x.z<=y;){if(x.x>=d&&x.x<=_&&x.y>=p&&x.y<=g&&x!==i&&x!==a&&Hs(o,h,l,u,c,f,x.x,x.y)&&wt(x.prev,x,x.next)>=0)return!1;x=x.nextZ}return!0}function Lm(r,e,t){let n=r;do{const i=n.prev,s=n.next.next;!Co(i,s)&&kd(i,n,n.next,s)&&Wr(i,s)&&Wr(s,i)&&(e.push(i.i/t|0),e.push(n.i/t|0),e.push(s.i/t|0),Xr(n),Xr(n.next),n=r=s),n=n.next}while(n!==r);return ls(n)}function Um(r,e,t,n,i,s){let a=r;do{let o=a.next.next;for(;o!==a.prev;){if(a.i!==o.i&&Gm(a,o)){let l=Vd(a,o);a=ls(a,a.next),l=ls(l,l.next),Gr(a,e,t,n,i,s,0),Gr(l,e,t,n,i,s,0);return}o=o.next}a=a.next}while(a!==r)}function Nm(r,e,t,n){const i=[];let s,a,o,l,c;for(s=0,a=e.length;s=t.next.y&&t.next.y!==t.y){const f=t.x+(a-t.y)*(t.next.x-t.x)/(t.next.y-t.y);if(f<=s&&f>n&&(n=f,i=t.x=t.x&&t.x>=l&&s!==t.x&&Hs(ai.x||t.x===i.x&&zm(i,t)))&&(i=t,h=u)),t=t.next;while(t!==o);return i}function zm(r,e){return wt(r.prev,r,e.prev)<0&&wt(e.next,r,r.next)<0}function km(r,e,t,n){let i=r;do i.z===0&&(i.z=pc(i.x,i.y,e,t,n)),i.prevZ=i.prev,i.nextZ=i.next,i=i.next;while(i!==r);i.prevZ.nextZ=null,i.prevZ=null,Vm(i)}function Vm(r){let e,t,n,i,s,a,o,l,c=1;do{for(t=r,r=null,s=null,a=0;t;){for(a++,n=t,o=0,e=0;e0||l>0&&n;)o!==0&&(l===0||!n||t.z<=n.z)?(i=t,t=t.nextZ,o--):(i=n,n=n.nextZ,l--),s?s.nextZ=i:r=i,i.prevZ=s,s=i;t=n}s.nextZ=null,c*=2}while(a>1);return r}function pc(r,e,t,n,i){return r=(r-t)*i|0,e=(e-n)*i|0,r=(r|r<<8)&16711935,r=(r|r<<4)&252645135,r=(r|r<<2)&858993459,r=(r|r<<1)&1431655765,e=(e|e<<8)&16711935,e=(e|e<<4)&252645135,e=(e|e<<2)&858993459,e=(e|e<<1)&1431655765,r|e<<1}function Hm(r){let e=r,t=r;do(e.x=(r-a)*(s-o)&&(r-a)*(n-o)>=(t-a)*(e-o)&&(t-a)*(s-o)>=(i-a)*(n-o)}function Gm(r,e){return r.next.i!==e.i&&r.prev.i!==e.i&&!Wm(r,e)&&(Wr(r,e)&&Wr(e,r)&&Xm(r,e)&&(wt(r.prev,r,e.prev)||wt(r,e.prev,e))||Co(r,e)&&wt(r.prev,r,r.next)>0&&wt(e.prev,e,e.next)>0)}function wt(r,e,t){return(e.y-r.y)*(t.x-e.x)-(e.x-r.x)*(t.y-e.y)}function Co(r,e){return r.x===e.x&&r.y===e.y}function kd(r,e,t,n){const i=Ia(wt(r,e,t)),s=Ia(wt(r,e,n)),a=Ia(wt(t,n,r)),o=Ia(wt(t,n,e));return!!(i!==s&&a!==o||i===0&&Ra(r,t,e)||s===0&&Ra(r,n,e)||a===0&&Ra(t,r,n)||o===0&&Ra(t,e,n))}function Ra(r,e,t){return e.x<=Math.max(r.x,t.x)&&e.x>=Math.min(r.x,t.x)&&e.y<=Math.max(r.y,t.y)&&e.y>=Math.min(r.y,t.y)}function Ia(r){return r>0?1:r<0?-1:0}function Wm(r,e){let t=r;do{if(t.i!==r.i&&t.next.i!==r.i&&t.i!==e.i&&t.next.i!==e.i&&kd(t,t.next,r,e))return!0;t=t.next}while(t!==r);return!1}function Wr(r,e){return wt(r.prev,r,r.next)<0?wt(r,e,r.next)>=0&&wt(r,r.prev,e)>=0:wt(r,e,r.prev)<0||wt(r,r.next,e)<0}function Xm(r,e){let t=r,n=!1;const i=(r.x+e.x)/2,s=(r.y+e.y)/2;do t.y>s!=t.next.y>s&&t.next.y!==t.y&&i<(t.next.x-t.x)*(s-t.y)/(t.next.y-t.y)+t.x&&(n=!n),t=t.next;while(t!==r);return n}function Vd(r,e){const t=new mc(r.i,r.x,r.y),n=new mc(e.i,e.x,e.y),i=r.next,s=e.prev;return r.next=e,e.prev=r,t.next=i,i.prev=t,n.next=t,t.prev=n,s.next=n,n.prev=s,n}function ru(r,e,t,n){const i=new mc(r,e,t);return n?(i.next=n.next,i.prev=n,n.next.prev=i,n.next=i):(i.prev=i,i.next=i),i}function Xr(r){r.next.prev=r.prev,r.prev.next=r.next,r.prevZ&&(r.prevZ.nextZ=r.nextZ),r.nextZ&&(r.nextZ.prevZ=r.prevZ)}function mc(r,e,t){this.i=r,this.x=e,this.y=t,this.prev=null,this.next=null,this.z=0,this.prevZ=null,this.nextZ=null,this.steiner=!1}function qm(r,e,t,n){let i=0;for(let s=e,a=t-n;s2&&r[e-1].equals(r[0])&&r.pop()}function ou(r,e){for(let t=0;tNumber.EPSILON){const k=Math.sqrt(b),X=Math.sqrt(Se*Se+R*R),W=de.x-Ve/k,ce=de.y+me/k,oe=I.x-R/X,ge=I.y+Se/X,Be=((oe-W)*R-(ge-ce)*Se)/(me*R-Ve*Se);De=W+me*Be-te.x,re=ce+Ve*Be-te.y;const $=De*De+re*re;if($<=2)return new j(De,re);Ae=Math.sqrt($/2)}else{let k=!1;me>Number.EPSILON?Se>Number.EPSILON&&(k=!0):me<-Number.EPSILON?Se<-Number.EPSILON&&(k=!0):Math.sign(Ve)===Math.sign(R)&&(k=!0),k?(De=-Ve,re=me,Ae=Math.sqrt(b)):(De=me,re=Ve,Ae=Math.sqrt(b/2))}return new j(De/Ae,re/Ae)}const _e=[];for(let te=0,de=V.length,I=de-1,De=te+1;te=0;te--){const de=te/g,I=d*Math.cos(de*Math.PI/2),De=p*Math.sin(de*Math.PI/2)+_;for(let re=0,Ae=V.length;re=0;){const De=I;let re=I-1;re<0&&(re=te.length-1);for(let Ae=0,me=h+g*2;Ae0)&&d.push(v,x,T),(m!==n-1||l0!=e>0&&this.version++,this._anisotropy=e}get clearcoat(){return this._clearcoat}set clearcoat(e){this._clearcoat>0!=e>0&&this.version++,this._clearcoat=e}get iridescence(){return this._iridescence}set iridescence(e){this._iridescence>0!=e>0&&this.version++,this._iridescence=e}get dispersion(){return this._dispersion}set dispersion(e){this._dispersion>0!=e>0&&this.version++,this._dispersion=e}get sheen(){return this._sheen}set sheen(e){this._sheen>0!=e>0&&this.version++,this._sheen=e}get transmission(){return this._transmission}set transmission(e){this._transmission>0!=e>0&&this.version++,this._transmission=e}copy(e){return super.copy(e),this.defines={STANDARD:"",PHYSICAL:""},this.anisotropy=e.anisotropy,this.anisotropyRotation=e.anisotropyRotation,this.anisotropyMap=e.anisotropyMap,this.clearcoat=e.clearcoat,this.clearcoatMap=e.clearcoatMap,this.clearcoatRoughness=e.clearcoatRoughness,this.clearcoatRoughnessMap=e.clearcoatRoughnessMap,this.clearcoatNormalMap=e.clearcoatNormalMap,this.clearcoatNormalScale.copy(e.clearcoatNormalScale),this.dispersion=e.dispersion,this.ior=e.ior,this.iridescence=e.iridescence,this.iridescenceMap=e.iridescenceMap,this.iridescenceIOR=e.iridescenceIOR,this.iridescenceThicknessRange=[...e.iridescenceThicknessRange],this.iridescenceThicknessMap=e.iridescenceThicknessMap,this.sheen=e.sheen,this.sheenColor.copy(e.sheenColor),this.sheenColorMap=e.sheenColorMap,this.sheenRoughness=e.sheenRoughness,this.sheenRoughnessMap=e.sheenRoughnessMap,this.transmission=e.transmission,this.transmissionMap=e.transmissionMap,this.thickness=e.thickness,this.thicknessMap=e.thicknessMap,this.attenuationDistance=e.attenuationDistance,this.attenuationColor.copy(e.attenuationColor),this.specularIntensity=e.specularIntensity,this.specularIntensityMap=e.specularIntensityMap,this.specularColor.copy(e.specularColor),this.specularColorMap=e.specularColorMap,this}}class eg extends on{constructor(e){super(),this.isMeshPhongMaterial=!0,this.type="MeshPhongMaterial",this.color=new ne(16777215),this.specular=new ne(1118481),this.shininess=30,this.map=null,this.lightMap=null,this.lightMapIntensity=1,this.aoMap=null,this.aoMapIntensity=1,this.emissive=new ne(0),this.emissiveIntensity=1,this.emissiveMap=null,this.bumpMap=null,this.bumpScale=1,this.normalMap=null,this.normalMapType=cs,this.normalScale=new j(1,1),this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.specularMap=null,this.alphaMap=null,this.envMap=null,this.envMapRotation=new In,this.combine=bo,this.reflectivity=1,this.refractionRatio=.98,this.wireframe=!1,this.wireframeLinewidth=1,this.wireframeLinecap="round",this.wireframeLinejoin="round",this.flatShading=!1,this.fog=!0,this.setValues(e)}copy(e){return super.copy(e),this.color.copy(e.color),this.specular.copy(e.specular),this.shininess=e.shininess,this.map=e.map,this.lightMap=e.lightMap,this.lightMapIntensity=e.lightMapIntensity,this.aoMap=e.aoMap,this.aoMapIntensity=e.aoMapIntensity,this.emissive.copy(e.emissive),this.emissiveMap=e.emissiveMap,this.emissiveIntensity=e.emissiveIntensity,this.bumpMap=e.bumpMap,this.bumpScale=e.bumpScale,this.normalMap=e.normalMap,this.normalMapType=e.normalMapType,this.normalScale.copy(e.normalScale),this.displacementMap=e.displacementMap,this.displacementScale=e.displacementScale,this.displacementBias=e.displacementBias,this.specularMap=e.specularMap,this.alphaMap=e.alphaMap,this.envMap=e.envMap,this.envMapRotation.copy(e.envMapRotation),this.combine=e.combine,this.reflectivity=e.reflectivity,this.refractionRatio=e.refractionRatio,this.wireframe=e.wireframe,this.wireframeLinewidth=e.wireframeLinewidth,this.wireframeLinecap=e.wireframeLinecap,this.wireframeLinejoin=e.wireframeLinejoin,this.flatShading=e.flatShading,this.fog=e.fog,this}}class tg extends on{constructor(e){super(),this.isMeshToonMaterial=!0,this.defines={TOON:""},this.type="MeshToonMaterial",this.color=new ne(16777215),this.map=null,this.gradientMap=null,this.lightMap=null,this.lightMapIntensity=1,this.aoMap=null,this.aoMapIntensity=1,this.emissive=new ne(0),this.emissiveIntensity=1,this.emissiveMap=null,this.bumpMap=null,this.bumpScale=1,this.normalMap=null,this.normalMapType=cs,this.normalScale=new j(1,1),this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.alphaMap=null,this.wireframe=!1,this.wireframeLinewidth=1,this.wireframeLinecap="round",this.wireframeLinejoin="round",this.fog=!0,this.setValues(e)}copy(e){return super.copy(e),this.color.copy(e.color),this.map=e.map,this.gradientMap=e.gradientMap,this.lightMap=e.lightMap,this.lightMapIntensity=e.lightMapIntensity,this.aoMap=e.aoMap,this.aoMapIntensity=e.aoMapIntensity,this.emissive.copy(e.emissive),this.emissiveMap=e.emissiveMap,this.emissiveIntensity=e.emissiveIntensity,this.bumpMap=e.bumpMap,this.bumpScale=e.bumpScale,this.normalMap=e.normalMap,this.normalMapType=e.normalMapType,this.normalScale.copy(e.normalScale),this.displacementMap=e.displacementMap,this.displacementScale=e.displacementScale,this.displacementBias=e.displacementBias,this.alphaMap=e.alphaMap,this.wireframe=e.wireframe,this.wireframeLinewidth=e.wireframeLinewidth,this.wireframeLinecap=e.wireframeLinecap,this.wireframeLinejoin=e.wireframeLinejoin,this.fog=e.fog,this}}class ng extends on{constructor(e){super(),this.isMeshNormalMaterial=!0,this.type="MeshNormalMaterial",this.bumpMap=null,this.bumpScale=1,this.normalMap=null,this.normalMapType=cs,this.normalScale=new j(1,1),this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.wireframe=!1,this.wireframeLinewidth=1,this.flatShading=!1,this.setValues(e)}copy(e){return super.copy(e),this.bumpMap=e.bumpMap,this.bumpScale=e.bumpScale,this.normalMap=e.normalMap,this.normalMapType=e.normalMapType,this.normalScale.copy(e.normalScale),this.displacementMap=e.displacementMap,this.displacementScale=e.displacementScale,this.displacementBias=e.displacementBias,this.wireframe=e.wireframe,this.wireframeLinewidth=e.wireframeLinewidth,this.flatShading=e.flatShading,this}}class ig extends on{constructor(e){super(),this.isMeshLambertMaterial=!0,this.type="MeshLambertMaterial",this.color=new ne(16777215),this.map=null,this.lightMap=null,this.lightMapIntensity=1,this.aoMap=null,this.aoMapIntensity=1,this.emissive=new ne(0),this.emissiveIntensity=1,this.emissiveMap=null,this.bumpMap=null,this.bumpScale=1,this.normalMap=null,this.normalMapType=cs,this.normalScale=new j(1,1),this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.specularMap=null,this.alphaMap=null,this.envMap=null,this.envMapRotation=new In,this.combine=bo,this.reflectivity=1,this.refractionRatio=.98,this.wireframe=!1,this.wireframeLinewidth=1,this.wireframeLinecap="round",this.wireframeLinejoin="round",this.flatShading=!1,this.fog=!0,this.setValues(e)}copy(e){return super.copy(e),this.color.copy(e.color),this.map=e.map,this.lightMap=e.lightMap,this.lightMapIntensity=e.lightMapIntensity,this.aoMap=e.aoMap,this.aoMapIntensity=e.aoMapIntensity,this.emissive.copy(e.emissive),this.emissiveMap=e.emissiveMap,this.emissiveIntensity=e.emissiveIntensity,this.bumpMap=e.bumpMap,this.bumpScale=e.bumpScale,this.normalMap=e.normalMap,this.normalMapType=e.normalMapType,this.normalScale.copy(e.normalScale),this.displacementMap=e.displacementMap,this.displacementScale=e.displacementScale,this.displacementBias=e.displacementBias,this.specularMap=e.specularMap,this.alphaMap=e.alphaMap,this.envMap=e.envMap,this.envMapRotation.copy(e.envMapRotation),this.combine=e.combine,this.reflectivity=e.reflectivity,this.refractionRatio=e.refractionRatio,this.wireframe=e.wireframe,this.wireframeLinewidth=e.wireframeLinewidth,this.wireframeLinecap=e.wireframeLinecap,this.wireframeLinejoin=e.wireframeLinejoin,this.flatShading=e.flatShading,this.fog=e.fog,this}}class Hd extends on{constructor(e){super(),this.isMeshDepthMaterial=!0,this.type="MeshDepthMaterial",this.depthPacking=rp,this.map=null,this.alphaMap=null,this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.wireframe=!1,this.wireframeLinewidth=1,this.setValues(e)}copy(e){return super.copy(e),this.depthPacking=e.depthPacking,this.map=e.map,this.alphaMap=e.alphaMap,this.displacementMap=e.displacementMap,this.displacementScale=e.displacementScale,this.displacementBias=e.displacementBias,this.wireframe=e.wireframe,this.wireframeLinewidth=e.wireframeLinewidth,this}}class Gd extends on{constructor(e){super(),this.isMeshDistanceMaterial=!0,this.type="MeshDistanceMaterial",this.map=null,this.alphaMap=null,this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.setValues(e)}copy(e){return super.copy(e),this.map=e.map,this.alphaMap=e.alphaMap,this.displacementMap=e.displacementMap,this.displacementScale=e.displacementScale,this.displacementBias=e.displacementBias,this}}class sg extends on{constructor(e){super(),this.isMeshMatcapMaterial=!0,this.defines={MATCAP:""},this.type="MeshMatcapMaterial",this.color=new ne(16777215),this.matcap=null,this.map=null,this.bumpMap=null,this.bumpScale=1,this.normalMap=null,this.normalMapType=cs,this.normalScale=new j(1,1),this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.alphaMap=null,this.flatShading=!1,this.fog=!0,this.setValues(e)}copy(e){return super.copy(e),this.defines={MATCAP:""},this.color.copy(e.color),this.matcap=e.matcap,this.map=e.map,this.bumpMap=e.bumpMap,this.bumpScale=e.bumpScale,this.normalMap=e.normalMap,this.normalMapType=e.normalMapType,this.normalScale.copy(e.normalScale),this.displacementMap=e.displacementMap,this.displacementScale=e.displacementScale,this.displacementBias=e.displacementBias,this.alphaMap=e.alphaMap,this.flatShading=e.flatShading,this.fog=e.fog,this}}class rg extends qt{constructor(e){super(),this.isLineDashedMaterial=!0,this.type="LineDashedMaterial",this.scale=1,this.dashSize=3,this.gapSize=1,this.setValues(e)}copy(e){return super.copy(e),this.scale=e.scale,this.dashSize=e.dashSize,this.gapSize=e.gapSize,this}}function is(r,e,t){return!r||!t&&r.constructor===e?r:typeof e.BYTES_PER_ELEMENT=="number"?new e(r):Array.prototype.slice.call(r)}function Wd(r){return ArrayBuffer.isView(r)&&!(r instanceof DataView)}function Xd(r){function e(i,s){return r[i]-r[s]}const t=r.length,n=new Array(t);for(let i=0;i!==t;++i)n[i]=i;return n.sort(e),n}function gc(r,e,t){const n=r.length,i=new r.constructor(n);for(let s=0,a=0;a!==n;++s){const o=t[s]*e;for(let l=0;l!==e;++l)i[a++]=r[o+l]}return i}function oh(r,e,t,n){let i=1,s=r[0];for(;s!==void 0&&s[n]===void 0;)s=r[i++];if(s===void 0)return;let a=s[n];if(a!==void 0)if(Array.isArray(a))do a=s[n],a!==void 0&&(e.push(s.time),t.push.apply(t,a)),s=r[i++];while(s!==void 0);else if(a.toArray!==void 0)do a=s[n],a!==void 0&&(e.push(s.time),a.toArray(t,t.length)),s=r[i++];while(s!==void 0);else do a=s[n],a!==void 0&&(e.push(s.time),t.push(a)),s=r[i++];while(s!==void 0)}function ag(r,e,t,n,i=30){const s=r.clone();s.name=e;const a=[];for(let l=0;l=n)){u.push(c.times[d]);for(let _=0;_s.tracks[l].times[0]&&(o=s.tracks[l].times[0]);for(let l=0;l=o.times[p]){const m=p*u+h,y=m+u-h;_=o.values.slice(m,y)}else{const m=o.createInterpolant(),y=h,v=u-h;m.evaluate(s),_=m.resultBuffer.slice(y,v)}l==="quaternion"&&new rn().fromArray(_).normalize().conjugate().toArray(_);const g=c.times.length;for(let m=0;m=s)){const o=t[1];e=s)break t}a=n,n=0;break n}break e}for(;n>>1;et;)--a;if(++a,s!==0||a!==i){s>=a&&(a=Math.max(a,1),s=a-1);const o=this.getValueSize();this.times=n.slice(s,a),this.values=this.values.slice(s*o,a*o)}return this}validate(){let e=!0;const t=this.getValueSize();t-Math.floor(t)!==0&&(console.error("THREE.KeyframeTrack: Invalid value size in track.",this),e=!1);const n=this.times,i=this.values,s=n.length;s===0&&(console.error("THREE.KeyframeTrack: Track is empty.",this),e=!1);let a=null;for(let o=0;o!==s;o++){const l=n[o];if(typeof l=="number"&&isNaN(l)){console.error("THREE.KeyframeTrack: Time is not a valid number.",this,o,l),e=!1;break}if(a!==null&&a>l){console.error("THREE.KeyframeTrack: Out of order keys.",this,o,l,a),e=!1;break}a=l}if(i!==void 0&&Wd(i))for(let o=0,l=i.length;o!==l;++o){const c=i[o];if(isNaN(c)){console.error("THREE.KeyframeTrack: Value is not a valid number.",this,o,c),e=!1;break}}return e}optimize(){const e=this.times.slice(),t=this.values.slice(),n=this.getValueSize(),i=this.getInterpolation()===Fo,s=e.length-1;let a=1;for(let o=1;o0){e[a]=e[s];for(let o=s*n,l=a*n,c=0;c!==n;++c)t[l+c]=t[o+c];++a}return a!==e.length?(this.times=e.slice(0,a),this.values=t.slice(0,a*n)):(this.times=e,this.values=t),this}clone(){const e=this.times.slice(),t=this.values.slice(),n=this.constructor,i=new n(this.name,e,t);return i.createInterpolant=this.createInterpolant,i}}Gn.prototype.TimeBufferType=Float32Array;Gn.prototype.ValueBufferType=Float32Array;Gn.prototype.DefaultInterpolation=dc;class hr extends Gn{constructor(e,t,n){super(e,t,n)}}hr.prototype.ValueTypeName="bool";hr.prototype.ValueBufferType=Array;hr.prototype.DefaultInterpolation=oo;hr.prototype.InterpolantFactoryMethodLinear=void 0;hr.prototype.InterpolantFactoryMethodSmooth=void 0;class Yd extends Gn{}Yd.prototype.ValueTypeName="color";class vo extends Gn{}vo.prototype.ValueTypeName="number";class hg extends Po{constructor(e,t,n,i){super(e,t,n,i)}interpolate_(e,t,n,i){const s=this.resultBuffer,a=this.sampleValues,o=this.valueSize,l=(n-t)/(i-t);let c=e*o;for(let h=c+o;c!==h;c+=4)rn.slerpFlat(s,0,a,c-o,a,c,l);return s}}class Do extends Gn{InterpolantFactoryMethodLinear(e){return new hg(this.times,this.values,this.getValueSize(),e)}}Do.prototype.ValueTypeName="quaternion";Do.prototype.InterpolantFactoryMethodSmooth=void 0;class ur extends Gn{constructor(e,t,n){super(e,t,n)}}ur.prototype.ValueTypeName="string";ur.prototype.ValueBufferType=Array;ur.prototype.DefaultInterpolation=oo;ur.prototype.InterpolantFactoryMethodLinear=void 0;ur.prototype.InterpolantFactoryMethodSmooth=void 0;class xo extends Gn{}xo.prototype.ValueTypeName="vector";class yo{constructor(e="",t=-1,n=[],i=Oc){this.name=e,this.tracks=n,this.duration=t,this.blendMode=i,this.uuid=bn(),this.duration<0&&this.resetDuration()}static parse(e){const t=[],n=e.tracks,i=1/(e.fps||1);for(let a=0,o=n.length;a!==o;++a)t.push(dg(n[a]).scale(i));const s=new this(e.name,e.duration,t,e.blendMode);return s.uuid=e.uuid,s}static toJSON(e){const t=[],n=e.tracks,i={name:e.name,duration:e.duration,tracks:t,uuid:e.uuid,blendMode:e.blendMode};for(let s=0,a=n.length;s!==a;++s)t.push(Gn.toJSON(n[s]));return i}static CreateFromMorphTargetSequence(e,t,n,i){const s=t.length,a=[];for(let o=0;o1){const u=h[1];let f=i[u];f||(i[u]=f=[]),f.push(c)}}const a=[];for(const o in i)a.push(this.CreateFromMorphTargetSequence(o,i[o],t,n));return a}static parseAnimation(e,t){if(!e)return console.error("THREE.AnimationClip: No animation in JSONLoader data."),null;const n=function(u,f,d,p,_){if(d.length!==0){const g=[],m=[];oh(d,g,m,p),g.length!==0&&_.push(new u(f,g,m))}},i=[],s=e.name||"default",a=e.fps||30,o=e.blendMode;let l=e.length||-1;const c=e.hierarchy||[];for(let u=0;u{t&&t(s),this.manager.itemEnd(e)},0),s;if($n[e]!==void 0){$n[e].push({onLoad:t,onProgress:n,onError:i});return}$n[e]=[],$n[e].push({onLoad:t,onProgress:n,onError:i});const a=new Request(e,{headers:new Headers(this.requestHeader),credentials:this.withCredentials?"include":"same-origin"}),o=this.mimeType,l=this.responseType;fetch(a).then(c=>{if(c.status===200||c.status===0){if(c.status===0&&console.warn("THREE.FileLoader: HTTP Status 0 received."),typeof ReadableStream>"u"||c.body===void 0||c.body.getReader===void 0)return c;const h=$n[e],u=c.body.getReader(),f=c.headers.get("X-File-Size")||c.headers.get("Content-Length"),d=f?parseInt(f):0,p=d!==0;let _=0;const g=new ReadableStream({start(m){y();function y(){u.read().then(({done:v,value:x})=>{if(v)m.close();else{_+=x.byteLength;const w=new ProgressEvent("progress",{lengthComputable:p,loaded:_,total:d});for(let T=0,C=h.length;T{m.error(v)})}}});return new Response(g)}else throw new pg(`fetch for "${c.url}" responded with ${c.status}: ${c.statusText}`,c)}).then(c=>{switch(l){case"arraybuffer":return c.arrayBuffer();case"blob":return c.blob();case"document":return c.text().then(h=>new DOMParser().parseFromString(h,o));case"json":return c.json();default:if(o===void 0)return c.text();{const u=/charset="?([^;"\s]*)"?/i.exec(o),f=u&&u[1]?u[1].toLowerCase():void 0,d=new TextDecoder(f);return c.arrayBuffer().then(p=>d.decode(p))}}}).then(c=>{Ei.add(e,c);const h=$n[e];delete $n[e];for(let u=0,f=h.length;u{const h=$n[e];if(h===void 0)throw this.manager.itemError(e),c;delete $n[e];for(let u=0,f=h.length;u{this.manager.itemEnd(e)}),this.manager.itemStart(e)}setResponseType(e){return this.responseType=e,this}setMimeType(e){return this.mimeType=e,this}}class QS extends wn{constructor(e){super(e)}load(e,t,n,i){const s=this,a=new Pi(this.manager);a.setPath(this.path),a.setRequestHeader(this.requestHeader),a.setWithCredentials(this.withCredentials),a.load(e,function(o){try{t(s.parse(JSON.parse(o)))}catch(l){i?i(l):console.error(l),s.manager.itemError(e)}},n,i)}parse(e){const t=[];for(let n=0;n0:i.vertexColors=e.vertexColors),e.uniforms!==void 0)for(const s in e.uniforms){const a=e.uniforms[s];switch(i.uniforms[s]={},a.type){case"t":i.uniforms[s].value=n(a.value);break;case"c":i.uniforms[s].value=new ne().setHex(a.value);break;case"v2":i.uniforms[s].value=new j().fromArray(a.value);break;case"v3":i.uniforms[s].value=new A().fromArray(a.value);break;case"v4":i.uniforms[s].value=new pt().fromArray(a.value);break;case"m3":i.uniforms[s].value=new tt().fromArray(a.value);break;case"m4":i.uniforms[s].value=new Ze().fromArray(a.value);break;default:i.uniforms[s].value=a.value}}if(e.defines!==void 0&&(i.defines=e.defines),e.vertexShader!==void 0&&(i.vertexShader=e.vertexShader),e.fragmentShader!==void 0&&(i.fragmentShader=e.fragmentShader),e.glslVersion!==void 0&&(i.glslVersion=e.glslVersion),e.extensions!==void 0)for(const s in e.extensions)i.extensions[s]=e.extensions[s];if(e.lights!==void 0&&(i.lights=e.lights),e.clipping!==void 0&&(i.clipping=e.clipping),e.size!==void 0&&(i.size=e.size),e.sizeAttenuation!==void 0&&(i.sizeAttenuation=e.sizeAttenuation),e.map!==void 0&&(i.map=n(e.map)),e.matcap!==void 0&&(i.matcap=n(e.matcap)),e.alphaMap!==void 0&&(i.alphaMap=n(e.alphaMap)),e.bumpMap!==void 0&&(i.bumpMap=n(e.bumpMap)),e.bumpScale!==void 0&&(i.bumpScale=e.bumpScale),e.normalMap!==void 0&&(i.normalMap=n(e.normalMap)),e.normalMapType!==void 0&&(i.normalMapType=e.normalMapType),e.normalScale!==void 0){let s=e.normalScale;Array.isArray(s)===!1&&(s=[s,s]),i.normalScale=new j().fromArray(s)}return e.displacementMap!==void 0&&(i.displacementMap=n(e.displacementMap)),e.displacementScale!==void 0&&(i.displacementScale=e.displacementScale),e.displacementBias!==void 0&&(i.displacementBias=e.displacementBias),e.roughnessMap!==void 0&&(i.roughnessMap=n(e.roughnessMap)),e.metalnessMap!==void 0&&(i.metalnessMap=n(e.metalnessMap)),e.emissiveMap!==void 0&&(i.emissiveMap=n(e.emissiveMap)),e.emissiveIntensity!==void 0&&(i.emissiveIntensity=e.emissiveIntensity),e.specularMap!==void 0&&(i.specularMap=n(e.specularMap)),e.specularIntensityMap!==void 0&&(i.specularIntensityMap=n(e.specularIntensityMap)),e.specularColorMap!==void 0&&(i.specularColorMap=n(e.specularColorMap)),e.envMap!==void 0&&(i.envMap=n(e.envMap)),e.envMapRotation!==void 0&&i.envMapRotation.fromArray(e.envMapRotation),e.envMapIntensity!==void 0&&(i.envMapIntensity=e.envMapIntensity),e.reflectivity!==void 0&&(i.reflectivity=e.reflectivity),e.refractionRatio!==void 0&&(i.refractionRatio=e.refractionRatio),e.lightMap!==void 0&&(i.lightMap=n(e.lightMap)),e.lightMapIntensity!==void 0&&(i.lightMapIntensity=e.lightMapIntensity),e.aoMap!==void 0&&(i.aoMap=n(e.aoMap)),e.aoMapIntensity!==void 0&&(i.aoMapIntensity=e.aoMapIntensity),e.gradientMap!==void 0&&(i.gradientMap=n(e.gradientMap)),e.clearcoatMap!==void 0&&(i.clearcoatMap=n(e.clearcoatMap)),e.clearcoatRoughnessMap!==void 0&&(i.clearcoatRoughnessMap=n(e.clearcoatRoughnessMap)),e.clearcoatNormalMap!==void 0&&(i.clearcoatNormalMap=n(e.clearcoatNormalMap)),e.clearcoatNormalScale!==void 0&&(i.clearcoatNormalScale=new j().fromArray(e.clearcoatNormalScale)),e.iridescenceMap!==void 0&&(i.iridescenceMap=n(e.iridescenceMap)),e.iridescenceThicknessMap!==void 0&&(i.iridescenceThicknessMap=n(e.iridescenceThicknessMap)),e.transmissionMap!==void 0&&(i.transmissionMap=n(e.transmissionMap)),e.thicknessMap!==void 0&&(i.thicknessMap=n(e.thicknessMap)),e.anisotropyMap!==void 0&&(i.anisotropyMap=n(e.anisotropyMap)),e.sheenColorMap!==void 0&&(i.sheenColorMap=n(e.sheenColorMap)),e.sheenRoughnessMap!==void 0&&(i.sheenRoughnessMap=n(e.sheenRoughnessMap)),i}setTextures(e){return this.textures=e,this}createMaterialFromType(e){return ch.createMaterialFromType(e)}static createMaterialFromType(e){const t={ShadowMaterial:$m,SpriteMaterial:rs,RawShaderMaterial:jm,ShaderMaterial:Xt,PointsMaterial:Ai,MeshPhysicalMaterial:Qm,MeshStandardMaterial:ah,MeshPhongMaterial:eg,MeshToonMaterial:tg,MeshNormalMaterial:ng,MeshLambertMaterial:ig,MeshDepthMaterial:Hd,MeshDistanceMaterial:Gd,MeshBasicMaterial:kn,MeshMatcapMaterial:sg,LineDashedMaterial:rg,LineBasicMaterial:qt,Material:on};return new t[e]}}class fu{static decodeText(e){if(console.warn("THREE.LoaderUtils: decodeText() has been deprecated with r165 and will be removed with r175. Use TextDecoder instead."),typeof TextDecoder<"u")return new TextDecoder().decode(e);let t="";for(let n=0,i=e.length;n0){const l=new Zd(t);s=new Mo(l),s.setCrossOrigin(this.crossOrigin);for(let c=0,h=e.length;c0){i=new Mo(this.manager),i.setCrossOrigin(this.crossOrigin);for(let a=0,o=e.length;a{const g=new pn;g.min.fromArray(_.boxMin),g.max.fromArray(_.boxMax);const m=new an;return m.radius=_.sphereRadius,m.center.fromArray(_.sphereCenter),{boxInitialized:_.boxInitialized,box:g,sphereInitialized:_.sphereInitialized,sphere:m}}),a._maxInstanceCount=e.maxInstanceCount,a._maxVertexCount=e.maxVertexCount,a._maxIndexCount=e.maxIndexCount,a._geometryInitialized=e.geometryInitialized,a._geometryCount=e.geometryCount,a._matricesTexture=c(e.matricesTexture.uuid),e.colorsTexture!==void 0&&(a._colorsTexture=c(e.colorsTexture.uuid));break;case"LOD":a=new nm;break;case"Line":a=new Vn(o(e.geometry),l(e.material));break;case"LineLoop":a=new gm(o(e.geometry),l(e.material));break;case"LineSegments":a=new hi(o(e.geometry),l(e.material));break;case"PointCloud":case"Points":a=new as(o(e.geometry),l(e.material));break;case"Sprite":a=new ts(l(e.material));break;case"Group":a=new ns;break;case"Bone":a=new Id;break;default:a=new mt}if(a.uuid=e.uuid,e.name!==void 0&&(a.name=e.name),e.matrix!==void 0?(a.matrix.fromArray(e.matrix),e.matrixAutoUpdate!==void 0&&(a.matrixAutoUpdate=e.matrixAutoUpdate),a.matrixAutoUpdate&&a.matrix.decompose(a.position,a.quaternion,a.scale)):(e.position!==void 0&&a.position.fromArray(e.position),e.rotation!==void 0&&a.rotation.fromArray(e.rotation),e.quaternion!==void 0&&a.quaternion.fromArray(e.quaternion),e.scale!==void 0&&a.scale.fromArray(e.scale)),e.up!==void 0&&a.up.fromArray(e.up),e.castShadow!==void 0&&(a.castShadow=e.castShadow),e.receiveShadow!==void 0&&(a.receiveShadow=e.receiveShadow),e.shadow&&(e.shadow.intensity!==void 0&&(a.shadow.intensity=e.shadow.intensity),e.shadow.bias!==void 0&&(a.shadow.bias=e.shadow.bias),e.shadow.normalBias!==void 0&&(a.shadow.normalBias=e.shadow.normalBias),e.shadow.radius!==void 0&&(a.shadow.radius=e.shadow.radius),e.shadow.mapSize!==void 0&&a.shadow.mapSize.fromArray(e.shadow.mapSize),e.shadow.camera!==void 0&&(a.shadow.camera=this.parseObject(e.shadow.camera))),e.visible!==void 0&&(a.visible=e.visible),e.frustumCulled!==void 0&&(a.frustumCulled=e.frustumCulled),e.renderOrder!==void 0&&(a.renderOrder=e.renderOrder),e.userData!==void 0&&(a.userData=e.userData),e.layers!==void 0&&(a.layers.mask=e.layers),e.children!==void 0){const f=e.children;for(let d=0;d"u"&&console.warn("THREE.ImageBitmapLoader: createImageBitmap() not supported."),typeof fetch>"u"&&console.warn("THREE.ImageBitmapLoader: fetch() not supported."),this.options={premultiplyAlpha:"none"}}setOptions(e){return this.options=e,this}load(e,t,n,i){e===void 0&&(e=""),this.path!==void 0&&(e=this.path+e),e=this.manager.resolveURL(e);const s=this,a=Ei.get(e);if(a!==void 0){if(s.manager.itemStart(e),a.then){a.then(c=>{t&&t(c),s.manager.itemEnd(e)}).catch(c=>{i&&i(c)});return}return setTimeout(function(){t&&t(a),s.manager.itemEnd(e)},0),a}const o={};o.credentials=this.crossOrigin==="anonymous"?"same-origin":"include",o.headers=this.requestHeader;const l=fetch(e,o).then(function(c){return c.blob()}).then(function(c){return createImageBitmap(c,Object.assign(s.options,{colorSpaceConversion:"none"}))}).then(function(c){return Ei.add(e,c),t&&t(c),s.manager.itemEnd(e),c}).catch(function(c){i&&i(c),Ei.remove(e),s.manager.itemError(e),s.manager.itemEnd(e)});Ei.add(e,l),s.manager.itemStart(e)}}let Pa;class Kd{static getContext(){return Pa===void 0&&(Pa=new(window.AudioContext||window.webkitAudioContext)),Pa}static setContext(e){Pa=e}}class aw extends wn{constructor(e){super(e)}load(e,t,n,i){const s=this,a=new Pi(this.manager);a.setResponseType("arraybuffer"),a.setPath(this.path),a.setRequestHeader(this.requestHeader),a.setWithCredentials(this.withCredentials),a.load(e,function(l){try{const c=l.slice(0);Kd.getContext().decodeAudioData(c,function(u){t(u)}).catch(o)}catch(c){o(c)}},n,i);function o(l){i?i(l):console.error(l),s.manager.itemError(e)}}}const gu=new Ze,_u=new Ze,Hi=new Ze;class ow{constructor(){this.type="StereoCamera",this.aspect=1,this.eyeSep=.064,this.cameraL=new kt,this.cameraL.layers.enable(1),this.cameraL.matrixAutoUpdate=!1,this.cameraR=new kt,this.cameraR.layers.enable(2),this.cameraR.matrixAutoUpdate=!1,this._cache={focus:null,fov:null,aspect:null,near:null,far:null,zoom:null,eyeSep:null}}update(e){const t=this._cache;if(t.focus!==e.focus||t.fov!==e.fov||t.aspect!==e.aspect*this.aspect||t.near!==e.near||t.far!==e.far||t.zoom!==e.zoom||t.eyeSep!==this.eyeSep){t.focus=e.focus,t.fov=e.fov,t.aspect=e.aspect*this.aspect,t.near=e.near,t.far=e.far,t.zoom=e.zoom,t.eyeSep=this.eyeSep,Hi.copy(e.projectionMatrix);const i=t.eyeSep/2,s=i*t.near/t.focus,a=t.near*Math.tan(ss*t.fov*.5)/t.zoom;let o,l;_u.elements[12]=-i,gu.elements[12]=i,o=-a*t.aspect+s,l=a*t.aspect+s,Hi.elements[0]=2*t.near/(l-o),Hi.elements[8]=(l+o)/(l-o),this.cameraL.projectionMatrix.copy(Hi),o=-a*t.aspect-s,l=a*t.aspect-s,Hi.elements[0]=2*t.near/(l-o),Hi.elements[8]=(l+o)/(l-o),this.cameraR.projectionMatrix.copy(Hi)}this.cameraL.matrixWorld.copy(e.matrixWorld).multiply(_u),this.cameraR.matrixWorld.copy(e.matrixWorld).multiply(gu)}}class Ag extends kt{constructor(e=[]){super(),this.isArrayCamera=!0,this.cameras=e}}class $d{constructor(e=!0){this.autoStart=e,this.startTime=0,this.oldTime=0,this.elapsedTime=0,this.running=!1}start(){this.startTime=vu(),this.oldTime=this.startTime,this.elapsedTime=0,this.running=!0}stop(){this.getElapsedTime(),this.running=!1,this.autoStart=!1}getElapsedTime(){return this.getDelta(),this.elapsedTime}getDelta(){let e=0;if(this.autoStart&&!this.running)return this.start(),0;if(this.running){const t=vu();e=(t-this.oldTime)/1e3,this.oldTime=t,this.elapsedTime+=e}return e}}function vu(){return performance.now()}const Gi=new A,xu=new rn,Cg=new A,Wi=new A;class lw extends mt{constructor(){super(),this.type="AudioListener",this.context=Kd.getContext(),this.gain=this.context.createGain(),this.gain.connect(this.context.destination),this.filter=null,this.timeDelta=0,this._clock=new $d}getInput(){return this.gain}removeFilter(){return this.filter!==null&&(this.gain.disconnect(this.filter),this.filter.disconnect(this.context.destination),this.gain.connect(this.context.destination),this.filter=null),this}getFilter(){return this.filter}setFilter(e){return this.filter!==null?(this.gain.disconnect(this.filter),this.filter.disconnect(this.context.destination)):this.gain.disconnect(this.context.destination),this.filter=e,this.gain.connect(this.filter),this.filter.connect(this.context.destination),this}getMasterVolume(){return this.gain.gain.value}setMasterVolume(e){return this.gain.gain.setTargetAtTime(e,this.context.currentTime,.01),this}updateMatrixWorld(e){super.updateMatrixWorld(e);const t=this.context.listener,n=this.up;if(this.timeDelta=this._clock.getDelta(),this.matrixWorld.decompose(Gi,xu,Cg),Wi.set(0,0,-1).applyQuaternion(xu),t.positionX){const i=this.context.currentTime+this.timeDelta;t.positionX.linearRampToValueAtTime(Gi.x,i),t.positionY.linearRampToValueAtTime(Gi.y,i),t.positionZ.linearRampToValueAtTime(Gi.z,i),t.forwardX.linearRampToValueAtTime(Wi.x,i),t.forwardY.linearRampToValueAtTime(Wi.y,i),t.forwardZ.linearRampToValueAtTime(Wi.z,i),t.upX.linearRampToValueAtTime(n.x,i),t.upY.linearRampToValueAtTime(n.y,i),t.upZ.linearRampToValueAtTime(n.z,i)}else t.setPosition(Gi.x,Gi.y,Gi.z),t.setOrientation(Wi.x,Wi.y,Wi.z,n.x,n.y,n.z)}}class Rg extends mt{constructor(e){super(),this.type="Audio",this.listener=e,this.context=e.context,this.gain=this.context.createGain(),this.gain.connect(e.getInput()),this.autoplay=!1,this.buffer=null,this.detune=0,this.loop=!1,this.loopStart=0,this.loopEnd=0,this.offset=0,this.duration=void 0,this.playbackRate=1,this.isPlaying=!1,this.hasPlaybackControl=!0,this.source=null,this.sourceType="empty",this._startedAt=0,this._progress=0,this._connected=!1,this.filters=[]}getOutput(){return this.gain}setNodeSource(e){return this.hasPlaybackControl=!1,this.sourceType="audioNode",this.source=e,this.connect(),this}setMediaElementSource(e){return this.hasPlaybackControl=!1,this.sourceType="mediaNode",this.source=this.context.createMediaElementSource(e),this.connect(),this}setMediaStreamSource(e){return this.hasPlaybackControl=!1,this.sourceType="mediaStreamNode",this.source=this.context.createMediaStreamSource(e),this.connect(),this}setBuffer(e){return this.buffer=e,this.sourceType="buffer",this.autoplay&&this.play(),this}play(e=0){if(this.isPlaying===!0){console.warn("THREE.Audio: Audio is already playing.");return}if(this.hasPlaybackControl===!1){console.warn("THREE.Audio: this Audio has no playback control.");return}this._startedAt=this.context.currentTime+e;const t=this.context.createBufferSource();return t.buffer=this.buffer,t.loop=this.loop,t.loopStart=this.loopStart,t.loopEnd=this.loopEnd,t.onended=this.onEnded.bind(this),t.start(this._startedAt,this._progress+this.offset,this.duration),this.isPlaying=!0,this.source=t,this.setDetune(this.detune),this.setPlaybackRate(this.playbackRate),this.connect()}pause(){if(this.hasPlaybackControl===!1){console.warn("THREE.Audio: this Audio has no playback control.");return}return this.isPlaying===!0&&(this._progress+=Math.max(this.context.currentTime-this._startedAt,0)*this.playbackRate,this.loop===!0&&(this._progress=this._progress%(this.duration||this.buffer.duration)),this.source.stop(),this.source.onended=null,this.isPlaying=!1),this}stop(e=0){if(this.hasPlaybackControl===!1){console.warn("THREE.Audio: this Audio has no playback control.");return}return this._progress=0,this.source!==null&&(this.source.stop(this.context.currentTime+e),this.source.onended=null),this.isPlaying=!1,this}connect(){if(this.filters.length>0){this.source.connect(this.filters[0]);for(let e=1,t=this.filters.length;e0){this.source.disconnect(this.filters[0]);for(let e=1,t=this.filters.length;e0&&this._mixBufferRegionAdditive(n,i,this._addIndex*t,1,t);for(let l=t,c=t+t;l!==c;++l)if(n[l]!==n[l+t]){o.setValue(n,i);break}}saveOriginalState(){const e=this.binding,t=this.buffer,n=this.valueSize,i=n*this._origIndex;e.getValue(t,i);for(let s=n,a=i;s!==a;++s)t[s]=t[i+s%n];this._setIdentity(),this.cumulativeWeight=0,this.cumulativeWeightAdditive=0}restoreOriginalState(){const e=this.valueSize*3;this.binding.setValue(this.buffer,e)}_setAdditiveIdentityNumeric(){const e=this._addIndex*this.valueSize,t=e+this.valueSize;for(let n=e;n=.5)for(let a=0;a!==s;++a)e[t+a]=e[n+a]}_slerp(e,t,n,i){rn.slerpFlat(e,t,e,t,e,n,i)}_slerpAdditive(e,t,n,i,s){const a=this._workIndex*s;rn.multiplyQuaternionsFlat(e,a,e,t,e,n),rn.slerpFlat(e,t,e,t,e,a,i)}_lerp(e,t,n,i,s){const a=1-i;for(let o=0;o!==s;++o){const l=t+o;e[l]=e[l]*a+e[n+o]*i}}_lerpAdditive(e,t,n,i,s){for(let a=0;a!==s;++a){const o=t+a;e[o]=e[o]+e[n+a]*i}}}const hh="\\[\\]\\.:\\/",Dg=new RegExp("["+hh+"]","g"),uh="[^"+hh+"]",Lg="[^"+hh.replace("\\.","")+"]",Ug=/((?:WC+[\/:])*)/.source.replace("WC",uh),Ng=/(WCOD+)?/.source.replace("WCOD",Lg),Fg=/(?:\.(WC+)(?:\[(.+)\])?)?/.source.replace("WC",uh),Og=/\.(WC+)(?:\[(.+)\])?/.source.replace("WC",uh),Bg=new RegExp("^"+Ug+Ng+Fg+Og+"$"),zg=["material","materials","bones","map"];class kg{constructor(e,t,n){const i=n||ft.parseTrackName(t);this._targetGroup=e,this._bindings=e.subscribe_(t,i)}getValue(e,t){this.bind();const n=this._targetGroup.nCachedObjects_,i=this._bindings[n];i!==void 0&&i.getValue(e,t)}setValue(e,t){const n=this._bindings;for(let i=this._targetGroup.nCachedObjects_,s=n.length;i!==s;++i)n[i].setValue(e,t)}bind(){const e=this._bindings;for(let t=this._targetGroup.nCachedObjects_,n=e.length;t!==n;++t)e[t].bind()}unbind(){const e=this._bindings;for(let t=this._targetGroup.nCachedObjects_,n=e.length;t!==n;++t)e[t].unbind()}}class ft{constructor(e,t,n){this.path=t,this.parsedPath=n||ft.parseTrackName(t),this.node=ft.findNode(e,this.parsedPath.nodeName),this.rootNode=e,this.getValue=this._getValue_unbound,this.setValue=this._setValue_unbound}static create(e,t,n){return e&&e.isAnimationObjectGroup?new ft.Composite(e,t,n):new ft(e,t,n)}static sanitizeNodeName(e){return e.replace(/\s/g,"_").replace(Dg,"")}static parseTrackName(e){const t=Bg.exec(e);if(t===null)throw new Error("PropertyBinding: Cannot parse trackName: "+e);const n={nodeName:t[2],objectName:t[3],objectIndex:t[4],propertyName:t[5],propertyIndex:t[6]},i=n.nodeName&&n.nodeName.lastIndexOf(".");if(i!==void 0&&i!==-1){const s=n.nodeName.substring(i+1);zg.indexOf(s)!==-1&&(n.nodeName=n.nodeName.substring(0,i),n.objectName=s)}if(n.propertyName===null||n.propertyName.length===0)throw new Error("PropertyBinding: can not parse propertyName from trackName: "+e);return n}static findNode(e,t){if(t===void 0||t===""||t==="."||t===-1||t===e.name||t===e.uuid)return e;if(e.skeleton){const n=e.skeleton.getBoneByName(t);if(n!==void 0)return n}if(e.children){const n=function(s){for(let a=0;a=s){const u=s++,f=e[u];t[f.uuid]=h,e[h]=f,t[c]=u,e[u]=l;for(let d=0,p=i;d!==p;++d){const _=n[d],g=_[u],m=_[h];_[h]=g,_[u]=m}}}this.nCachedObjects_=s}uncache(){const e=this._objects,t=this._indicesByUUID,n=this._bindings,i=n.length;let s=this.nCachedObjects_,a=e.length;for(let o=0,l=arguments.length;o!==l;++o){const c=arguments[o],h=c.uuid,u=t[h];if(u!==void 0)if(delete t[h],u0&&(t[d.uuid]=u),e[u]=d,e.pop();for(let p=0,_=i;p!==_;++p){const g=n[p];g[u]=g[f],g.pop()}}}this.nCachedObjects_=s}subscribe_(e,t){const n=this._bindingsIndicesByPath;let i=n[e];const s=this._bindings;if(i!==void 0)return s[i];const a=this._paths,o=this._parsedPaths,l=this._objects,c=l.length,h=this.nCachedObjects_,u=new Array(c);i=s.length,n[e]=i,a.push(e),o.push(t),s.push(u);for(let f=h,d=l.length;f!==d;++f){const p=l[f];u[f]=new ft(p,e,t)}return u}unsubscribe_(e){const t=this._bindingsIndicesByPath,n=t[e];if(n!==void 0){const i=this._paths,s=this._parsedPaths,a=this._bindings,o=a.length-1,l=a[o],c=e[o];t[c]=n,a[n]=l,a.pop(),s[n]=s[o],s.pop(),i[n]=i[o],i.pop()}}}class Vg{constructor(e,t,n=null,i=t.blendMode){this._mixer=e,this._clip=t,this._localRoot=n,this.blendMode=i;const s=t.tracks,a=s.length,o=new Array(a),l={endingStart:Bs,endingEnd:Bs};for(let c=0;c!==a;++c){const h=s[c].createInterpolant(null);o[c]=h,h.settings=l}this._interpolantSettings=l,this._interpolants=o,this._propertyBindings=new Array(a),this._cacheIndex=null,this._byClipCacheIndex=null,this._timeScaleInterpolant=null,this._weightInterpolant=null,this.loop=ip,this._loopCount=-1,this._startTime=null,this.time=0,this.timeScale=1,this._effectiveTimeScale=1,this.weight=1,this._effectiveWeight=1,this.repetitions=1/0,this.paused=!1,this.enabled=!0,this.clampWhenFinished=!1,this.zeroSlopeAtStart=!0,this.zeroSlopeAtEnd=!0}play(){return this._mixer._activateAction(this),this}stop(){return this._mixer._deactivateAction(this),this.reset()}reset(){return this.paused=!1,this.enabled=!0,this.time=0,this._loopCount=-1,this._startTime=null,this.stopFading().stopWarping()}isRunning(){return this.enabled&&!this.paused&&this.timeScale!==0&&this._startTime===null&&this._mixer._isActiveAction(this)}isScheduled(){return this._mixer._isActiveAction(this)}startAt(e){return this._startTime=e,this}setLoop(e,t){return this.loop=e,this.repetitions=t,this}setEffectiveWeight(e){return this.weight=e,this._effectiveWeight=this.enabled?e:0,this.stopFading()}getEffectiveWeight(){return this._effectiveWeight}fadeIn(e){return this._scheduleFading(e,0,1)}fadeOut(e){return this._scheduleFading(e,1,0)}crossFadeFrom(e,t,n){if(e.fadeOut(t),this.fadeIn(t),n){const i=this._clip.duration,s=e._clip.duration,a=s/i,o=i/s;e.warp(1,a,t),this.warp(o,1,t)}return this}crossFadeTo(e,t,n){return e.crossFadeFrom(this,t,n)}stopFading(){const e=this._weightInterpolant;return e!==null&&(this._weightInterpolant=null,this._mixer._takeBackControlInterpolant(e)),this}setEffectiveTimeScale(e){return this.timeScale=e,this._effectiveTimeScale=this.paused?0:e,this.stopWarping()}getEffectiveTimeScale(){return this._effectiveTimeScale}setDuration(e){return this.timeScale=this._clip.duration/e,this.stopWarping()}syncWith(e){return this.time=e.time,this.timeScale=e.timeScale,this.stopWarping()}halt(e){return this.warp(this._effectiveTimeScale,0,e)}warp(e,t,n){const i=this._mixer,s=i.time,a=this.timeScale;let o=this._timeScaleInterpolant;o===null&&(o=i._lendControlInterpolant(),this._timeScaleInterpolant=o);const l=o.parameterPositions,c=o.sampleValues;return l[0]=s,l[1]=s+n,c[0]=e/a,c[1]=t/a,this}stopWarping(){const e=this._timeScaleInterpolant;return e!==null&&(this._timeScaleInterpolant=null,this._mixer._takeBackControlInterpolant(e)),this}getMixer(){return this._mixer}getClip(){return this._clip}getRoot(){return this._localRoot||this._mixer._root}_update(e,t,n,i){if(!this.enabled){this._updateWeight(e);return}const s=this._startTime;if(s!==null){const l=(e-s)*n;l<0||n===0?t=0:(this._startTime=null,t=n*l)}t*=this._updateTimeScale(e);const a=this._updateTime(t),o=this._updateWeight(e);if(o>0){const l=this._interpolants,c=this._propertyBindings;switch(this.blendMode){case yd:for(let h=0,u=l.length;h!==u;++h)l[h].evaluate(a),c[h].accumulateAdditive(o);break;case Oc:default:for(let h=0,u=l.length;h!==u;++h)l[h].evaluate(a),c[h].accumulate(i,o)}}}_updateWeight(e){let t=0;if(this.enabled){t=this.weight;const n=this._weightInterpolant;if(n!==null){const i=n.evaluate(e)[0];t*=i,e>n.parameterPositions[1]&&(this.stopFading(),i===0&&(this.enabled=!1))}}return this._effectiveWeight=t,t}_updateTimeScale(e){let t=0;if(!this.paused){t=this.timeScale;const n=this._timeScaleInterpolant;if(n!==null){const i=n.evaluate(e)[0];t*=i,e>n.parameterPositions[1]&&(this.stopWarping(),t===0?this.paused=!0:this.timeScale=t)}}return this._effectiveTimeScale=t,t}_updateTime(e){const t=this._clip.duration,n=this.loop;let i=this.time+e,s=this._loopCount;const a=n===sp;if(e===0)return s===-1?i:a&&(s&1)===1?t-i:i;if(n===np){s===-1&&(this._loopCount=0,this._setEndings(!0,!0,!1));e:{if(i>=t)i=t;else if(i<0)i=0;else{this.time=i;break e}this.clampWhenFinished?this.paused=!0:this.enabled=!1,this.time=i,this._mixer.dispatchEvent({type:"finished",action:this,direction:e<0?-1:1})}}else{if(s===-1&&(e>=0?(s=0,this._setEndings(!0,this.repetitions===0,a)):this._setEndings(this.repetitions===0,!0,a)),i>=t||i<0){const o=Math.floor(i/t);i-=t*o,s+=Math.abs(o);const l=this.repetitions-s;if(l<=0)this.clampWhenFinished?this.paused=!0:this.enabled=!1,i=e>0?t:0,this.time=i,this._mixer.dispatchEvent({type:"finished",action:this,direction:e>0?1:-1});else{if(l===1){const c=e<0;this._setEndings(c,!c,a)}else this._setEndings(!1,!1,a);this._loopCount=s,this.time=i,this._mixer.dispatchEvent({type:"loop",action:this,loopDelta:o})}}else this.time=i;if(a&&(s&1)===1)return t-i}return i}_setEndings(e,t,n){const i=this._interpolantSettings;n?(i.endingStart=zs,i.endingEnd=zs):(e?i.endingStart=this.zeroSlopeAtStart?zs:Bs:i.endingStart=lo,t?i.endingEnd=this.zeroSlopeAtEnd?zs:Bs:i.endingEnd=lo)}_scheduleFading(e,t,n){const i=this._mixer,s=i.time;let a=this._weightInterpolant;a===null&&(a=i._lendControlInterpolant(),this._weightInterpolant=a);const o=a.parameterPositions,l=a.sampleValues;return o[0]=s,l[0]=t,o[1]=s+e,l[1]=n,this}}const Hg=new Float32Array(1);class dw extends ci{constructor(e){super(),this._root=e,this._initMemoryManager(),this._accuIndex=0,this.time=0,this.timeScale=1}_bindAction(e,t){const n=e._localRoot||this._root,i=e._clip.tracks,s=i.length,a=e._propertyBindings,o=e._interpolants,l=n.uuid,c=this._bindingsByRootAndName;let h=c[l];h===void 0&&(h={},c[l]=h);for(let u=0;u!==s;++u){const f=i[u],d=f.name;let p=h[d];if(p!==void 0)++p.referenceCount,a[u]=p;else{if(p=a[u],p!==void 0){p._cacheIndex===null&&(++p.referenceCount,this._addInactiveBinding(p,l,d));continue}const _=t&&t._propertyBindings[u].binding.parsedPath;p=new Pg(ft.create(n,d,_),f.ValueTypeName,f.getValueSize()),++p.referenceCount,this._addInactiveBinding(p,l,d),a[u]=p}o[u].resultBuffer=p.buffer}}_activateAction(e){if(!this._isActiveAction(e)){if(e._cacheIndex===null){const n=(e._localRoot||this._root).uuid,i=e._clip.uuid,s=this._actionsByClip[i];this._bindAction(e,s&&s.knownActions[0]),this._addInactiveAction(e,i,n)}const t=e._propertyBindings;for(let n=0,i=t.length;n!==i;++n){const s=t[n];s.useCount++===0&&(this._lendBinding(s),s.saveOriginalState())}this._lendAction(e)}}_deactivateAction(e){if(this._isActiveAction(e)){const t=e._propertyBindings;for(let n=0,i=t.length;n!==i;++n){const s=t[n];--s.useCount===0&&(s.restoreOriginalState(),this._takeBackBinding(s))}this._takeBackAction(e)}}_initMemoryManager(){this._actions=[],this._nActiveActions=0,this._actionsByClip={},this._bindings=[],this._nActiveBindings=0,this._bindingsByRootAndName={},this._controlInterpolants=[],this._nActiveControlInterpolants=0;const e=this;this.stats={actions:{get total(){return e._actions.length},get inUse(){return e._nActiveActions}},bindings:{get total(){return e._bindings.length},get inUse(){return e._nActiveBindings}},controlInterpolants:{get total(){return e._controlInterpolants.length},get inUse(){return e._nActiveControlInterpolants}}}}_isActiveAction(e){const t=e._cacheIndex;return t!==null&&t=0;--n)e[n].stop();return this}update(e){e*=this.timeScale;const t=this._actions,n=this._nActiveActions,i=this.time+=e,s=Math.sign(e),a=this._accuIndex^=1;for(let c=0;c!==n;++c)t[c]._update(i,e,s,a);const o=this._bindings,l=this._nActiveBindings;for(let c=0;c!==l;++c)o[c].apply(a);return this}setTime(e){this.time=0;for(let t=0;t=this.min.x&&e.x<=this.max.x&&e.y>=this.min.y&&e.y<=this.max.y}containsBox(e){return this.min.x<=e.min.x&&e.max.x<=this.max.x&&this.min.y<=e.min.y&&e.max.y<=this.max.y}getParameter(e,t){return t.set((e.x-this.min.x)/(this.max.x-this.min.x),(e.y-this.min.y)/(this.max.y-this.min.y))}intersectsBox(e){return e.max.x>=this.min.x&&e.min.x<=this.max.x&&e.max.y>=this.min.y&&e.min.y<=this.max.y}clampPoint(e,t){return t.copy(e).clamp(this.min,this.max)}distanceToPoint(e){return this.clampPoint(e,Su).distanceTo(e)}intersect(e){return this.min.max(e.min),this.max.min(e.max),this.isEmpty()&&this.makeEmpty(),this}union(e){return this.min.min(e.min),this.max.max(e.max),this}translate(e){return this.min.add(e),this.max.add(e),this}equals(e){return e.min.equals(this.min)&&e.max.equals(this.max)}}const wu=new A,Da=new A;class yw{constructor(e=new A,t=new A){this.start=e,this.end=t}set(e,t){return this.start.copy(e),this.end.copy(t),this}copy(e){return this.start.copy(e.start),this.end.copy(e.end),this}getCenter(e){return e.addVectors(this.start,this.end).multiplyScalar(.5)}delta(e){return e.subVectors(this.end,this.start)}distanceSq(){return this.start.distanceToSquared(this.end)}distance(){return this.start.distanceTo(this.end)}at(e,t){return this.delta(t).multiplyScalar(e).add(this.start)}closestPointToPointParameter(e,t){wu.subVectors(e,this.start),Da.subVectors(this.end,this.start);const n=Da.dot(Da);let s=Da.dot(wu)/n;return t&&(s=et(s,0,1)),s}closestPointToPoint(e,t,n){const i=this.closestPointToPointParameter(e,t);return this.delta(n).multiplyScalar(i).add(this.start)}applyMatrix4(e){return this.start.applyMatrix4(e),this.end.applyMatrix4(e),this}equals(e){return e.start.equals(this.start)&&e.end.equals(this.end)}clone(){return new this.constructor().copy(this)}}const Eu=new A;class Mw extends mt{constructor(e,t){super(),this.light=e,this.matrixAutoUpdate=!1,this.color=t,this.type="SpotLightHelper";const n=new qe,i=[0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,-1,0,1,0,0,0,0,1,1,0,0,0,0,-1,1];for(let a=0,o=1,l=32;a1)for(let u=0;u.99999)this.quaternion.set(0,0,0,1);else if(e.y<-.99999)this.quaternion.set(1,0,0,0);else{Iu.set(e.z,0,-e.x).normalize();const t=Math.acos(e.y);this.quaternion.setFromAxisAngle(Iu,t)}}setLength(e,t=e*.2,n=t*.2){this.line.scale.set(1,Math.max(1e-4,e-t),1),this.line.updateMatrix(),this.cone.scale.set(n,t,n),this.cone.position.y=e,this.cone.updateMatrix()}setColor(e){this.line.material.color.set(e),this.cone.material.color.set(e)}copy(e){return super.copy(e,!1),this.line.copy(e.line),this.cone.copy(e.cone),this}dispose(){this.line.geometry.dispose(),this.line.material.dispose(),this.cone.geometry.dispose(),this.cone.material.dispose()}}class Lw extends hi{constructor(e=1){const t=[0,0,0,e,0,0,0,0,0,0,e,0,0,0,0,0,0,e],n=[1,0,0,1,.6,0,0,1,0,.6,1,0,0,0,1,0,.6,1],i=new qe;i.setAttribute("position",new Fe(t,3)),i.setAttribute("color",new Fe(n,3));const s=new qt({vertexColors:!0,toneMapped:!1});super(i,s),this.type="AxesHelper"}setColors(e,t,n){const i=new ne,s=this.geometry.attributes.color.array;return i.set(e),i.toArray(s,0),i.toArray(s,3),i.set(t),i.toArray(s,6),i.toArray(s,9),i.set(n),i.toArray(s,12),i.toArray(s,15),this.geometry.attributes.color.needsUpdate=!0,this}dispose(){this.geometry.dispose(),this.material.dispose()}}class Uw{constructor(){this.type="ShapePath",this.color=new ne,this.subPaths=[],this.currentPath=null}moveTo(e,t){return this.currentPath=new _o,this.subPaths.push(this.currentPath),this.currentPath.moveTo(e,t),this}lineTo(e,t){return this.currentPath.lineTo(e,t),this}quadraticCurveTo(e,t,n,i){return this.currentPath.quadraticCurveTo(e,t,n,i),this}bezierCurveTo(e,t,n,i,s,a){return this.currentPath.bezierCurveTo(e,t,n,i,s,a),this}splineThru(e){return this.currentPath.splineThru(e),this}toShapes(e){function t(m){const y=[];for(let v=0,x=m.length;vNumber.EPSILON){if(M<0&&(C=y[T],S=-S,P=y[w],M=-M),m.yP.y)continue;if(m.y===C.y){if(m.x===C.x)return!0}else{const D=M*(m.x-C.x)-S*(m.y-C.y);if(D===0)return!0;if(D<0)continue;x=!x}}else{if(m.y!==C.y)continue;if(P.x<=m.x&&m.x<=C.x||C.x<=m.x&&m.x<=P.x)return!0}}return x}const i=oi.isClockWise,s=this.subPaths;if(s.length===0)return[];let a,o,l;const c=[];if(s.length===1)return o=s[0],l=new Js,l.curves=o.curves,c.push(l),c;let h=!i(s[0].getPoints());h=e?!h:h;const u=[],f=[];let d=[],p=0,_;f[p]=void 0,d[p]=[];for(let m=0,y=s.length;m1){let m=!1,y=0;for(let v=0,x=f.length;v0&&m===!1&&(d=u)}let g;for(let m=0,y=f.length;me?(r.repeat.x=1,r.repeat.y=t/e,r.offset.x=0,r.offset.y=(1-r.repeat.y)/2):(r.repeat.x=e/t,r.repeat.y=1,r.offset.x=(1-r.repeat.x)/2,r.offset.y=0),r}function Zg(r,e){const t=r.image&&r.image.width?r.image.width/r.image.height:1;return t>e?(r.repeat.x=e/t,r.repeat.y=1,r.offset.x=(1-r.repeat.x)/2,r.offset.y=0):(r.repeat.x=1,r.repeat.y=t/e,r.offset.x=0,r.offset.y=(1-r.repeat.y)/2),r}function Jg(r){return r.repeat.x=1,r.repeat.y=1,r.offset.x=0,r.offset.y=0,r}function yc(r,e,t,n){const i=Kg(n);switch(t){case pd:return r*e;case gd:return r*e;case _d:return r*e*2;case Uc:return r*e/i.components*i.byteLength;case So:return r*e/i.components*i.byteLength;case vd:return r*e*2/i.components*i.byteLength;case Nc:return r*e*2/i.components*i.byteLength;case md:return r*e*3/i.components*i.byteLength;case dn:return r*e*4/i.components*i.byteLength;case Fc:return r*e*4/i.components*i.byteLength;case qa:case Ya:return Math.floor((r+3)/4)*Math.floor((e+3)/4)*8;case Za:case Ja:return Math.floor((r+3)/4)*Math.floor((e+3)/4)*16;case Vl:case Gl:return Math.max(r,16)*Math.max(e,8)/4;case kl:case Hl:return Math.max(r,8)*Math.max(e,8)/2;case Wl:case Xl:return Math.floor((r+3)/4)*Math.floor((e+3)/4)*8;case ql:return Math.floor((r+3)/4)*Math.floor((e+3)/4)*16;case Yl:return Math.floor((r+3)/4)*Math.floor((e+3)/4)*16;case Zl:return Math.floor((r+4)/5)*Math.floor((e+3)/4)*16;case Jl:return Math.floor((r+4)/5)*Math.floor((e+4)/5)*16;case Kl:return Math.floor((r+5)/6)*Math.floor((e+4)/5)*16;case $l:return Math.floor((r+5)/6)*Math.floor((e+5)/6)*16;case jl:return Math.floor((r+7)/8)*Math.floor((e+4)/5)*16;case Ql:return Math.floor((r+7)/8)*Math.floor((e+5)/6)*16;case ec:return Math.floor((r+7)/8)*Math.floor((e+7)/8)*16;case tc:return Math.floor((r+9)/10)*Math.floor((e+4)/5)*16;case nc:return Math.floor((r+9)/10)*Math.floor((e+5)/6)*16;case ic:return Math.floor((r+9)/10)*Math.floor((e+7)/8)*16;case sc:return Math.floor((r+9)/10)*Math.floor((e+9)/10)*16;case rc:return Math.floor((r+11)/12)*Math.floor((e+9)/10)*16;case ac:return Math.floor((r+11)/12)*Math.floor((e+11)/12)*16;case Ka:case oc:case lc:return Math.ceil(r/4)*Math.ceil(e/4)*16;case xd:case cc:return Math.ceil(r/4)*Math.ceil(e/4)*8;case hc:case uc:return Math.ceil(r/4)*Math.ceil(e/4)*16}throw new Error(`Unable to determine texture byte length for ${t} format.`)}function Kg(r){switch(r){case li:case ud:return{byteLength:1,components:1};case kr:case dd:case si:return{byteLength:2,components:1};case Dc:case Lc:return{byteLength:2,components:4};case Ii:case Pc:case Mn:return{byteLength:4,components:1};case fd:return{byteLength:4,components:3}}throw new Error(`Unknown texture type ${r}.`)}const Nw={contain:Yg,cover:Zg,fill:Jg,getByteLength:yc};typeof __THREE_DEVTOOLS__<"u"&&__THREE_DEVTOOLS__.dispatchEvent(new CustomEvent("register",{detail:{revision:Rc}}));typeof window<"u"&&(window.__THREE__?console.warn("WARNING: Multiple instances of Three.js being imported."):window.__THREE__=Rc);/** - * @license - * Copyright 2010-2024 Three.js Authors - * SPDX-License-Identifier: MIT - */function tf(){let r=null,e=!1,t=null,n=null;function i(s,a){t(s,a),n=r.requestAnimationFrame(i)}return{start:function(){e!==!0&&t!==null&&(n=r.requestAnimationFrame(i),e=!0)},stop:function(){r.cancelAnimationFrame(n),e=!1},setAnimationLoop:function(s){t=s},setContext:function(s){r=s}}}function $g(r){const e=new WeakMap;function t(o,l){const c=o.array,h=o.usage,u=c.byteLength,f=r.createBuffer();r.bindBuffer(l,f),r.bufferData(l,c,h),o.onUploadCallback();let d;if(c instanceof Float32Array)d=r.FLOAT;else if(c instanceof Uint16Array)o.isFloat16BufferAttribute?d=r.HALF_FLOAT:d=r.UNSIGNED_SHORT;else if(c instanceof Int16Array)d=r.SHORT;else if(c instanceof Uint32Array)d=r.UNSIGNED_INT;else if(c instanceof Int32Array)d=r.INT;else if(c instanceof Int8Array)d=r.BYTE;else if(c instanceof Uint8Array)d=r.UNSIGNED_BYTE;else if(c instanceof Uint8ClampedArray)d=r.UNSIGNED_BYTE;else throw new Error("THREE.WebGLAttributes: Unsupported buffer data format: "+c);return{buffer:f,type:d,bytesPerElement:c.BYTES_PER_ELEMENT,version:o.version,size:u}}function n(o,l,c){const h=l.array,u=l.updateRanges;if(r.bindBuffer(c,o),u.length===0)r.bufferSubData(c,0,h);else{u.sort((d,p)=>d.start-p.start);let f=0;for(let d=1;d 0 - vec4 plane; - #ifdef ALPHA_TO_COVERAGE - float distanceToPlane, distanceGradient; - float clipOpacity = 1.0; - #pragma unroll_loop_start - for ( int i = 0; i < UNION_CLIPPING_PLANES; i ++ ) { - plane = clippingPlanes[ i ]; - distanceToPlane = - dot( vClipPosition, plane.xyz ) + plane.w; - distanceGradient = fwidth( distanceToPlane ) / 2.0; - clipOpacity *= smoothstep( - distanceGradient, distanceGradient, distanceToPlane ); - if ( clipOpacity == 0.0 ) discard; - } - #pragma unroll_loop_end - #if UNION_CLIPPING_PLANES < NUM_CLIPPING_PLANES - float unionClipOpacity = 1.0; - #pragma unroll_loop_start - for ( int i = UNION_CLIPPING_PLANES; i < NUM_CLIPPING_PLANES; i ++ ) { - plane = clippingPlanes[ i ]; - distanceToPlane = - dot( vClipPosition, plane.xyz ) + plane.w; - distanceGradient = fwidth( distanceToPlane ) / 2.0; - unionClipOpacity *= 1.0 - smoothstep( - distanceGradient, distanceGradient, distanceToPlane ); - } - #pragma unroll_loop_end - clipOpacity *= 1.0 - unionClipOpacity; - #endif - diffuseColor.a *= clipOpacity; - if ( diffuseColor.a == 0.0 ) discard; - #else - #pragma unroll_loop_start - for ( int i = 0; i < UNION_CLIPPING_PLANES; i ++ ) { - plane = clippingPlanes[ i ]; - if ( dot( vClipPosition, plane.xyz ) > plane.w ) discard; - } - #pragma unroll_loop_end - #if UNION_CLIPPING_PLANES < NUM_CLIPPING_PLANES - bool clipped = true; - #pragma unroll_loop_start - for ( int i = UNION_CLIPPING_PLANES; i < NUM_CLIPPING_PLANES; i ++ ) { - plane = clippingPlanes[ i ]; - clipped = ( dot( vClipPosition, plane.xyz ) > plane.w ) && clipped; - } - #pragma unroll_loop_end - if ( clipped ) discard; - #endif - #endif -#endif`,p0=`#if NUM_CLIPPING_PLANES > 0 - varying vec3 vClipPosition; - uniform vec4 clippingPlanes[ NUM_CLIPPING_PLANES ]; -#endif`,m0=`#if NUM_CLIPPING_PLANES > 0 - varying vec3 vClipPosition; -#endif`,g0=`#if NUM_CLIPPING_PLANES > 0 - vClipPosition = - mvPosition.xyz; -#endif`,_0=`#if defined( USE_COLOR_ALPHA ) - diffuseColor *= vColor; -#elif defined( USE_COLOR ) - diffuseColor.rgb *= vColor; -#endif`,v0=`#if defined( USE_COLOR_ALPHA ) - varying vec4 vColor; -#elif defined( USE_COLOR ) - varying vec3 vColor; -#endif`,x0=`#if defined( USE_COLOR_ALPHA ) - varying vec4 vColor; -#elif defined( USE_COLOR ) || defined( USE_INSTANCING_COLOR ) || defined( USE_BATCHING_COLOR ) - varying vec3 vColor; -#endif`,y0=`#if defined( USE_COLOR_ALPHA ) - vColor = vec4( 1.0 ); -#elif defined( USE_COLOR ) || defined( USE_INSTANCING_COLOR ) || defined( USE_BATCHING_COLOR ) - vColor = vec3( 1.0 ); -#endif -#ifdef USE_COLOR - vColor *= color; -#endif -#ifdef USE_INSTANCING_COLOR - vColor.xyz *= instanceColor.xyz; -#endif -#ifdef USE_BATCHING_COLOR - vec3 batchingColor = getBatchingColor( getIndirectIndex( gl_DrawID ) ); - vColor.xyz *= batchingColor.xyz; -#endif`,M0=`#define PI 3.141592653589793 -#define PI2 6.283185307179586 -#define PI_HALF 1.5707963267948966 -#define RECIPROCAL_PI 0.3183098861837907 -#define RECIPROCAL_PI2 0.15915494309189535 -#define EPSILON 1e-6 -#ifndef saturate -#define saturate( a ) clamp( a, 0.0, 1.0 ) -#endif -#define whiteComplement( a ) ( 1.0 - saturate( a ) ) -float pow2( const in float x ) { return x*x; } -vec3 pow2( const in vec3 x ) { return x*x; } -float pow3( const in float x ) { return x*x*x; } -float pow4( const in float x ) { float x2 = x*x; return x2*x2; } -float max3( const in vec3 v ) { return max( max( v.x, v.y ), v.z ); } -float average( const in vec3 v ) { return dot( v, vec3( 0.3333333 ) ); } -highp float rand( const in vec2 uv ) { - const highp float a = 12.9898, b = 78.233, c = 43758.5453; - highp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI ); - return fract( sin( sn ) * c ); -} -#ifdef HIGH_PRECISION - float precisionSafeLength( vec3 v ) { return length( v ); } -#else - float precisionSafeLength( vec3 v ) { - float maxComponent = max3( abs( v ) ); - return length( v / maxComponent ) * maxComponent; - } -#endif -struct IncidentLight { - vec3 color; - vec3 direction; - bool visible; -}; -struct ReflectedLight { - vec3 directDiffuse; - vec3 directSpecular; - vec3 indirectDiffuse; - vec3 indirectSpecular; -}; -#ifdef USE_ALPHAHASH - varying vec3 vPosition; -#endif -vec3 transformDirection( in vec3 dir, in mat4 matrix ) { - return normalize( ( matrix * vec4( dir, 0.0 ) ).xyz ); -} -vec3 inverseTransformDirection( in vec3 dir, in mat4 matrix ) { - return normalize( ( vec4( dir, 0.0 ) * matrix ).xyz ); -} -mat3 transposeMat3( const in mat3 m ) { - mat3 tmp; - tmp[ 0 ] = vec3( m[ 0 ].x, m[ 1 ].x, m[ 2 ].x ); - tmp[ 1 ] = vec3( m[ 0 ].y, m[ 1 ].y, m[ 2 ].y ); - tmp[ 2 ] = vec3( m[ 0 ].z, m[ 1 ].z, m[ 2 ].z ); - return tmp; -} -bool isPerspectiveMatrix( mat4 m ) { - return m[ 2 ][ 3 ] == - 1.0; -} -vec2 equirectUv( in vec3 dir ) { - float u = atan( dir.z, dir.x ) * RECIPROCAL_PI2 + 0.5; - float v = asin( clamp( dir.y, - 1.0, 1.0 ) ) * RECIPROCAL_PI + 0.5; - return vec2( u, v ); -} -vec3 BRDF_Lambert( const in vec3 diffuseColor ) { - return RECIPROCAL_PI * diffuseColor; -} -vec3 F_Schlick( const in vec3 f0, const in float f90, const in float dotVH ) { - float fresnel = exp2( ( - 5.55473 * dotVH - 6.98316 ) * dotVH ); - return f0 * ( 1.0 - fresnel ) + ( f90 * fresnel ); -} -float F_Schlick( const in float f0, const in float f90, const in float dotVH ) { - float fresnel = exp2( ( - 5.55473 * dotVH - 6.98316 ) * dotVH ); - return f0 * ( 1.0 - fresnel ) + ( f90 * fresnel ); -} // validated`,b0=`#ifdef ENVMAP_TYPE_CUBE_UV - #define cubeUV_minMipLevel 4.0 - #define cubeUV_minTileSize 16.0 - float getFace( vec3 direction ) { - vec3 absDirection = abs( direction ); - float face = - 1.0; - if ( absDirection.x > absDirection.z ) { - if ( absDirection.x > absDirection.y ) - face = direction.x > 0.0 ? 0.0 : 3.0; - else - face = direction.y > 0.0 ? 1.0 : 4.0; - } else { - if ( absDirection.z > absDirection.y ) - face = direction.z > 0.0 ? 2.0 : 5.0; - else - face = direction.y > 0.0 ? 1.0 : 4.0; - } - return face; - } - vec2 getUV( vec3 direction, float face ) { - vec2 uv; - if ( face == 0.0 ) { - uv = vec2( direction.z, direction.y ) / abs( direction.x ); - } else if ( face == 1.0 ) { - uv = vec2( - direction.x, - direction.z ) / abs( direction.y ); - } else if ( face == 2.0 ) { - uv = vec2( - direction.x, direction.y ) / abs( direction.z ); - } else if ( face == 3.0 ) { - uv = vec2( - direction.z, direction.y ) / abs( direction.x ); - } else if ( face == 4.0 ) { - uv = vec2( - direction.x, direction.z ) / abs( direction.y ); - } else { - uv = vec2( direction.x, direction.y ) / abs( direction.z ); - } - return 0.5 * ( uv + 1.0 ); - } - vec3 bilinearCubeUV( sampler2D envMap, vec3 direction, float mipInt ) { - float face = getFace( direction ); - float filterInt = max( cubeUV_minMipLevel - mipInt, 0.0 ); - mipInt = max( mipInt, cubeUV_minMipLevel ); - float faceSize = exp2( mipInt ); - highp vec2 uv = getUV( direction, face ) * ( faceSize - 2.0 ) + 1.0; - if ( face > 2.0 ) { - uv.y += faceSize; - face -= 3.0; - } - uv.x += face * faceSize; - uv.x += filterInt * 3.0 * cubeUV_minTileSize; - uv.y += 4.0 * ( exp2( CUBEUV_MAX_MIP ) - faceSize ); - uv.x *= CUBEUV_TEXEL_WIDTH; - uv.y *= CUBEUV_TEXEL_HEIGHT; - #ifdef texture2DGradEXT - return texture2DGradEXT( envMap, uv, vec2( 0.0 ), vec2( 0.0 ) ).rgb; - #else - return texture2D( envMap, uv ).rgb; - #endif - } - #define cubeUV_r0 1.0 - #define cubeUV_m0 - 2.0 - #define cubeUV_r1 0.8 - #define cubeUV_m1 - 1.0 - #define cubeUV_r4 0.4 - #define cubeUV_m4 2.0 - #define cubeUV_r5 0.305 - #define cubeUV_m5 3.0 - #define cubeUV_r6 0.21 - #define cubeUV_m6 4.0 - float roughnessToMip( float roughness ) { - float mip = 0.0; - if ( roughness >= cubeUV_r1 ) { - mip = ( cubeUV_r0 - roughness ) * ( cubeUV_m1 - cubeUV_m0 ) / ( cubeUV_r0 - cubeUV_r1 ) + cubeUV_m0; - } else if ( roughness >= cubeUV_r4 ) { - mip = ( cubeUV_r1 - roughness ) * ( cubeUV_m4 - cubeUV_m1 ) / ( cubeUV_r1 - cubeUV_r4 ) + cubeUV_m1; - } else if ( roughness >= cubeUV_r5 ) { - mip = ( cubeUV_r4 - roughness ) * ( cubeUV_m5 - cubeUV_m4 ) / ( cubeUV_r4 - cubeUV_r5 ) + cubeUV_m4; - } else if ( roughness >= cubeUV_r6 ) { - mip = ( cubeUV_r5 - roughness ) * ( cubeUV_m6 - cubeUV_m5 ) / ( cubeUV_r5 - cubeUV_r6 ) + cubeUV_m5; - } else { - mip = - 2.0 * log2( 1.16 * roughness ); } - return mip; - } - vec4 textureCubeUV( sampler2D envMap, vec3 sampleDir, float roughness ) { - float mip = clamp( roughnessToMip( roughness ), cubeUV_m0, CUBEUV_MAX_MIP ); - float mipF = fract( mip ); - float mipInt = floor( mip ); - vec3 color0 = bilinearCubeUV( envMap, sampleDir, mipInt ); - if ( mipF == 0.0 ) { - return vec4( color0, 1.0 ); - } else { - vec3 color1 = bilinearCubeUV( envMap, sampleDir, mipInt + 1.0 ); - return vec4( mix( color0, color1, mipF ), 1.0 ); - } - } -#endif`,S0=`vec3 transformedNormal = objectNormal; -#ifdef USE_TANGENT - vec3 transformedTangent = objectTangent; -#endif -#ifdef USE_BATCHING - mat3 bm = mat3( batchingMatrix ); - transformedNormal /= vec3( dot( bm[ 0 ], bm[ 0 ] ), dot( bm[ 1 ], bm[ 1 ] ), dot( bm[ 2 ], bm[ 2 ] ) ); - transformedNormal = bm * transformedNormal; - #ifdef USE_TANGENT - transformedTangent = bm * transformedTangent; - #endif -#endif -#ifdef USE_INSTANCING - mat3 im = mat3( instanceMatrix ); - transformedNormal /= vec3( dot( im[ 0 ], im[ 0 ] ), dot( im[ 1 ], im[ 1 ] ), dot( im[ 2 ], im[ 2 ] ) ); - transformedNormal = im * transformedNormal; - #ifdef USE_TANGENT - transformedTangent = im * transformedTangent; - #endif -#endif -transformedNormal = normalMatrix * transformedNormal; -#ifdef FLIP_SIDED - transformedNormal = - transformedNormal; -#endif -#ifdef USE_TANGENT - transformedTangent = ( modelViewMatrix * vec4( transformedTangent, 0.0 ) ).xyz; - #ifdef FLIP_SIDED - transformedTangent = - transformedTangent; - #endif -#endif`,w0=`#ifdef USE_DISPLACEMENTMAP - uniform sampler2D displacementMap; - uniform float displacementScale; - uniform float displacementBias; -#endif`,E0=`#ifdef USE_DISPLACEMENTMAP - transformed += normalize( objectNormal ) * ( texture2D( displacementMap, vDisplacementMapUv ).x * displacementScale + displacementBias ); -#endif`,T0=`#ifdef USE_EMISSIVEMAP - vec4 emissiveColor = texture2D( emissiveMap, vEmissiveMapUv ); - #ifdef DECODE_VIDEO_TEXTURE_EMISSIVE - emissiveColor = sRGBTransferEOTF( emissiveColor ); - #endif - totalEmissiveRadiance *= emissiveColor.rgb; -#endif`,A0=`#ifdef USE_EMISSIVEMAP - uniform sampler2D emissiveMap; -#endif`,C0="gl_FragColor = linearToOutputTexel( gl_FragColor );",R0=`vec4 LinearTransferOETF( in vec4 value ) { - return value; -} -vec4 sRGBTransferEOTF( in vec4 value ) { - return vec4( mix( pow( value.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), value.rgb * 0.0773993808, vec3( lessThanEqual( value.rgb, vec3( 0.04045 ) ) ) ), value.a ); -} -vec4 sRGBTransferOETF( in vec4 value ) { - return vec4( mix( pow( value.rgb, vec3( 0.41666 ) ) * 1.055 - vec3( 0.055 ), value.rgb * 12.92, vec3( lessThanEqual( value.rgb, vec3( 0.0031308 ) ) ) ), value.a ); -}`,I0=`#ifdef USE_ENVMAP - #ifdef ENV_WORLDPOS - vec3 cameraToFrag; - if ( isOrthographic ) { - cameraToFrag = normalize( vec3( - viewMatrix[ 0 ][ 2 ], - viewMatrix[ 1 ][ 2 ], - viewMatrix[ 2 ][ 2 ] ) ); - } else { - cameraToFrag = normalize( vWorldPosition - cameraPosition ); - } - vec3 worldNormal = inverseTransformDirection( normal, viewMatrix ); - #ifdef ENVMAP_MODE_REFLECTION - vec3 reflectVec = reflect( cameraToFrag, worldNormal ); - #else - vec3 reflectVec = refract( cameraToFrag, worldNormal, refractionRatio ); - #endif - #else - vec3 reflectVec = vReflect; - #endif - #ifdef ENVMAP_TYPE_CUBE - vec4 envColor = textureCube( envMap, envMapRotation * vec3( flipEnvMap * reflectVec.x, reflectVec.yz ) ); - #else - vec4 envColor = vec4( 0.0 ); - #endif - #ifdef ENVMAP_BLENDING_MULTIPLY - outgoingLight = mix( outgoingLight, outgoingLight * envColor.xyz, specularStrength * reflectivity ); - #elif defined( ENVMAP_BLENDING_MIX ) - outgoingLight = mix( outgoingLight, envColor.xyz, specularStrength * reflectivity ); - #elif defined( ENVMAP_BLENDING_ADD ) - outgoingLight += envColor.xyz * specularStrength * reflectivity; - #endif -#endif`,P0=`#ifdef USE_ENVMAP - uniform float envMapIntensity; - uniform float flipEnvMap; - uniform mat3 envMapRotation; - #ifdef ENVMAP_TYPE_CUBE - uniform samplerCube envMap; - #else - uniform sampler2D envMap; - #endif - -#endif`,D0=`#ifdef USE_ENVMAP - uniform float reflectivity; - #if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( PHONG ) || defined( LAMBERT ) - #define ENV_WORLDPOS - #endif - #ifdef ENV_WORLDPOS - varying vec3 vWorldPosition; - uniform float refractionRatio; - #else - varying vec3 vReflect; - #endif -#endif`,L0=`#ifdef USE_ENVMAP - #if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( PHONG ) || defined( LAMBERT ) - #define ENV_WORLDPOS - #endif - #ifdef ENV_WORLDPOS - - varying vec3 vWorldPosition; - #else - varying vec3 vReflect; - uniform float refractionRatio; - #endif -#endif`,U0=`#ifdef USE_ENVMAP - #ifdef ENV_WORLDPOS - vWorldPosition = worldPosition.xyz; - #else - vec3 cameraToVertex; - if ( isOrthographic ) { - cameraToVertex = normalize( vec3( - viewMatrix[ 0 ][ 2 ], - viewMatrix[ 1 ][ 2 ], - viewMatrix[ 2 ][ 2 ] ) ); - } else { - cameraToVertex = normalize( worldPosition.xyz - cameraPosition ); - } - vec3 worldNormal = inverseTransformDirection( transformedNormal, viewMatrix ); - #ifdef ENVMAP_MODE_REFLECTION - vReflect = reflect( cameraToVertex, worldNormal ); - #else - vReflect = refract( cameraToVertex, worldNormal, refractionRatio ); - #endif - #endif -#endif`,N0=`#ifdef USE_FOG - vFogDepth = - mvPosition.z; -#endif`,F0=`#ifdef USE_FOG - varying float vFogDepth; -#endif`,O0=`#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`,B0=`#ifdef USE_FOG - uniform vec3 fogColor; - varying float vFogDepth; - #ifdef FOG_EXP2 - uniform float fogDensity; - #else - uniform float fogNear; - uniform float fogFar; - #endif -#endif`,z0=`#ifdef USE_GRADIENTMAP - uniform sampler2D gradientMap; -#endif -vec3 getGradientIrradiance( vec3 normal, vec3 lightDirection ) { - float dotNL = dot( normal, lightDirection ); - vec2 coord = vec2( dotNL * 0.5 + 0.5, 0.0 ); - #ifdef USE_GRADIENTMAP - return vec3( texture2D( gradientMap, coord ).r ); - #else - vec2 fw = fwidth( coord ) * 0.5; - return mix( vec3( 0.7 ), vec3( 1.0 ), smoothstep( 0.7 - fw.x, 0.7 + fw.x, coord.x ) ); - #endif -}`,k0=`#ifdef USE_LIGHTMAP - uniform sampler2D lightMap; - uniform float lightMapIntensity; -#endif`,V0=`LambertMaterial material; -material.diffuseColor = diffuseColor.rgb; -material.specularStrength = specularStrength;`,H0=`varying vec3 vViewPosition; -struct LambertMaterial { - vec3 diffuseColor; - float specularStrength; -}; -void RE_Direct_Lambert( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in LambertMaterial material, inout ReflectedLight reflectedLight ) { - float dotNL = saturate( dot( geometryNormal, directLight.direction ) ); - vec3 irradiance = dotNL * directLight.color; - reflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); -} -void RE_IndirectDiffuse_Lambert( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in LambertMaterial material, inout ReflectedLight reflectedLight ) { - reflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); -} -#define RE_Direct RE_Direct_Lambert -#define RE_IndirectDiffuse RE_IndirectDiffuse_Lambert`,G0=`uniform bool receiveShadow; -uniform vec3 ambientLightColor; -#if defined( USE_LIGHT_PROBES ) - uniform vec3 lightProbe[ 9 ]; -#endif -vec3 shGetIrradianceAt( in vec3 normal, in vec3 shCoefficients[ 9 ] ) { - float x = normal.x, y = normal.y, z = normal.z; - vec3 result = shCoefficients[ 0 ] * 0.886227; - result += shCoefficients[ 1 ] * 2.0 * 0.511664 * y; - result += shCoefficients[ 2 ] * 2.0 * 0.511664 * z; - result += shCoefficients[ 3 ] * 2.0 * 0.511664 * x; - result += shCoefficients[ 4 ] * 2.0 * 0.429043 * x * y; - result += shCoefficients[ 5 ] * 2.0 * 0.429043 * y * z; - result += shCoefficients[ 6 ] * ( 0.743125 * z * z - 0.247708 ); - result += shCoefficients[ 7 ] * 2.0 * 0.429043 * x * z; - result += shCoefficients[ 8 ] * 0.429043 * ( x * x - y * y ); - return result; -} -vec3 getLightProbeIrradiance( const in vec3 lightProbe[ 9 ], const in vec3 normal ) { - vec3 worldNormal = inverseTransformDirection( normal, viewMatrix ); - vec3 irradiance = shGetIrradianceAt( worldNormal, lightProbe ); - return irradiance; -} -vec3 getAmbientLightIrradiance( const in vec3 ambientLightColor ) { - vec3 irradiance = ambientLightColor; - return irradiance; -} -float getDistanceAttenuation( const in float lightDistance, const in float cutoffDistance, const in float decayExponent ) { - float distanceFalloff = 1.0 / max( pow( lightDistance, decayExponent ), 0.01 ); - if ( cutoffDistance > 0.0 ) { - distanceFalloff *= pow2( saturate( 1.0 - pow4( lightDistance / cutoffDistance ) ) ); - } - return distanceFalloff; -} -float getSpotAttenuation( const in float coneCosine, const in float penumbraCosine, const in float angleCosine ) { - return smoothstep( coneCosine, penumbraCosine, angleCosine ); -} -#if NUM_DIR_LIGHTS > 0 - struct DirectionalLight { - vec3 direction; - vec3 color; - }; - uniform DirectionalLight directionalLights[ NUM_DIR_LIGHTS ]; - void getDirectionalLightInfo( const in DirectionalLight directionalLight, out IncidentLight light ) { - light.color = directionalLight.color; - light.direction = directionalLight.direction; - light.visible = true; - } -#endif -#if NUM_POINT_LIGHTS > 0 - struct PointLight { - vec3 position; - vec3 color; - float distance; - float decay; - }; - uniform PointLight pointLights[ NUM_POINT_LIGHTS ]; - void getPointLightInfo( const in PointLight pointLight, const in vec3 geometryPosition, out IncidentLight light ) { - vec3 lVector = pointLight.position - geometryPosition; - light.direction = normalize( lVector ); - float lightDistance = length( lVector ); - light.color = pointLight.color; - light.color *= getDistanceAttenuation( lightDistance, pointLight.distance, pointLight.decay ); - light.visible = ( light.color != vec3( 0.0 ) ); - } -#endif -#if NUM_SPOT_LIGHTS > 0 - struct SpotLight { - vec3 position; - vec3 direction; - vec3 color; - float distance; - float decay; - float coneCos; - float penumbraCos; - }; - uniform SpotLight spotLights[ NUM_SPOT_LIGHTS ]; - void getSpotLightInfo( const in SpotLight spotLight, const in vec3 geometryPosition, out IncidentLight light ) { - vec3 lVector = spotLight.position - geometryPosition; - light.direction = normalize( lVector ); - float angleCos = dot( light.direction, spotLight.direction ); - float spotAttenuation = getSpotAttenuation( spotLight.coneCos, spotLight.penumbraCos, angleCos ); - if ( spotAttenuation > 0.0 ) { - float lightDistance = length( lVector ); - light.color = spotLight.color * spotAttenuation; - light.color *= getDistanceAttenuation( lightDistance, spotLight.distance, spotLight.decay ); - light.visible = ( light.color != vec3( 0.0 ) ); - } else { - light.color = vec3( 0.0 ); - light.visible = false; - } - } -#endif -#if NUM_RECT_AREA_LIGHTS > 0 - struct RectAreaLight { - vec3 color; - vec3 position; - vec3 halfWidth; - vec3 halfHeight; - }; - uniform sampler2D ltc_1; uniform sampler2D ltc_2; - uniform RectAreaLight rectAreaLights[ NUM_RECT_AREA_LIGHTS ]; -#endif -#if NUM_HEMI_LIGHTS > 0 - struct HemisphereLight { - vec3 direction; - vec3 skyColor; - vec3 groundColor; - }; - uniform HemisphereLight hemisphereLights[ NUM_HEMI_LIGHTS ]; - vec3 getHemisphereLightIrradiance( const in HemisphereLight hemiLight, const in vec3 normal ) { - float dotNL = dot( normal, hemiLight.direction ); - float hemiDiffuseWeight = 0.5 * dotNL + 0.5; - vec3 irradiance = mix( hemiLight.groundColor, hemiLight.skyColor, hemiDiffuseWeight ); - return irradiance; - } -#endif`,W0=`#ifdef USE_ENVMAP - vec3 getIBLIrradiance( const in vec3 normal ) { - #ifdef ENVMAP_TYPE_CUBE_UV - vec3 worldNormal = inverseTransformDirection( normal, viewMatrix ); - vec4 envMapColor = textureCubeUV( envMap, envMapRotation * worldNormal, 1.0 ); - return PI * envMapColor.rgb * envMapIntensity; - #else - return vec3( 0.0 ); - #endif - } - vec3 getIBLRadiance( const in vec3 viewDir, const in vec3 normal, const in float roughness ) { - #ifdef ENVMAP_TYPE_CUBE_UV - vec3 reflectVec = reflect( - viewDir, normal ); - reflectVec = normalize( mix( reflectVec, normal, roughness * roughness) ); - reflectVec = inverseTransformDirection( reflectVec, viewMatrix ); - vec4 envMapColor = textureCubeUV( envMap, envMapRotation * reflectVec, roughness ); - return envMapColor.rgb * envMapIntensity; - #else - return vec3( 0.0 ); - #endif - } - #ifdef USE_ANISOTROPY - vec3 getIBLAnisotropyRadiance( const in vec3 viewDir, const in vec3 normal, const in float roughness, const in vec3 bitangent, const in float anisotropy ) { - #ifdef ENVMAP_TYPE_CUBE_UV - vec3 bentNormal = cross( bitangent, viewDir ); - bentNormal = normalize( cross( bentNormal, bitangent ) ); - bentNormal = normalize( mix( bentNormal, normal, pow2( pow2( 1.0 - anisotropy * ( 1.0 - roughness ) ) ) ) ); - return getIBLRadiance( viewDir, bentNormal, roughness ); - #else - return vec3( 0.0 ); - #endif - } - #endif -#endif`,X0=`ToonMaterial material; -material.diffuseColor = diffuseColor.rgb;`,q0=`varying vec3 vViewPosition; -struct ToonMaterial { - vec3 diffuseColor; -}; -void RE_Direct_Toon( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in ToonMaterial material, inout ReflectedLight reflectedLight ) { - vec3 irradiance = getGradientIrradiance( geometryNormal, directLight.direction ) * directLight.color; - reflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); -} -void RE_IndirectDiffuse_Toon( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in ToonMaterial material, inout ReflectedLight reflectedLight ) { - reflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); -} -#define RE_Direct RE_Direct_Toon -#define RE_IndirectDiffuse RE_IndirectDiffuse_Toon`,Y0=`BlinnPhongMaterial material; -material.diffuseColor = diffuseColor.rgb; -material.specularColor = specular; -material.specularShininess = shininess; -material.specularStrength = specularStrength;`,Z0=`varying vec3 vViewPosition; -struct BlinnPhongMaterial { - vec3 diffuseColor; - vec3 specularColor; - float specularShininess; - float specularStrength; -}; -void RE_Direct_BlinnPhong( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) { - float dotNL = saturate( dot( geometryNormal, directLight.direction ) ); - vec3 irradiance = dotNL * directLight.color; - reflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); - reflectedLight.directSpecular += irradiance * BRDF_BlinnPhong( directLight.direction, geometryViewDir, geometryNormal, material.specularColor, material.specularShininess ) * material.specularStrength; -} -void RE_IndirectDiffuse_BlinnPhong( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) { - reflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); -} -#define RE_Direct RE_Direct_BlinnPhong -#define RE_IndirectDiffuse RE_IndirectDiffuse_BlinnPhong`,J0=`PhysicalMaterial material; -material.diffuseColor = diffuseColor.rgb * ( 1.0 - metalnessFactor ); -vec3 dxy = max( abs( dFdx( nonPerturbedNormal ) ), abs( dFdy( nonPerturbedNormal ) ) ); -float geometryRoughness = max( max( dxy.x, dxy.y ), dxy.z ); -material.roughness = max( roughnessFactor, 0.0525 );material.roughness += geometryRoughness; -material.roughness = min( material.roughness, 1.0 ); -#ifdef IOR - material.ior = ior; - #ifdef USE_SPECULAR - float specularIntensityFactor = specularIntensity; - vec3 specularColorFactor = specularColor; - #ifdef USE_SPECULAR_COLORMAP - specularColorFactor *= texture2D( specularColorMap, vSpecularColorMapUv ).rgb; - #endif - #ifdef USE_SPECULAR_INTENSITYMAP - specularIntensityFactor *= texture2D( specularIntensityMap, vSpecularIntensityMapUv ).a; - #endif - material.specularF90 = mix( specularIntensityFactor, 1.0, metalnessFactor ); - #else - float specularIntensityFactor = 1.0; - vec3 specularColorFactor = vec3( 1.0 ); - material.specularF90 = 1.0; - #endif - material.specularColor = mix( min( pow2( ( material.ior - 1.0 ) / ( material.ior + 1.0 ) ) * specularColorFactor, vec3( 1.0 ) ) * specularIntensityFactor, diffuseColor.rgb, metalnessFactor ); -#else - material.specularColor = mix( vec3( 0.04 ), diffuseColor.rgb, metalnessFactor ); - material.specularF90 = 1.0; -#endif -#ifdef USE_CLEARCOAT - material.clearcoat = clearcoat; - material.clearcoatRoughness = clearcoatRoughness; - material.clearcoatF0 = vec3( 0.04 ); - material.clearcoatF90 = 1.0; - #ifdef USE_CLEARCOATMAP - material.clearcoat *= texture2D( clearcoatMap, vClearcoatMapUv ).x; - #endif - #ifdef USE_CLEARCOAT_ROUGHNESSMAP - material.clearcoatRoughness *= texture2D( clearcoatRoughnessMap, vClearcoatRoughnessMapUv ).y; - #endif - material.clearcoat = saturate( material.clearcoat ); material.clearcoatRoughness = max( material.clearcoatRoughness, 0.0525 ); - material.clearcoatRoughness += geometryRoughness; - material.clearcoatRoughness = min( material.clearcoatRoughness, 1.0 ); -#endif -#ifdef USE_DISPERSION - material.dispersion = dispersion; -#endif -#ifdef USE_IRIDESCENCE - material.iridescence = iridescence; - material.iridescenceIOR = iridescenceIOR; - #ifdef USE_IRIDESCENCEMAP - material.iridescence *= texture2D( iridescenceMap, vIridescenceMapUv ).r; - #endif - #ifdef USE_IRIDESCENCE_THICKNESSMAP - material.iridescenceThickness = (iridescenceThicknessMaximum - iridescenceThicknessMinimum) * texture2D( iridescenceThicknessMap, vIridescenceThicknessMapUv ).g + iridescenceThicknessMinimum; - #else - material.iridescenceThickness = iridescenceThicknessMaximum; - #endif -#endif -#ifdef USE_SHEEN - material.sheenColor = sheenColor; - #ifdef USE_SHEEN_COLORMAP - material.sheenColor *= texture2D( sheenColorMap, vSheenColorMapUv ).rgb; - #endif - material.sheenRoughness = clamp( sheenRoughness, 0.07, 1.0 ); - #ifdef USE_SHEEN_ROUGHNESSMAP - material.sheenRoughness *= texture2D( sheenRoughnessMap, vSheenRoughnessMapUv ).a; - #endif -#endif -#ifdef USE_ANISOTROPY - #ifdef USE_ANISOTROPYMAP - mat2 anisotropyMat = mat2( anisotropyVector.x, anisotropyVector.y, - anisotropyVector.y, anisotropyVector.x ); - vec3 anisotropyPolar = texture2D( anisotropyMap, vAnisotropyMapUv ).rgb; - vec2 anisotropyV = anisotropyMat * normalize( 2.0 * anisotropyPolar.rg - vec2( 1.0 ) ) * anisotropyPolar.b; - #else - vec2 anisotropyV = anisotropyVector; - #endif - material.anisotropy = length( anisotropyV ); - if( material.anisotropy == 0.0 ) { - anisotropyV = vec2( 1.0, 0.0 ); - } else { - anisotropyV /= material.anisotropy; - material.anisotropy = saturate( material.anisotropy ); - } - material.alphaT = mix( pow2( material.roughness ), 1.0, pow2( material.anisotropy ) ); - material.anisotropyT = tbn[ 0 ] * anisotropyV.x + tbn[ 1 ] * anisotropyV.y; - material.anisotropyB = tbn[ 1 ] * anisotropyV.x - tbn[ 0 ] * anisotropyV.y; -#endif`,K0=`struct PhysicalMaterial { - vec3 diffuseColor; - float roughness; - vec3 specularColor; - float specularF90; - float dispersion; - #ifdef USE_CLEARCOAT - float clearcoat; - float clearcoatRoughness; - vec3 clearcoatF0; - float clearcoatF90; - #endif - #ifdef USE_IRIDESCENCE - float iridescence; - float iridescenceIOR; - float iridescenceThickness; - vec3 iridescenceFresnel; - vec3 iridescenceF0; - #endif - #ifdef USE_SHEEN - vec3 sheenColor; - float sheenRoughness; - #endif - #ifdef IOR - float ior; - #endif - #ifdef USE_TRANSMISSION - float transmission; - float transmissionAlpha; - float thickness; - float attenuationDistance; - vec3 attenuationColor; - #endif - #ifdef USE_ANISOTROPY - float anisotropy; - float alphaT; - vec3 anisotropyT; - vec3 anisotropyB; - #endif -}; -vec3 clearcoatSpecularDirect = vec3( 0.0 ); -vec3 clearcoatSpecularIndirect = vec3( 0.0 ); -vec3 sheenSpecularDirect = vec3( 0.0 ); -vec3 sheenSpecularIndirect = vec3(0.0 ); -vec3 Schlick_to_F0( const in vec3 f, const in float f90, const in float dotVH ) { - float x = clamp( 1.0 - dotVH, 0.0, 1.0 ); - float x2 = x * x; - float x5 = clamp( x * x2 * x2, 0.0, 0.9999 ); - return ( f - vec3( f90 ) * x5 ) / ( 1.0 - x5 ); -} -float V_GGX_SmithCorrelated( const in float alpha, const in float dotNL, const in float dotNV ) { - float a2 = pow2( alpha ); - float gv = dotNL * sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNV ) ); - float gl = dotNV * sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNL ) ); - return 0.5 / max( gv + gl, EPSILON ); -} -float D_GGX( const in float alpha, const in float dotNH ) { - float a2 = pow2( alpha ); - float denom = pow2( dotNH ) * ( a2 - 1.0 ) + 1.0; - return RECIPROCAL_PI * a2 / pow2( denom ); -} -#ifdef USE_ANISOTROPY - float V_GGX_SmithCorrelated_Anisotropic( const in float alphaT, const in float alphaB, const in float dotTV, const in float dotBV, const in float dotTL, const in float dotBL, const in float dotNV, const in float dotNL ) { - float gv = dotNL * length( vec3( alphaT * dotTV, alphaB * dotBV, dotNV ) ); - float gl = dotNV * length( vec3( alphaT * dotTL, alphaB * dotBL, dotNL ) ); - float v = 0.5 / ( gv + gl ); - return saturate(v); - } - float D_GGX_Anisotropic( const in float alphaT, const in float alphaB, const in float dotNH, const in float dotTH, const in float dotBH ) { - float a2 = alphaT * alphaB; - highp vec3 v = vec3( alphaB * dotTH, alphaT * dotBH, a2 * dotNH ); - highp float v2 = dot( v, v ); - float w2 = a2 / v2; - return RECIPROCAL_PI * a2 * pow2 ( w2 ); - } -#endif -#ifdef USE_CLEARCOAT - vec3 BRDF_GGX_Clearcoat( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, const in PhysicalMaterial material) { - vec3 f0 = material.clearcoatF0; - float f90 = material.clearcoatF90; - float roughness = material.clearcoatRoughness; - float alpha = pow2( roughness ); - vec3 halfDir = normalize( lightDir + viewDir ); - float dotNL = saturate( dot( normal, lightDir ) ); - float dotNV = saturate( dot( normal, viewDir ) ); - float dotNH = saturate( dot( normal, halfDir ) ); - float dotVH = saturate( dot( viewDir, halfDir ) ); - vec3 F = F_Schlick( f0, f90, dotVH ); - float V = V_GGX_SmithCorrelated( alpha, dotNL, dotNV ); - float D = D_GGX( alpha, dotNH ); - return F * ( V * D ); - } -#endif -vec3 BRDF_GGX( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, const in PhysicalMaterial material ) { - vec3 f0 = material.specularColor; - float f90 = material.specularF90; - float roughness = material.roughness; - float alpha = pow2( roughness ); - vec3 halfDir = normalize( lightDir + viewDir ); - float dotNL = saturate( dot( normal, lightDir ) ); - float dotNV = saturate( dot( normal, viewDir ) ); - float dotNH = saturate( dot( normal, halfDir ) ); - float dotVH = saturate( dot( viewDir, halfDir ) ); - vec3 F = F_Schlick( f0, f90, dotVH ); - #ifdef USE_IRIDESCENCE - F = mix( F, material.iridescenceFresnel, material.iridescence ); - #endif - #ifdef USE_ANISOTROPY - float dotTL = dot( material.anisotropyT, lightDir ); - float dotTV = dot( material.anisotropyT, viewDir ); - float dotTH = dot( material.anisotropyT, halfDir ); - float dotBL = dot( material.anisotropyB, lightDir ); - float dotBV = dot( material.anisotropyB, viewDir ); - float dotBH = dot( material.anisotropyB, halfDir ); - float V = V_GGX_SmithCorrelated_Anisotropic( material.alphaT, alpha, dotTV, dotBV, dotTL, dotBL, dotNV, dotNL ); - float D = D_GGX_Anisotropic( material.alphaT, alpha, dotNH, dotTH, dotBH ); - #else - float V = V_GGX_SmithCorrelated( alpha, dotNL, dotNV ); - float D = D_GGX( alpha, dotNH ); - #endif - return F * ( V * D ); -} -vec2 LTC_Uv( const in vec3 N, const in vec3 V, const in float roughness ) { - const float LUT_SIZE = 64.0; - const float LUT_SCALE = ( LUT_SIZE - 1.0 ) / LUT_SIZE; - const float LUT_BIAS = 0.5 / LUT_SIZE; - float dotNV = saturate( dot( N, V ) ); - vec2 uv = vec2( roughness, sqrt( 1.0 - dotNV ) ); - uv = uv * LUT_SCALE + LUT_BIAS; - return uv; -} -float LTC_ClippedSphereFormFactor( const in vec3 f ) { - float l = length( f ); - return max( ( l * l + f.z ) / ( l + 1.0 ), 0.0 ); -} -vec3 LTC_EdgeVectorFormFactor( const in vec3 v1, const in vec3 v2 ) { - float x = dot( v1, v2 ); - float y = abs( x ); - float a = 0.8543985 + ( 0.4965155 + 0.0145206 * y ) * y; - float b = 3.4175940 + ( 4.1616724 + y ) * y; - float v = a / b; - float theta_sintheta = ( x > 0.0 ) ? v : 0.5 * inversesqrt( max( 1.0 - x * x, 1e-7 ) ) - v; - return cross( v1, v2 ) * theta_sintheta; -} -vec3 LTC_Evaluate( const in vec3 N, const in vec3 V, const in vec3 P, const in mat3 mInv, const in vec3 rectCoords[ 4 ] ) { - vec3 v1 = rectCoords[ 1 ] - rectCoords[ 0 ]; - vec3 v2 = rectCoords[ 3 ] - rectCoords[ 0 ]; - vec3 lightNormal = cross( v1, v2 ); - if( dot( lightNormal, P - rectCoords[ 0 ] ) < 0.0 ) return vec3( 0.0 ); - vec3 T1, T2; - T1 = normalize( V - N * dot( V, N ) ); - T2 = - cross( N, T1 ); - mat3 mat = mInv * transposeMat3( mat3( T1, T2, N ) ); - vec3 coords[ 4 ]; - coords[ 0 ] = mat * ( rectCoords[ 0 ] - P ); - coords[ 1 ] = mat * ( rectCoords[ 1 ] - P ); - coords[ 2 ] = mat * ( rectCoords[ 2 ] - P ); - coords[ 3 ] = mat * ( rectCoords[ 3 ] - P ); - coords[ 0 ] = normalize( coords[ 0 ] ); - coords[ 1 ] = normalize( coords[ 1 ] ); - coords[ 2 ] = normalize( coords[ 2 ] ); - coords[ 3 ] = normalize( coords[ 3 ] ); - vec3 vectorFormFactor = vec3( 0.0 ); - vectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 0 ], coords[ 1 ] ); - vectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 1 ], coords[ 2 ] ); - vectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 2 ], coords[ 3 ] ); - vectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 3 ], coords[ 0 ] ); - float result = LTC_ClippedSphereFormFactor( vectorFormFactor ); - return vec3( result ); -} -#if defined( USE_SHEEN ) -float D_Charlie( float roughness, float dotNH ) { - float alpha = pow2( roughness ); - float invAlpha = 1.0 / alpha; - float cos2h = dotNH * dotNH; - float sin2h = max( 1.0 - cos2h, 0.0078125 ); - return ( 2.0 + invAlpha ) * pow( sin2h, invAlpha * 0.5 ) / ( 2.0 * PI ); -} -float V_Neubelt( float dotNV, float dotNL ) { - return saturate( 1.0 / ( 4.0 * ( dotNL + dotNV - dotNL * dotNV ) ) ); -} -vec3 BRDF_Sheen( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, vec3 sheenColor, const in float sheenRoughness ) { - vec3 halfDir = normalize( lightDir + viewDir ); - float dotNL = saturate( dot( normal, lightDir ) ); - float dotNV = saturate( dot( normal, viewDir ) ); - float dotNH = saturate( dot( normal, halfDir ) ); - float D = D_Charlie( sheenRoughness, dotNH ); - float V = V_Neubelt( dotNV, dotNL ); - return sheenColor * ( D * V ); -} -#endif -float IBLSheenBRDF( const in vec3 normal, const in vec3 viewDir, const in float roughness ) { - float dotNV = saturate( dot( normal, viewDir ) ); - float r2 = roughness * roughness; - float a = roughness < 0.25 ? -339.2 * r2 + 161.4 * roughness - 25.9 : -8.48 * r2 + 14.3 * roughness - 9.95; - float b = roughness < 0.25 ? 44.0 * r2 - 23.7 * roughness + 3.26 : 1.97 * r2 - 3.27 * roughness + 0.72; - float DG = exp( a * dotNV + b ) + ( roughness < 0.25 ? 0.0 : 0.1 * ( roughness - 0.25 ) ); - return saturate( DG * RECIPROCAL_PI ); -} -vec2 DFGApprox( const in vec3 normal, const in vec3 viewDir, const in float roughness ) { - float dotNV = saturate( dot( normal, viewDir ) ); - const vec4 c0 = vec4( - 1, - 0.0275, - 0.572, 0.022 ); - const vec4 c1 = vec4( 1, 0.0425, 1.04, - 0.04 ); - vec4 r = roughness * c0 + c1; - float a004 = min( r.x * r.x, exp2( - 9.28 * dotNV ) ) * r.x + r.y; - vec2 fab = vec2( - 1.04, 1.04 ) * a004 + r.zw; - return fab; -} -vec3 EnvironmentBRDF( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float roughness ) { - vec2 fab = DFGApprox( normal, viewDir, roughness ); - return specularColor * fab.x + specularF90 * fab.y; -} -#ifdef USE_IRIDESCENCE -void computeMultiscatteringIridescence( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float iridescence, const in vec3 iridescenceF0, const in float roughness, inout vec3 singleScatter, inout vec3 multiScatter ) { -#else -void computeMultiscattering( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float roughness, inout vec3 singleScatter, inout vec3 multiScatter ) { -#endif - vec2 fab = DFGApprox( normal, viewDir, roughness ); - #ifdef USE_IRIDESCENCE - vec3 Fr = mix( specularColor, iridescenceF0, iridescence ); - #else - vec3 Fr = specularColor; - #endif - vec3 FssEss = Fr * fab.x + specularF90 * fab.y; - float Ess = fab.x + fab.y; - float Ems = 1.0 - Ess; - vec3 Favg = Fr + ( 1.0 - Fr ) * 0.047619; vec3 Fms = FssEss * Favg / ( 1.0 - Ems * Favg ); - singleScatter += FssEss; - multiScatter += Fms * Ems; -} -#if NUM_RECT_AREA_LIGHTS > 0 - void RE_Direct_RectArea_Physical( const in RectAreaLight rectAreaLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) { - vec3 normal = geometryNormal; - vec3 viewDir = geometryViewDir; - vec3 position = geometryPosition; - vec3 lightPos = rectAreaLight.position; - vec3 halfWidth = rectAreaLight.halfWidth; - vec3 halfHeight = rectAreaLight.halfHeight; - vec3 lightColor = rectAreaLight.color; - float roughness = material.roughness; - vec3 rectCoords[ 4 ]; - rectCoords[ 0 ] = lightPos + halfWidth - halfHeight; rectCoords[ 1 ] = lightPos - halfWidth - halfHeight; - rectCoords[ 2 ] = lightPos - halfWidth + halfHeight; - rectCoords[ 3 ] = lightPos + halfWidth + halfHeight; - vec2 uv = LTC_Uv( normal, viewDir, roughness ); - vec4 t1 = texture2D( ltc_1, uv ); - vec4 t2 = texture2D( ltc_2, uv ); - mat3 mInv = mat3( - vec3( t1.x, 0, t1.y ), - vec3( 0, 1, 0 ), - vec3( t1.z, 0, t1.w ) - ); - vec3 fresnel = ( material.specularColor * t2.x + ( vec3( 1.0 ) - material.specularColor ) * t2.y ); - reflectedLight.directSpecular += lightColor * fresnel * LTC_Evaluate( normal, viewDir, position, mInv, rectCoords ); - reflectedLight.directDiffuse += lightColor * material.diffuseColor * LTC_Evaluate( normal, viewDir, position, mat3( 1.0 ), rectCoords ); - } -#endif -void RE_Direct_Physical( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) { - float dotNL = saturate( dot( geometryNormal, directLight.direction ) ); - vec3 irradiance = dotNL * directLight.color; - #ifdef USE_CLEARCOAT - float dotNLcc = saturate( dot( geometryClearcoatNormal, directLight.direction ) ); - vec3 ccIrradiance = dotNLcc * directLight.color; - clearcoatSpecularDirect += ccIrradiance * BRDF_GGX_Clearcoat( directLight.direction, geometryViewDir, geometryClearcoatNormal, material ); - #endif - #ifdef USE_SHEEN - sheenSpecularDirect += irradiance * BRDF_Sheen( directLight.direction, geometryViewDir, geometryNormal, material.sheenColor, material.sheenRoughness ); - #endif - reflectedLight.directSpecular += irradiance * BRDF_GGX( directLight.direction, geometryViewDir, geometryNormal, material ); - reflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); -} -void RE_IndirectDiffuse_Physical( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) { - reflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); -} -void RE_IndirectSpecular_Physical( const in vec3 radiance, const in vec3 irradiance, const in vec3 clearcoatRadiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight) { - #ifdef USE_CLEARCOAT - clearcoatSpecularIndirect += clearcoatRadiance * EnvironmentBRDF( geometryClearcoatNormal, geometryViewDir, material.clearcoatF0, material.clearcoatF90, material.clearcoatRoughness ); - #endif - #ifdef USE_SHEEN - sheenSpecularIndirect += irradiance * material.sheenColor * IBLSheenBRDF( geometryNormal, geometryViewDir, material.sheenRoughness ); - #endif - vec3 singleScattering = vec3( 0.0 ); - vec3 multiScattering = vec3( 0.0 ); - vec3 cosineWeightedIrradiance = irradiance * RECIPROCAL_PI; - #ifdef USE_IRIDESCENCE - computeMultiscatteringIridescence( geometryNormal, geometryViewDir, material.specularColor, material.specularF90, material.iridescence, material.iridescenceFresnel, material.roughness, singleScattering, multiScattering ); - #else - computeMultiscattering( geometryNormal, geometryViewDir, material.specularColor, material.specularF90, material.roughness, singleScattering, multiScattering ); - #endif - vec3 totalScattering = singleScattering + multiScattering; - vec3 diffuse = material.diffuseColor * ( 1.0 - max( max( totalScattering.r, totalScattering.g ), totalScattering.b ) ); - reflectedLight.indirectSpecular += radiance * singleScattering; - reflectedLight.indirectSpecular += multiScattering * cosineWeightedIrradiance; - reflectedLight.indirectDiffuse += diffuse * cosineWeightedIrradiance; -} -#define RE_Direct RE_Direct_Physical -#define RE_Direct_RectArea RE_Direct_RectArea_Physical -#define RE_IndirectDiffuse RE_IndirectDiffuse_Physical -#define RE_IndirectSpecular RE_IndirectSpecular_Physical -float computeSpecularOcclusion( const in float dotNV, const in float ambientOcclusion, const in float roughness ) { - return saturate( pow( dotNV + ambientOcclusion, exp2( - 16.0 * roughness - 1.0 ) ) - 1.0 + ambientOcclusion ); -}`,$0=` -vec3 geometryPosition = - vViewPosition; -vec3 geometryNormal = normal; -vec3 geometryViewDir = ( isOrthographic ) ? vec3( 0, 0, 1 ) : normalize( vViewPosition ); -vec3 geometryClearcoatNormal = vec3( 0.0 ); -#ifdef USE_CLEARCOAT - geometryClearcoatNormal = clearcoatNormal; -#endif -#ifdef USE_IRIDESCENCE - float dotNVi = saturate( dot( normal, geometryViewDir ) ); - if ( material.iridescenceThickness == 0.0 ) { - material.iridescence = 0.0; - } else { - material.iridescence = saturate( material.iridescence ); - } - if ( material.iridescence > 0.0 ) { - material.iridescenceFresnel = evalIridescence( 1.0, material.iridescenceIOR, dotNVi, material.iridescenceThickness, material.specularColor ); - material.iridescenceF0 = Schlick_to_F0( material.iridescenceFresnel, 1.0, dotNVi ); - } -#endif -IncidentLight directLight; -#if ( NUM_POINT_LIGHTS > 0 ) && defined( RE_Direct ) - PointLight pointLight; - #if defined( USE_SHADOWMAP ) && NUM_POINT_LIGHT_SHADOWS > 0 - PointLightShadow pointLightShadow; - #endif - #pragma unroll_loop_start - for ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) { - pointLight = pointLights[ i ]; - getPointLightInfo( pointLight, geometryPosition, directLight ); - #if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_POINT_LIGHT_SHADOWS ) - pointLightShadow = pointLightShadows[ i ]; - directLight.color *= ( directLight.visible && receiveShadow ) ? getPointShadow( pointShadowMap[ i ], pointLightShadow.shadowMapSize, pointLightShadow.shadowIntensity, pointLightShadow.shadowBias, pointLightShadow.shadowRadius, vPointShadowCoord[ i ], pointLightShadow.shadowCameraNear, pointLightShadow.shadowCameraFar ) : 1.0; - #endif - RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight ); - } - #pragma unroll_loop_end -#endif -#if ( NUM_SPOT_LIGHTS > 0 ) && defined( RE_Direct ) - SpotLight spotLight; - vec4 spotColor; - vec3 spotLightCoord; - bool inSpotLightMap; - #if defined( USE_SHADOWMAP ) && NUM_SPOT_LIGHT_SHADOWS > 0 - SpotLightShadow spotLightShadow; - #endif - #pragma unroll_loop_start - for ( int i = 0; i < NUM_SPOT_LIGHTS; i ++ ) { - spotLight = spotLights[ i ]; - getSpotLightInfo( spotLight, geometryPosition, directLight ); - #if ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS_WITH_MAPS ) - #define SPOT_LIGHT_MAP_INDEX UNROLLED_LOOP_INDEX - #elif ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS ) - #define SPOT_LIGHT_MAP_INDEX NUM_SPOT_LIGHT_MAPS - #else - #define SPOT_LIGHT_MAP_INDEX ( UNROLLED_LOOP_INDEX - NUM_SPOT_LIGHT_SHADOWS + NUM_SPOT_LIGHT_SHADOWS_WITH_MAPS ) - #endif - #if ( SPOT_LIGHT_MAP_INDEX < NUM_SPOT_LIGHT_MAPS ) - spotLightCoord = vSpotLightCoord[ i ].xyz / vSpotLightCoord[ i ].w; - inSpotLightMap = all( lessThan( abs( spotLightCoord * 2. - 1. ), vec3( 1.0 ) ) ); - spotColor = texture2D( spotLightMap[ SPOT_LIGHT_MAP_INDEX ], spotLightCoord.xy ); - directLight.color = inSpotLightMap ? directLight.color * spotColor.rgb : directLight.color; - #endif - #undef SPOT_LIGHT_MAP_INDEX - #if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS ) - spotLightShadow = spotLightShadows[ i ]; - directLight.color *= ( directLight.visible && receiveShadow ) ? getShadow( spotShadowMap[ i ], spotLightShadow.shadowMapSize, spotLightShadow.shadowIntensity, spotLightShadow.shadowBias, spotLightShadow.shadowRadius, vSpotLightCoord[ i ] ) : 1.0; - #endif - RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight ); - } - #pragma unroll_loop_end -#endif -#if ( NUM_DIR_LIGHTS > 0 ) && defined( RE_Direct ) - DirectionalLight directionalLight; - #if defined( USE_SHADOWMAP ) && NUM_DIR_LIGHT_SHADOWS > 0 - DirectionalLightShadow directionalLightShadow; - #endif - #pragma unroll_loop_start - for ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) { - directionalLight = directionalLights[ i ]; - getDirectionalLightInfo( directionalLight, directLight ); - #if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS ) - directionalLightShadow = directionalLightShadows[ i ]; - directLight.color *= ( directLight.visible && receiveShadow ) ? getShadow( directionalShadowMap[ i ], directionalLightShadow.shadowMapSize, directionalLightShadow.shadowIntensity, directionalLightShadow.shadowBias, directionalLightShadow.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0; - #endif - RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight ); - } - #pragma unroll_loop_end -#endif -#if ( NUM_RECT_AREA_LIGHTS > 0 ) && defined( RE_Direct_RectArea ) - RectAreaLight rectAreaLight; - #pragma unroll_loop_start - for ( int i = 0; i < NUM_RECT_AREA_LIGHTS; i ++ ) { - rectAreaLight = rectAreaLights[ i ]; - RE_Direct_RectArea( rectAreaLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight ); - } - #pragma unroll_loop_end -#endif -#if defined( RE_IndirectDiffuse ) - vec3 iblIrradiance = vec3( 0.0 ); - vec3 irradiance = getAmbientLightIrradiance( ambientLightColor ); - #if defined( USE_LIGHT_PROBES ) - irradiance += getLightProbeIrradiance( lightProbe, geometryNormal ); - #endif - #if ( NUM_HEMI_LIGHTS > 0 ) - #pragma unroll_loop_start - for ( int i = 0; i < NUM_HEMI_LIGHTS; i ++ ) { - irradiance += getHemisphereLightIrradiance( hemisphereLights[ i ], geometryNormal ); - } - #pragma unroll_loop_end - #endif -#endif -#if defined( RE_IndirectSpecular ) - vec3 radiance = vec3( 0.0 ); - vec3 clearcoatRadiance = vec3( 0.0 ); -#endif`,j0=`#if defined( RE_IndirectDiffuse ) - #ifdef USE_LIGHTMAP - vec4 lightMapTexel = texture2D( lightMap, vLightMapUv ); - vec3 lightMapIrradiance = lightMapTexel.rgb * lightMapIntensity; - irradiance += lightMapIrradiance; - #endif - #if defined( USE_ENVMAP ) && defined( STANDARD ) && defined( ENVMAP_TYPE_CUBE_UV ) - iblIrradiance += getIBLIrradiance( geometryNormal ); - #endif -#endif -#if defined( USE_ENVMAP ) && defined( RE_IndirectSpecular ) - #ifdef USE_ANISOTROPY - radiance += getIBLAnisotropyRadiance( geometryViewDir, geometryNormal, material.roughness, material.anisotropyB, material.anisotropy ); - #else - radiance += getIBLRadiance( geometryViewDir, geometryNormal, material.roughness ); - #endif - #ifdef USE_CLEARCOAT - clearcoatRadiance += getIBLRadiance( geometryViewDir, geometryClearcoatNormal, material.clearcoatRoughness ); - #endif -#endif`,Q0=`#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`,e_=`#if defined( USE_LOGDEPTHBUF ) - gl_FragDepth = vIsPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5; -#endif`,t_=`#if defined( USE_LOGDEPTHBUF ) - uniform float logDepthBufFC; - varying float vFragDepth; - varying float vIsPerspective; -#endif`,n_=`#ifdef USE_LOGDEPTHBUF - varying float vFragDepth; - varying float vIsPerspective; -#endif`,i_=`#ifdef USE_LOGDEPTHBUF - vFragDepth = 1.0 + gl_Position.w; - vIsPerspective = float( isPerspectiveMatrix( projectionMatrix ) ); -#endif`,s_=`#ifdef USE_MAP - vec4 sampledDiffuseColor = texture2D( map, vMapUv ); - #ifdef DECODE_VIDEO_TEXTURE - sampledDiffuseColor = sRGBTransferEOTF( sampledDiffuseColor ); - #endif - diffuseColor *= sampledDiffuseColor; -#endif`,r_=`#ifdef USE_MAP - uniform sampler2D map; -#endif`,a_=`#if defined( USE_MAP ) || defined( USE_ALPHAMAP ) - #if defined( USE_POINTS_UV ) - vec2 uv = vUv; - #else - vec2 uv = ( uvTransform * vec3( gl_PointCoord.x, 1.0 - gl_PointCoord.y, 1 ) ).xy; - #endif -#endif -#ifdef USE_MAP - diffuseColor *= texture2D( map, uv ); -#endif -#ifdef USE_ALPHAMAP - diffuseColor.a *= texture2D( alphaMap, uv ).g; -#endif`,o_=`#if defined( USE_POINTS_UV ) - varying vec2 vUv; -#else - #if defined( USE_MAP ) || defined( USE_ALPHAMAP ) - uniform mat3 uvTransform; - #endif -#endif -#ifdef USE_MAP - uniform sampler2D map; -#endif -#ifdef USE_ALPHAMAP - uniform sampler2D alphaMap; -#endif`,l_=`float metalnessFactor = metalness; -#ifdef USE_METALNESSMAP - vec4 texelMetalness = texture2D( metalnessMap, vMetalnessMapUv ); - metalnessFactor *= texelMetalness.b; -#endif`,c_=`#ifdef USE_METALNESSMAP - uniform sampler2D metalnessMap; -#endif`,h_=`#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`,u_=`#if defined( USE_MORPHCOLORS ) - vColor *= morphTargetBaseInfluence; - for ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) { - #if defined( USE_COLOR_ALPHA ) - if ( morphTargetInfluences[ i ] != 0.0 ) vColor += getMorph( gl_VertexID, i, 2 ) * morphTargetInfluences[ i ]; - #elif defined( USE_COLOR ) - if ( morphTargetInfluences[ i ] != 0.0 ) vColor += getMorph( gl_VertexID, i, 2 ).rgb * morphTargetInfluences[ i ]; - #endif - } -#endif`,d_=`#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`,f_=`#ifdef USE_MORPHTARGETS - #ifndef USE_INSTANCING_MORPH - uniform float morphTargetBaseInfluence; - uniform float morphTargetInfluences[ MORPHTARGETS_COUNT ]; - #endif - uniform sampler2DArray morphTargetsTexture; - uniform ivec2 morphTargetsTextureSize; - vec4 getMorph( const in int vertexIndex, const in int morphTargetIndex, const in int offset ) { - int texelIndex = vertexIndex * MORPHTARGETS_TEXTURE_STRIDE + offset; - int y = texelIndex / morphTargetsTextureSize.x; - int x = texelIndex - y * morphTargetsTextureSize.x; - ivec3 morphUV = ivec3( x, y, morphTargetIndex ); - return texelFetch( morphTargetsTexture, morphUV, 0 ); - } -#endif`,p_=`#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`,m_=`float faceDirection = gl_FrontFacing ? 1.0 : - 1.0; -#ifdef FLAT_SHADED - vec3 fdx = dFdx( vViewPosition ); - vec3 fdy = dFdy( vViewPosition ); - vec3 normal = normalize( cross( fdx, fdy ) ); -#else - vec3 normal = normalize( vNormal ); - #ifdef DOUBLE_SIDED - normal *= faceDirection; - #endif -#endif -#if defined( USE_NORMALMAP_TANGENTSPACE ) || defined( USE_CLEARCOAT_NORMALMAP ) || defined( USE_ANISOTROPY ) - #ifdef USE_TANGENT - mat3 tbn = mat3( normalize( vTangent ), normalize( vBitangent ), normal ); - #else - mat3 tbn = getTangentFrame( - vViewPosition, normal, - #if defined( USE_NORMALMAP ) - vNormalMapUv - #elif defined( USE_CLEARCOAT_NORMALMAP ) - vClearcoatNormalMapUv - #else - vUv - #endif - ); - #endif - #if defined( DOUBLE_SIDED ) && ! defined( FLAT_SHADED ) - tbn[0] *= faceDirection; - tbn[1] *= faceDirection; - #endif -#endif -#ifdef USE_CLEARCOAT_NORMALMAP - #ifdef USE_TANGENT - mat3 tbn2 = mat3( normalize( vTangent ), normalize( vBitangent ), normal ); - #else - mat3 tbn2 = getTangentFrame( - vViewPosition, normal, vClearcoatNormalMapUv ); - #endif - #if defined( DOUBLE_SIDED ) && ! defined( FLAT_SHADED ) - tbn2[0] *= faceDirection; - tbn2[1] *= faceDirection; - #endif -#endif -vec3 nonPerturbedNormal = normal;`,g_=`#ifdef USE_NORMALMAP_OBJECTSPACE - normal = texture2D( normalMap, vNormalMapUv ).xyz * 2.0 - 1.0; - #ifdef FLIP_SIDED - normal = - normal; - #endif - #ifdef DOUBLE_SIDED - normal = normal * faceDirection; - #endif - normal = normalize( normalMatrix * normal ); -#elif defined( USE_NORMALMAP_TANGENTSPACE ) - vec3 mapN = texture2D( normalMap, vNormalMapUv ).xyz * 2.0 - 1.0; - mapN.xy *= normalScale; - normal = normalize( tbn * mapN ); -#elif defined( USE_BUMPMAP ) - normal = perturbNormalArb( - vViewPosition, normal, dHdxy_fwd(), faceDirection ); -#endif`,__=`#ifndef FLAT_SHADED - varying vec3 vNormal; - #ifdef USE_TANGENT - varying vec3 vTangent; - varying vec3 vBitangent; - #endif -#endif`,v_=`#ifndef FLAT_SHADED - varying vec3 vNormal; - #ifdef USE_TANGENT - varying vec3 vTangent; - varying vec3 vBitangent; - #endif -#endif`,x_=`#ifndef FLAT_SHADED - vNormal = normalize( transformedNormal ); - #ifdef USE_TANGENT - vTangent = normalize( transformedTangent ); - vBitangent = normalize( cross( vNormal, vTangent ) * tangent.w ); - #endif -#endif`,y_=`#ifdef USE_NORMALMAP - uniform sampler2D normalMap; - uniform vec2 normalScale; -#endif -#ifdef USE_NORMALMAP_OBJECTSPACE - uniform mat3 normalMatrix; -#endif -#if ! defined ( USE_TANGENT ) && ( defined ( USE_NORMALMAP_TANGENTSPACE ) || defined ( USE_CLEARCOAT_NORMALMAP ) || defined( USE_ANISOTROPY ) ) - mat3 getTangentFrame( vec3 eye_pos, vec3 surf_norm, vec2 uv ) { - vec3 q0 = dFdx( eye_pos.xyz ); - vec3 q1 = dFdy( eye_pos.xyz ); - vec2 st0 = dFdx( uv.st ); - vec2 st1 = dFdy( uv.st ); - vec3 N = surf_norm; - vec3 q1perp = cross( q1, N ); - vec3 q0perp = cross( N, q0 ); - vec3 T = q1perp * st0.x + q0perp * st1.x; - vec3 B = q1perp * st0.y + q0perp * st1.y; - float det = max( dot( T, T ), dot( B, B ) ); - float scale = ( det == 0.0 ) ? 0.0 : inversesqrt( det ); - return mat3( T * scale, B * scale, N ); - } -#endif`,M_=`#ifdef USE_CLEARCOAT - vec3 clearcoatNormal = nonPerturbedNormal; -#endif`,b_=`#ifdef USE_CLEARCOAT_NORMALMAP - vec3 clearcoatMapN = texture2D( clearcoatNormalMap, vClearcoatNormalMapUv ).xyz * 2.0 - 1.0; - clearcoatMapN.xy *= clearcoatNormalScale; - clearcoatNormal = normalize( tbn2 * clearcoatMapN ); -#endif`,S_=`#ifdef USE_CLEARCOATMAP - uniform sampler2D clearcoatMap; -#endif -#ifdef USE_CLEARCOAT_NORMALMAP - uniform sampler2D clearcoatNormalMap; - uniform vec2 clearcoatNormalScale; -#endif -#ifdef USE_CLEARCOAT_ROUGHNESSMAP - uniform sampler2D clearcoatRoughnessMap; -#endif`,w_=`#ifdef USE_IRIDESCENCEMAP - uniform sampler2D iridescenceMap; -#endif -#ifdef USE_IRIDESCENCE_THICKNESSMAP - uniform sampler2D iridescenceThicknessMap; -#endif`,E_=`#ifdef OPAQUE -diffuseColor.a = 1.0; -#endif -#ifdef USE_TRANSMISSION -diffuseColor.a *= material.transmissionAlpha; -#endif -gl_FragColor = vec4( outgoingLight, diffuseColor.a );`,T_=`vec3 packNormalToRGB( const in vec3 normal ) { - return normalize( normal ) * 0.5 + 0.5; -} -vec3 unpackRGBToNormal( const in vec3 rgb ) { - return 2.0 * rgb.xyz - 1.0; -} -const float PackUpscale = 256. / 255.;const float UnpackDownscale = 255. / 256.;const float ShiftRight8 = 1. / 256.; -const float Inv255 = 1. / 255.; -const vec4 PackFactors = vec4( 1.0, 256.0, 256.0 * 256.0, 256.0 * 256.0 * 256.0 ); -const vec2 UnpackFactors2 = vec2( UnpackDownscale, 1.0 / PackFactors.g ); -const vec3 UnpackFactors3 = vec3( UnpackDownscale / PackFactors.rg, 1.0 / PackFactors.b ); -const vec4 UnpackFactors4 = vec4( UnpackDownscale / PackFactors.rgb, 1.0 / PackFactors.a ); -vec4 packDepthToRGBA( const in float v ) { - if( v <= 0.0 ) - return vec4( 0., 0., 0., 0. ); - if( v >= 1.0 ) - return vec4( 1., 1., 1., 1. ); - float vuf; - float af = modf( v * PackFactors.a, vuf ); - float bf = modf( vuf * ShiftRight8, vuf ); - float gf = modf( vuf * ShiftRight8, vuf ); - return vec4( vuf * Inv255, gf * PackUpscale, bf * PackUpscale, af ); -} -vec3 packDepthToRGB( const in float v ) { - if( v <= 0.0 ) - return vec3( 0., 0., 0. ); - if( v >= 1.0 ) - return vec3( 1., 1., 1. ); - float vuf; - float bf = modf( v * PackFactors.b, vuf ); - float gf = modf( vuf * ShiftRight8, vuf ); - return vec3( vuf * Inv255, gf * PackUpscale, bf ); -} -vec2 packDepthToRG( const in float v ) { - if( v <= 0.0 ) - return vec2( 0., 0. ); - if( v >= 1.0 ) - return vec2( 1., 1. ); - float vuf; - float gf = modf( v * 256., vuf ); - return vec2( vuf * Inv255, gf ); -} -float unpackRGBAToDepth( const in vec4 v ) { - return dot( v, UnpackFactors4 ); -} -float unpackRGBToDepth( const in vec3 v ) { - return dot( v, UnpackFactors3 ); -} -float unpackRGToDepth( const in vec2 v ) { - return v.r * UnpackFactors2.r + v.g * UnpackFactors2.g; -} -vec4 pack2HalfToRGBA( const in vec2 v ) { - vec4 r = vec4( v.x, fract( v.x * 255.0 ), v.y, fract( v.y * 255.0 ) ); - return vec4( r.x - r.y / 255.0, r.y, r.z - r.w / 255.0, r.w ); -} -vec2 unpackRGBATo2Half( const in vec4 v ) { - return vec2( v.x + ( v.y / 255.0 ), v.z + ( v.w / 255.0 ) ); -} -float viewZToOrthographicDepth( const in float viewZ, const in float near, const in float far ) { - return ( viewZ + near ) / ( near - far ); -} -float orthographicDepthToViewZ( const in float depth, const in float near, const in float far ) { - return depth * ( near - far ) - near; -} -float viewZToPerspectiveDepth( const in float viewZ, const in float near, const in float far ) { - return ( ( near + viewZ ) * far ) / ( ( far - near ) * viewZ ); -} -float perspectiveDepthToViewZ( const in float depth, const in float near, const in float far ) { - return ( near * far ) / ( ( far - near ) * depth - far ); -}`,A_=`#ifdef PREMULTIPLIED_ALPHA - gl_FragColor.rgb *= gl_FragColor.a; -#endif`,C_=`vec4 mvPosition = vec4( transformed, 1.0 ); -#ifdef USE_BATCHING - mvPosition = batchingMatrix * mvPosition; -#endif -#ifdef USE_INSTANCING - mvPosition = instanceMatrix * mvPosition; -#endif -mvPosition = modelViewMatrix * mvPosition; -gl_Position = projectionMatrix * mvPosition;`,R_=`#ifdef DITHERING - gl_FragColor.rgb = dithering( gl_FragColor.rgb ); -#endif`,I_=`#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`,P_=`float roughnessFactor = roughness; -#ifdef USE_ROUGHNESSMAP - vec4 texelRoughness = texture2D( roughnessMap, vRoughnessMapUv ); - roughnessFactor *= texelRoughness.g; -#endif`,D_=`#ifdef USE_ROUGHNESSMAP - uniform sampler2D roughnessMap; -#endif`,L_=`#if NUM_SPOT_LIGHT_COORDS > 0 - varying vec4 vSpotLightCoord[ NUM_SPOT_LIGHT_COORDS ]; -#endif -#if NUM_SPOT_LIGHT_MAPS > 0 - uniform sampler2D spotLightMap[ NUM_SPOT_LIGHT_MAPS ]; -#endif -#ifdef USE_SHADOWMAP - #if NUM_DIR_LIGHT_SHADOWS > 0 - uniform sampler2D directionalShadowMap[ NUM_DIR_LIGHT_SHADOWS ]; - varying vec4 vDirectionalShadowCoord[ NUM_DIR_LIGHT_SHADOWS ]; - struct DirectionalLightShadow { - float shadowIntensity; - float shadowBias; - float shadowNormalBias; - float shadowRadius; - vec2 shadowMapSize; - }; - uniform DirectionalLightShadow directionalLightShadows[ NUM_DIR_LIGHT_SHADOWS ]; - #endif - #if NUM_SPOT_LIGHT_SHADOWS > 0 - uniform sampler2D spotShadowMap[ NUM_SPOT_LIGHT_SHADOWS ]; - struct SpotLightShadow { - float shadowIntensity; - float shadowBias; - float shadowNormalBias; - float shadowRadius; - vec2 shadowMapSize; - }; - uniform SpotLightShadow spotLightShadows[ NUM_SPOT_LIGHT_SHADOWS ]; - #endif - #if NUM_POINT_LIGHT_SHADOWS > 0 - uniform sampler2D pointShadowMap[ NUM_POINT_LIGHT_SHADOWS ]; - varying vec4 vPointShadowCoord[ NUM_POINT_LIGHT_SHADOWS ]; - struct PointLightShadow { - float shadowIntensity; - float shadowBias; - float shadowNormalBias; - float shadowRadius; - vec2 shadowMapSize; - float shadowCameraNear; - float shadowCameraFar; - }; - uniform PointLightShadow pointLightShadows[ NUM_POINT_LIGHT_SHADOWS ]; - #endif - float texture2DCompare( sampler2D depths, vec2 uv, float compare ) { - return step( compare, unpackRGBAToDepth( texture2D( depths, uv ) ) ); - } - vec2 texture2DDistribution( sampler2D shadow, vec2 uv ) { - return unpackRGBATo2Half( texture2D( shadow, uv ) ); - } - float VSMShadow (sampler2D shadow, vec2 uv, float compare ){ - float occlusion = 1.0; - vec2 distribution = texture2DDistribution( shadow, uv ); - float hard_shadow = step( compare , distribution.x ); - if (hard_shadow != 1.0 ) { - float distance = compare - distribution.x ; - float variance = max( 0.00000, distribution.y * distribution.y ); - float softness_probability = variance / (variance + distance * distance ); softness_probability = clamp( ( softness_probability - 0.3 ) / ( 0.95 - 0.3 ), 0.0, 1.0 ); occlusion = clamp( max( hard_shadow, softness_probability ), 0.0, 1.0 ); - } - return occlusion; - } - float getShadow( sampler2D shadowMap, vec2 shadowMapSize, float shadowIntensity, float shadowBias, float shadowRadius, vec4 shadowCoord ) { - float shadow = 1.0; - shadowCoord.xyz /= shadowCoord.w; - shadowCoord.z += shadowBias; - bool inFrustum = shadowCoord.x >= 0.0 && shadowCoord.x <= 1.0 && shadowCoord.y >= 0.0 && shadowCoord.y <= 1.0; - bool frustumTest = inFrustum && shadowCoord.z <= 1.0; - if ( frustumTest ) { - #if defined( SHADOWMAP_TYPE_PCF ) - vec2 texelSize = vec2( 1.0 ) / shadowMapSize; - float dx0 = - texelSize.x * shadowRadius; - float dy0 = - texelSize.y * shadowRadius; - float dx1 = + texelSize.x * shadowRadius; - float dy1 = + texelSize.y * shadowRadius; - float dx2 = dx0 / 2.0; - float dy2 = dy0 / 2.0; - float dx3 = dx1 / 2.0; - float dy3 = dy1 / 2.0; - shadow = ( - texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, dy0 ), shadowCoord.z ) + - texture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy0 ), shadowCoord.z ) + - texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, dy0 ), shadowCoord.z ) + - texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, dy2 ), shadowCoord.z ) + - texture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy2 ), shadowCoord.z ) + - texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, dy2 ), shadowCoord.z ) + - texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, 0.0 ), shadowCoord.z ) + - texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, 0.0 ), shadowCoord.z ) + - texture2DCompare( shadowMap, shadowCoord.xy, shadowCoord.z ) + - texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, 0.0 ), shadowCoord.z ) + - texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, 0.0 ), shadowCoord.z ) + - texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, dy3 ), shadowCoord.z ) + - texture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy3 ), shadowCoord.z ) + - texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, dy3 ), shadowCoord.z ) + - texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, dy1 ), shadowCoord.z ) + - texture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy1 ), shadowCoord.z ) + - texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, dy1 ), shadowCoord.z ) - ) * ( 1.0 / 17.0 ); - #elif defined( SHADOWMAP_TYPE_PCF_SOFT ) - vec2 texelSize = vec2( 1.0 ) / shadowMapSize; - float dx = texelSize.x; - float dy = texelSize.y; - vec2 uv = shadowCoord.xy; - vec2 f = fract( uv * shadowMapSize + 0.5 ); - uv -= f * texelSize; - shadow = ( - texture2DCompare( shadowMap, uv, shadowCoord.z ) + - texture2DCompare( shadowMap, uv + vec2( dx, 0.0 ), shadowCoord.z ) + - texture2DCompare( shadowMap, uv + vec2( 0.0, dy ), shadowCoord.z ) + - texture2DCompare( shadowMap, uv + texelSize, shadowCoord.z ) + - mix( texture2DCompare( shadowMap, uv + vec2( -dx, 0.0 ), shadowCoord.z ), - texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, 0.0 ), shadowCoord.z ), - f.x ) + - mix( texture2DCompare( shadowMap, uv + vec2( -dx, dy ), shadowCoord.z ), - texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, dy ), shadowCoord.z ), - f.x ) + - mix( texture2DCompare( shadowMap, uv + vec2( 0.0, -dy ), shadowCoord.z ), - texture2DCompare( shadowMap, uv + vec2( 0.0, 2.0 * dy ), shadowCoord.z ), - f.y ) + - mix( texture2DCompare( shadowMap, uv + vec2( dx, -dy ), shadowCoord.z ), - texture2DCompare( shadowMap, uv + vec2( dx, 2.0 * dy ), shadowCoord.z ), - f.y ) + - mix( mix( texture2DCompare( shadowMap, uv + vec2( -dx, -dy ), shadowCoord.z ), - texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, -dy ), shadowCoord.z ), - f.x ), - mix( texture2DCompare( shadowMap, uv + vec2( -dx, 2.0 * dy ), shadowCoord.z ), - texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, 2.0 * dy ), shadowCoord.z ), - f.x ), - f.y ) - ) * ( 1.0 / 9.0 ); - #elif defined( SHADOWMAP_TYPE_VSM ) - shadow = VSMShadow( shadowMap, shadowCoord.xy, shadowCoord.z ); - #else - shadow = texture2DCompare( shadowMap, shadowCoord.xy, shadowCoord.z ); - #endif - } - return mix( 1.0, shadow, shadowIntensity ); - } - vec2 cubeToUV( vec3 v, float texelSizeY ) { - vec3 absV = abs( v ); - float scaleToCube = 1.0 / max( absV.x, max( absV.y, absV.z ) ); - absV *= scaleToCube; - v *= scaleToCube * ( 1.0 - 2.0 * texelSizeY ); - vec2 planar = v.xy; - float almostATexel = 1.5 * texelSizeY; - float almostOne = 1.0 - almostATexel; - if ( absV.z >= almostOne ) { - if ( v.z > 0.0 ) - planar.x = 4.0 - v.x; - } else if ( absV.x >= almostOne ) { - float signX = sign( v.x ); - planar.x = v.z * signX + 2.0 * signX; - } else if ( absV.y >= almostOne ) { - float signY = sign( v.y ); - planar.x = v.x + 2.0 * signY + 2.0; - planar.y = v.z * signY - 2.0; - } - return vec2( 0.125, 0.25 ) * planar + vec2( 0.375, 0.75 ); - } - float getPointShadow( sampler2D shadowMap, vec2 shadowMapSize, float shadowIntensity, float shadowBias, float shadowRadius, vec4 shadowCoord, float shadowCameraNear, float shadowCameraFar ) { - float shadow = 1.0; - vec3 lightToPosition = shadowCoord.xyz; - - float lightToPositionLength = length( lightToPosition ); - if ( lightToPositionLength - shadowCameraFar <= 0.0 && lightToPositionLength - shadowCameraNear >= 0.0 ) { - float dp = ( lightToPositionLength - shadowCameraNear ) / ( shadowCameraFar - shadowCameraNear ); dp += shadowBias; - vec3 bd3D = normalize( lightToPosition ); - vec2 texelSize = vec2( 1.0 ) / ( shadowMapSize * vec2( 4.0, 2.0 ) ); - #if defined( SHADOWMAP_TYPE_PCF ) || defined( SHADOWMAP_TYPE_PCF_SOFT ) || defined( SHADOWMAP_TYPE_VSM ) - vec2 offset = vec2( - 1, 1 ) * shadowRadius * texelSize.y; - shadow = ( - texture2DCompare( shadowMap, cubeToUV( bd3D + offset.xyy, texelSize.y ), dp ) + - texture2DCompare( shadowMap, cubeToUV( bd3D + offset.yyy, texelSize.y ), dp ) + - texture2DCompare( shadowMap, cubeToUV( bd3D + offset.xyx, texelSize.y ), dp ) + - texture2DCompare( shadowMap, cubeToUV( bd3D + offset.yyx, texelSize.y ), dp ) + - texture2DCompare( shadowMap, cubeToUV( bd3D, texelSize.y ), dp ) + - texture2DCompare( shadowMap, cubeToUV( bd3D + offset.xxy, texelSize.y ), dp ) + - texture2DCompare( shadowMap, cubeToUV( bd3D + offset.yxy, texelSize.y ), dp ) + - texture2DCompare( shadowMap, cubeToUV( bd3D + offset.xxx, texelSize.y ), dp ) + - texture2DCompare( shadowMap, cubeToUV( bd3D + offset.yxx, texelSize.y ), dp ) - ) * ( 1.0 / 9.0 ); - #else - shadow = texture2DCompare( shadowMap, cubeToUV( bd3D, texelSize.y ), dp ); - #endif - } - return mix( 1.0, shadow, shadowIntensity ); - } -#endif`,U_=`#if NUM_SPOT_LIGHT_COORDS > 0 - uniform mat4 spotLightMatrix[ NUM_SPOT_LIGHT_COORDS ]; - varying vec4 vSpotLightCoord[ NUM_SPOT_LIGHT_COORDS ]; -#endif -#ifdef USE_SHADOWMAP - #if NUM_DIR_LIGHT_SHADOWS > 0 - uniform mat4 directionalShadowMatrix[ NUM_DIR_LIGHT_SHADOWS ]; - varying vec4 vDirectionalShadowCoord[ NUM_DIR_LIGHT_SHADOWS ]; - struct DirectionalLightShadow { - float shadowIntensity; - float shadowBias; - float shadowNormalBias; - float shadowRadius; - vec2 shadowMapSize; - }; - uniform DirectionalLightShadow directionalLightShadows[ NUM_DIR_LIGHT_SHADOWS ]; - #endif - #if NUM_SPOT_LIGHT_SHADOWS > 0 - struct SpotLightShadow { - float shadowIntensity; - float shadowBias; - float shadowNormalBias; - float shadowRadius; - vec2 shadowMapSize; - }; - uniform SpotLightShadow spotLightShadows[ NUM_SPOT_LIGHT_SHADOWS ]; - #endif - #if NUM_POINT_LIGHT_SHADOWS > 0 - uniform mat4 pointShadowMatrix[ NUM_POINT_LIGHT_SHADOWS ]; - varying vec4 vPointShadowCoord[ NUM_POINT_LIGHT_SHADOWS ]; - struct PointLightShadow { - float shadowIntensity; - float shadowBias; - float shadowNormalBias; - float shadowRadius; - vec2 shadowMapSize; - float shadowCameraNear; - float shadowCameraFar; - }; - uniform PointLightShadow pointLightShadows[ NUM_POINT_LIGHT_SHADOWS ]; - #endif -#endif`,N_=`#if ( defined( USE_SHADOWMAP ) && ( NUM_DIR_LIGHT_SHADOWS > 0 || NUM_POINT_LIGHT_SHADOWS > 0 ) ) || ( NUM_SPOT_LIGHT_COORDS > 0 ) - vec3 shadowWorldNormal = inverseTransformDirection( transformedNormal, viewMatrix ); - vec4 shadowWorldPosition; -#endif -#if defined( USE_SHADOWMAP ) - #if NUM_DIR_LIGHT_SHADOWS > 0 - #pragma unroll_loop_start - for ( int i = 0; i < NUM_DIR_LIGHT_SHADOWS; i ++ ) { - shadowWorldPosition = worldPosition + vec4( shadowWorldNormal * directionalLightShadows[ i ].shadowNormalBias, 0 ); - vDirectionalShadowCoord[ i ] = directionalShadowMatrix[ i ] * shadowWorldPosition; - } - #pragma unroll_loop_end - #endif - #if NUM_POINT_LIGHT_SHADOWS > 0 - #pragma unroll_loop_start - for ( int i = 0; i < NUM_POINT_LIGHT_SHADOWS; i ++ ) { - shadowWorldPosition = worldPosition + vec4( shadowWorldNormal * pointLightShadows[ i ].shadowNormalBias, 0 ); - vPointShadowCoord[ i ] = pointShadowMatrix[ i ] * shadowWorldPosition; - } - #pragma unroll_loop_end - #endif -#endif -#if NUM_SPOT_LIGHT_COORDS > 0 - #pragma unroll_loop_start - for ( int i = 0; i < NUM_SPOT_LIGHT_COORDS; i ++ ) { - shadowWorldPosition = worldPosition; - #if ( defined( USE_SHADOWMAP ) && UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS ) - shadowWorldPosition.xyz += shadowWorldNormal * spotLightShadows[ i ].shadowNormalBias; - #endif - vSpotLightCoord[ i ] = spotLightMatrix[ i ] * shadowWorldPosition; - } - #pragma unroll_loop_end -#endif`,F_=`float getShadowMask() { - float shadow = 1.0; - #ifdef USE_SHADOWMAP - #if NUM_DIR_LIGHT_SHADOWS > 0 - DirectionalLightShadow directionalLight; - #pragma unroll_loop_start - for ( int i = 0; i < NUM_DIR_LIGHT_SHADOWS; i ++ ) { - directionalLight = directionalLightShadows[ i ]; - shadow *= receiveShadow ? getShadow( directionalShadowMap[ i ], directionalLight.shadowMapSize, directionalLight.shadowIntensity, directionalLight.shadowBias, directionalLight.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0; - } - #pragma unroll_loop_end - #endif - #if NUM_SPOT_LIGHT_SHADOWS > 0 - SpotLightShadow spotLight; - #pragma unroll_loop_start - for ( int i = 0; i < NUM_SPOT_LIGHT_SHADOWS; i ++ ) { - spotLight = spotLightShadows[ i ]; - shadow *= receiveShadow ? getShadow( spotShadowMap[ i ], spotLight.shadowMapSize, spotLight.shadowIntensity, spotLight.shadowBias, spotLight.shadowRadius, vSpotLightCoord[ i ] ) : 1.0; - } - #pragma unroll_loop_end - #endif - #if NUM_POINT_LIGHT_SHADOWS > 0 - PointLightShadow pointLight; - #pragma unroll_loop_start - for ( int i = 0; i < NUM_POINT_LIGHT_SHADOWS; i ++ ) { - pointLight = pointLightShadows[ i ]; - shadow *= receiveShadow ? getPointShadow( pointShadowMap[ i ], pointLight.shadowMapSize, pointLight.shadowIntensity, pointLight.shadowBias, pointLight.shadowRadius, vPointShadowCoord[ i ], pointLight.shadowCameraNear, pointLight.shadowCameraFar ) : 1.0; - } - #pragma unroll_loop_end - #endif - #endif - return shadow; -}`,O_=`#ifdef USE_SKINNING - mat4 boneMatX = getBoneMatrix( skinIndex.x ); - mat4 boneMatY = getBoneMatrix( skinIndex.y ); - mat4 boneMatZ = getBoneMatrix( skinIndex.z ); - mat4 boneMatW = getBoneMatrix( skinIndex.w ); -#endif`,B_=`#ifdef USE_SKINNING - uniform mat4 bindMatrix; - uniform mat4 bindMatrixInverse; - uniform highp sampler2D boneTexture; - mat4 getBoneMatrix( const in float i ) { - int size = textureSize( boneTexture, 0 ).x; - int j = int( i ) * 4; - int x = j % size; - int y = j / size; - vec4 v1 = texelFetch( boneTexture, ivec2( x, y ), 0 ); - vec4 v2 = texelFetch( boneTexture, ivec2( x + 1, y ), 0 ); - vec4 v3 = texelFetch( boneTexture, ivec2( x + 2, y ), 0 ); - vec4 v4 = texelFetch( boneTexture, ivec2( x + 3, y ), 0 ); - return mat4( v1, v2, v3, v4 ); - } -#endif`,z_=`#ifdef USE_SKINNING - vec4 skinVertex = bindMatrix * vec4( transformed, 1.0 ); - vec4 skinned = vec4( 0.0 ); - skinned += boneMatX * skinVertex * skinWeight.x; - skinned += boneMatY * skinVertex * skinWeight.y; - skinned += boneMatZ * skinVertex * skinWeight.z; - skinned += boneMatW * skinVertex * skinWeight.w; - transformed = ( bindMatrixInverse * skinned ).xyz; -#endif`,k_=`#ifdef USE_SKINNING - mat4 skinMatrix = mat4( 0.0 ); - skinMatrix += skinWeight.x * boneMatX; - skinMatrix += skinWeight.y * boneMatY; - skinMatrix += skinWeight.z * boneMatZ; - skinMatrix += skinWeight.w * boneMatW; - skinMatrix = bindMatrixInverse * skinMatrix * bindMatrix; - objectNormal = vec4( skinMatrix * vec4( objectNormal, 0.0 ) ).xyz; - #ifdef USE_TANGENT - objectTangent = vec4( skinMatrix * vec4( objectTangent, 0.0 ) ).xyz; - #endif -#endif`,V_=`float specularStrength; -#ifdef USE_SPECULARMAP - vec4 texelSpecular = texture2D( specularMap, vSpecularMapUv ); - specularStrength = texelSpecular.r; -#else - specularStrength = 1.0; -#endif`,H_=`#ifdef USE_SPECULARMAP - uniform sampler2D specularMap; -#endif`,G_=`#if defined( TONE_MAPPING ) - gl_FragColor.rgb = toneMapping( gl_FragColor.rgb ); -#endif`,W_=`#ifndef saturate -#define saturate( a ) clamp( a, 0.0, 1.0 ) -#endif -uniform float toneMappingExposure; -vec3 LinearToneMapping( vec3 color ) { - return saturate( toneMappingExposure * color ); -} -vec3 ReinhardToneMapping( vec3 color ) { - color *= toneMappingExposure; - return saturate( color / ( vec3( 1.0 ) + color ) ); -} -vec3 CineonToneMapping( vec3 color ) { - color *= toneMappingExposure; - color = max( vec3( 0.0 ), color - 0.004 ); - return pow( ( color * ( 6.2 * color + 0.5 ) ) / ( color * ( 6.2 * color + 1.7 ) + 0.06 ), vec3( 2.2 ) ); -} -vec3 RRTAndODTFit( vec3 v ) { - vec3 a = v * ( v + 0.0245786 ) - 0.000090537; - vec3 b = v * ( 0.983729 * v + 0.4329510 ) + 0.238081; - return a / b; -} -vec3 ACESFilmicToneMapping( vec3 color ) { - const mat3 ACESInputMat = mat3( - vec3( 0.59719, 0.07600, 0.02840 ), vec3( 0.35458, 0.90834, 0.13383 ), - vec3( 0.04823, 0.01566, 0.83777 ) - ); - const mat3 ACESOutputMat = mat3( - vec3( 1.60475, -0.10208, -0.00327 ), vec3( -0.53108, 1.10813, -0.07276 ), - vec3( -0.07367, -0.00605, 1.07602 ) - ); - color *= toneMappingExposure / 0.6; - color = ACESInputMat * color; - color = RRTAndODTFit( color ); - color = ACESOutputMat * color; - return saturate( color ); -} -const mat3 LINEAR_REC2020_TO_LINEAR_SRGB = mat3( - vec3( 1.6605, - 0.1246, - 0.0182 ), - vec3( - 0.5876, 1.1329, - 0.1006 ), - vec3( - 0.0728, - 0.0083, 1.1187 ) -); -const mat3 LINEAR_SRGB_TO_LINEAR_REC2020 = mat3( - vec3( 0.6274, 0.0691, 0.0164 ), - vec3( 0.3293, 0.9195, 0.0880 ), - vec3( 0.0433, 0.0113, 0.8956 ) -); -vec3 agxDefaultContrastApprox( vec3 x ) { - vec3 x2 = x * x; - vec3 x4 = x2 * x2; - return + 15.5 * x4 * x2 - - 40.14 * x4 * x - + 31.96 * x4 - - 6.868 * x2 * x - + 0.4298 * x2 - + 0.1191 * x - - 0.00232; -} -vec3 AgXToneMapping( vec3 color ) { - const mat3 AgXInsetMatrix = mat3( - vec3( 0.856627153315983, 0.137318972929847, 0.11189821299995 ), - vec3( 0.0951212405381588, 0.761241990602591, 0.0767994186031903 ), - vec3( 0.0482516061458583, 0.101439036467562, 0.811302368396859 ) - ); - const mat3 AgXOutsetMatrix = mat3( - vec3( 1.1271005818144368, - 0.1413297634984383, - 0.14132976349843826 ), - vec3( - 0.11060664309660323, 1.157823702216272, - 0.11060664309660294 ), - vec3( - 0.016493938717834573, - 0.016493938717834257, 1.2519364065950405 ) - ); - const float AgxMinEv = - 12.47393; const float AgxMaxEv = 4.026069; - color *= toneMappingExposure; - color = LINEAR_SRGB_TO_LINEAR_REC2020 * color; - color = AgXInsetMatrix * color; - color = max( color, 1e-10 ); color = log2( color ); - color = ( color - AgxMinEv ) / ( AgxMaxEv - AgxMinEv ); - color = clamp( color, 0.0, 1.0 ); - color = agxDefaultContrastApprox( color ); - color = AgXOutsetMatrix * color; - color = pow( max( vec3( 0.0 ), color ), vec3( 2.2 ) ); - color = LINEAR_REC2020_TO_LINEAR_SRGB * color; - color = clamp( color, 0.0, 1.0 ); - return color; -} -vec3 NeutralToneMapping( vec3 color ) { - const float StartCompression = 0.8 - 0.04; - const float Desaturation = 0.15; - color *= toneMappingExposure; - float x = min( color.r, min( color.g, color.b ) ); - float offset = x < 0.08 ? x - 6.25 * x * x : 0.04; - color -= offset; - float peak = max( color.r, max( color.g, color.b ) ); - if ( peak < StartCompression ) return color; - float d = 1. - StartCompression; - float newPeak = 1. - d * d / ( peak + d - StartCompression ); - color *= newPeak / peak; - float g = 1. - 1. / ( Desaturation * ( peak - newPeak ) + 1. ); - return mix( color, vec3( newPeak ), g ); -} -vec3 CustomToneMapping( vec3 color ) { return color; }`,X_=`#ifdef USE_TRANSMISSION - material.transmission = transmission; - material.transmissionAlpha = 1.0; - material.thickness = thickness; - material.attenuationDistance = attenuationDistance; - material.attenuationColor = attenuationColor; - #ifdef USE_TRANSMISSIONMAP - material.transmission *= texture2D( transmissionMap, vTransmissionMapUv ).r; - #endif - #ifdef USE_THICKNESSMAP - material.thickness *= texture2D( thicknessMap, vThicknessMapUv ).g; - #endif - vec3 pos = vWorldPosition; - vec3 v = normalize( cameraPosition - pos ); - vec3 n = inverseTransformDirection( normal, viewMatrix ); - vec4 transmitted = getIBLVolumeRefraction( - n, v, material.roughness, material.diffuseColor, material.specularColor, material.specularF90, - pos, modelMatrix, viewMatrix, projectionMatrix, material.dispersion, material.ior, material.thickness, - material.attenuationColor, material.attenuationDistance ); - material.transmissionAlpha = mix( material.transmissionAlpha, transmitted.a, material.transmission ); - totalDiffuse = mix( totalDiffuse, transmitted.rgb, material.transmission ); -#endif`,q_=`#ifdef USE_TRANSMISSION - uniform float transmission; - uniform float thickness; - uniform float attenuationDistance; - uniform vec3 attenuationColor; - #ifdef USE_TRANSMISSIONMAP - uniform sampler2D transmissionMap; - #endif - #ifdef USE_THICKNESSMAP - uniform sampler2D thicknessMap; - #endif - uniform vec2 transmissionSamplerSize; - uniform sampler2D transmissionSamplerMap; - uniform mat4 modelMatrix; - uniform mat4 projectionMatrix; - varying vec3 vWorldPosition; - float w0( float a ) { - return ( 1.0 / 6.0 ) * ( a * ( a * ( - a + 3.0 ) - 3.0 ) + 1.0 ); - } - float w1( float a ) { - return ( 1.0 / 6.0 ) * ( a * a * ( 3.0 * a - 6.0 ) + 4.0 ); - } - float w2( float a ){ - return ( 1.0 / 6.0 ) * ( a * ( a * ( - 3.0 * a + 3.0 ) + 3.0 ) + 1.0 ); - } - float w3( float a ) { - return ( 1.0 / 6.0 ) * ( a * a * a ); - } - float g0( float a ) { - return w0( a ) + w1( a ); - } - float g1( float a ) { - return w2( a ) + w3( a ); - } - float h0( float a ) { - return - 1.0 + w1( a ) / ( w0( a ) + w1( a ) ); - } - float h1( float a ) { - return 1.0 + w3( a ) / ( w2( a ) + w3( a ) ); - } - vec4 bicubic( sampler2D tex, vec2 uv, vec4 texelSize, float lod ) { - uv = uv * texelSize.zw + 0.5; - vec2 iuv = floor( uv ); - vec2 fuv = fract( uv ); - float g0x = g0( fuv.x ); - float g1x = g1( fuv.x ); - float h0x = h0( fuv.x ); - float h1x = h1( fuv.x ); - float h0y = h0( fuv.y ); - float h1y = h1( fuv.y ); - vec2 p0 = ( vec2( iuv.x + h0x, iuv.y + h0y ) - 0.5 ) * texelSize.xy; - vec2 p1 = ( vec2( iuv.x + h1x, iuv.y + h0y ) - 0.5 ) * texelSize.xy; - vec2 p2 = ( vec2( iuv.x + h0x, iuv.y + h1y ) - 0.5 ) * texelSize.xy; - vec2 p3 = ( vec2( iuv.x + h1x, iuv.y + h1y ) - 0.5 ) * texelSize.xy; - return g0( fuv.y ) * ( g0x * textureLod( tex, p0, lod ) + g1x * textureLod( tex, p1, lod ) ) + - g1( fuv.y ) * ( g0x * textureLod( tex, p2, lod ) + g1x * textureLod( tex, p3, lod ) ); - } - vec4 textureBicubic( sampler2D sampler, vec2 uv, float lod ) { - vec2 fLodSize = vec2( textureSize( sampler, int( lod ) ) ); - vec2 cLodSize = vec2( textureSize( sampler, int( lod + 1.0 ) ) ); - vec2 fLodSizeInv = 1.0 / fLodSize; - vec2 cLodSizeInv = 1.0 / cLodSize; - vec4 fSample = bicubic( sampler, uv, vec4( fLodSizeInv, fLodSize ), floor( lod ) ); - vec4 cSample = bicubic( sampler, uv, vec4( cLodSizeInv, cLodSize ), ceil( lod ) ); - return mix( fSample, cSample, fract( lod ) ); - } - vec3 getVolumeTransmissionRay( const in vec3 n, const in vec3 v, const in float thickness, const in float ior, const in mat4 modelMatrix ) { - vec3 refractionVector = refract( - v, normalize( n ), 1.0 / ior ); - vec3 modelScale; - modelScale.x = length( vec3( modelMatrix[ 0 ].xyz ) ); - modelScale.y = length( vec3( modelMatrix[ 1 ].xyz ) ); - modelScale.z = length( vec3( modelMatrix[ 2 ].xyz ) ); - return normalize( refractionVector ) * thickness * modelScale; - } - float applyIorToRoughness( const in float roughness, const in float ior ) { - return roughness * clamp( ior * 2.0 - 2.0, 0.0, 1.0 ); - } - vec4 getTransmissionSample( const in vec2 fragCoord, const in float roughness, const in float ior ) { - float lod = log2( transmissionSamplerSize.x ) * applyIorToRoughness( roughness, ior ); - return textureBicubic( transmissionSamplerMap, fragCoord.xy, lod ); - } - vec3 volumeAttenuation( const in float transmissionDistance, const in vec3 attenuationColor, const in float attenuationDistance ) { - if ( isinf( attenuationDistance ) ) { - return vec3( 1.0 ); - } else { - vec3 attenuationCoefficient = -log( attenuationColor ) / attenuationDistance; - vec3 transmittance = exp( - attenuationCoefficient * transmissionDistance ); return transmittance; - } - } - vec4 getIBLVolumeRefraction( const in vec3 n, const in vec3 v, const in float roughness, const in vec3 diffuseColor, - const in vec3 specularColor, const in float specularF90, const in vec3 position, const in mat4 modelMatrix, - const in mat4 viewMatrix, const in mat4 projMatrix, const in float dispersion, const in float ior, const in float thickness, - const in vec3 attenuationColor, const in float attenuationDistance ) { - vec4 transmittedLight; - vec3 transmittance; - #ifdef USE_DISPERSION - float halfSpread = ( ior - 1.0 ) * 0.025 * dispersion; - vec3 iors = vec3( ior - halfSpread, ior, ior + halfSpread ); - for ( int i = 0; i < 3; i ++ ) { - vec3 transmissionRay = getVolumeTransmissionRay( n, v, thickness, iors[ i ], modelMatrix ); - vec3 refractedRayExit = position + transmissionRay; - vec4 ndcPos = projMatrix * viewMatrix * vec4( refractedRayExit, 1.0 ); - vec2 refractionCoords = ndcPos.xy / ndcPos.w; - refractionCoords += 1.0; - refractionCoords /= 2.0; - vec4 transmissionSample = getTransmissionSample( refractionCoords, roughness, iors[ i ] ); - transmittedLight[ i ] = transmissionSample[ i ]; - transmittedLight.a += transmissionSample.a; - transmittance[ i ] = diffuseColor[ i ] * volumeAttenuation( length( transmissionRay ), attenuationColor, attenuationDistance )[ i ]; - } - transmittedLight.a /= 3.0; - #else - vec3 transmissionRay = getVolumeTransmissionRay( n, v, thickness, ior, modelMatrix ); - vec3 refractedRayExit = position + transmissionRay; - vec4 ndcPos = projMatrix * viewMatrix * vec4( refractedRayExit, 1.0 ); - vec2 refractionCoords = ndcPos.xy / ndcPos.w; - refractionCoords += 1.0; - refractionCoords /= 2.0; - transmittedLight = getTransmissionSample( refractionCoords, roughness, ior ); - transmittance = diffuseColor * volumeAttenuation( length( transmissionRay ), attenuationColor, attenuationDistance ); - #endif - vec3 attenuatedColor = transmittance * transmittedLight.rgb; - vec3 F = EnvironmentBRDF( n, v, specularColor, specularF90, roughness ); - float transmittanceFactor = ( transmittance.r + transmittance.g + transmittance.b ) / 3.0; - return vec4( ( 1.0 - F ) * attenuatedColor, 1.0 - ( 1.0 - transmittedLight.a ) * transmittanceFactor ); - } -#endif`,Y_=`#if defined( USE_UV ) || defined( USE_ANISOTROPY ) - varying vec2 vUv; -#endif -#ifdef USE_MAP - varying vec2 vMapUv; -#endif -#ifdef USE_ALPHAMAP - varying vec2 vAlphaMapUv; -#endif -#ifdef USE_LIGHTMAP - varying vec2 vLightMapUv; -#endif -#ifdef USE_AOMAP - varying vec2 vAoMapUv; -#endif -#ifdef USE_BUMPMAP - varying vec2 vBumpMapUv; -#endif -#ifdef USE_NORMALMAP - varying vec2 vNormalMapUv; -#endif -#ifdef USE_EMISSIVEMAP - varying vec2 vEmissiveMapUv; -#endif -#ifdef USE_METALNESSMAP - varying vec2 vMetalnessMapUv; -#endif -#ifdef USE_ROUGHNESSMAP - varying vec2 vRoughnessMapUv; -#endif -#ifdef USE_ANISOTROPYMAP - varying vec2 vAnisotropyMapUv; -#endif -#ifdef USE_CLEARCOATMAP - varying vec2 vClearcoatMapUv; -#endif -#ifdef USE_CLEARCOAT_NORMALMAP - varying vec2 vClearcoatNormalMapUv; -#endif -#ifdef USE_CLEARCOAT_ROUGHNESSMAP - varying vec2 vClearcoatRoughnessMapUv; -#endif -#ifdef USE_IRIDESCENCEMAP - varying vec2 vIridescenceMapUv; -#endif -#ifdef USE_IRIDESCENCE_THICKNESSMAP - varying vec2 vIridescenceThicknessMapUv; -#endif -#ifdef USE_SHEEN_COLORMAP - varying vec2 vSheenColorMapUv; -#endif -#ifdef USE_SHEEN_ROUGHNESSMAP - varying vec2 vSheenRoughnessMapUv; -#endif -#ifdef USE_SPECULARMAP - varying vec2 vSpecularMapUv; -#endif -#ifdef USE_SPECULAR_COLORMAP - varying vec2 vSpecularColorMapUv; -#endif -#ifdef USE_SPECULAR_INTENSITYMAP - varying vec2 vSpecularIntensityMapUv; -#endif -#ifdef USE_TRANSMISSIONMAP - uniform mat3 transmissionMapTransform; - varying vec2 vTransmissionMapUv; -#endif -#ifdef USE_THICKNESSMAP - uniform mat3 thicknessMapTransform; - varying vec2 vThicknessMapUv; -#endif`,Z_=`#if defined( USE_UV ) || defined( USE_ANISOTROPY ) - varying vec2 vUv; -#endif -#ifdef USE_MAP - uniform mat3 mapTransform; - varying vec2 vMapUv; -#endif -#ifdef USE_ALPHAMAP - uniform mat3 alphaMapTransform; - varying vec2 vAlphaMapUv; -#endif -#ifdef USE_LIGHTMAP - uniform mat3 lightMapTransform; - varying vec2 vLightMapUv; -#endif -#ifdef USE_AOMAP - uniform mat3 aoMapTransform; - varying vec2 vAoMapUv; -#endif -#ifdef USE_BUMPMAP - uniform mat3 bumpMapTransform; - varying vec2 vBumpMapUv; -#endif -#ifdef USE_NORMALMAP - uniform mat3 normalMapTransform; - varying vec2 vNormalMapUv; -#endif -#ifdef USE_DISPLACEMENTMAP - uniform mat3 displacementMapTransform; - varying vec2 vDisplacementMapUv; -#endif -#ifdef USE_EMISSIVEMAP - uniform mat3 emissiveMapTransform; - varying vec2 vEmissiveMapUv; -#endif -#ifdef USE_METALNESSMAP - uniform mat3 metalnessMapTransform; - varying vec2 vMetalnessMapUv; -#endif -#ifdef USE_ROUGHNESSMAP - uniform mat3 roughnessMapTransform; - varying vec2 vRoughnessMapUv; -#endif -#ifdef USE_ANISOTROPYMAP - uniform mat3 anisotropyMapTransform; - varying vec2 vAnisotropyMapUv; -#endif -#ifdef USE_CLEARCOATMAP - uniform mat3 clearcoatMapTransform; - varying vec2 vClearcoatMapUv; -#endif -#ifdef USE_CLEARCOAT_NORMALMAP - uniform mat3 clearcoatNormalMapTransform; - varying vec2 vClearcoatNormalMapUv; -#endif -#ifdef USE_CLEARCOAT_ROUGHNESSMAP - uniform mat3 clearcoatRoughnessMapTransform; - varying vec2 vClearcoatRoughnessMapUv; -#endif -#ifdef USE_SHEEN_COLORMAP - uniform mat3 sheenColorMapTransform; - varying vec2 vSheenColorMapUv; -#endif -#ifdef USE_SHEEN_ROUGHNESSMAP - uniform mat3 sheenRoughnessMapTransform; - varying vec2 vSheenRoughnessMapUv; -#endif -#ifdef USE_IRIDESCENCEMAP - uniform mat3 iridescenceMapTransform; - varying vec2 vIridescenceMapUv; -#endif -#ifdef USE_IRIDESCENCE_THICKNESSMAP - uniform mat3 iridescenceThicknessMapTransform; - varying vec2 vIridescenceThicknessMapUv; -#endif -#ifdef USE_SPECULARMAP - uniform mat3 specularMapTransform; - varying vec2 vSpecularMapUv; -#endif -#ifdef USE_SPECULAR_COLORMAP - uniform mat3 specularColorMapTransform; - varying vec2 vSpecularColorMapUv; -#endif -#ifdef USE_SPECULAR_INTENSITYMAP - uniform mat3 specularIntensityMapTransform; - varying vec2 vSpecularIntensityMapUv; -#endif -#ifdef USE_TRANSMISSIONMAP - uniform mat3 transmissionMapTransform; - varying vec2 vTransmissionMapUv; -#endif -#ifdef USE_THICKNESSMAP - uniform mat3 thicknessMapTransform; - varying vec2 vThicknessMapUv; -#endif`,J_=`#if defined( USE_UV ) || defined( USE_ANISOTROPY ) - vUv = vec3( uv, 1 ).xy; -#endif -#ifdef USE_MAP - vMapUv = ( mapTransform * vec3( MAP_UV, 1 ) ).xy; -#endif -#ifdef USE_ALPHAMAP - vAlphaMapUv = ( alphaMapTransform * vec3( ALPHAMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_LIGHTMAP - vLightMapUv = ( lightMapTransform * vec3( LIGHTMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_AOMAP - vAoMapUv = ( aoMapTransform * vec3( AOMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_BUMPMAP - vBumpMapUv = ( bumpMapTransform * vec3( BUMPMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_NORMALMAP - vNormalMapUv = ( normalMapTransform * vec3( NORMALMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_DISPLACEMENTMAP - vDisplacementMapUv = ( displacementMapTransform * vec3( DISPLACEMENTMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_EMISSIVEMAP - vEmissiveMapUv = ( emissiveMapTransform * vec3( EMISSIVEMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_METALNESSMAP - vMetalnessMapUv = ( metalnessMapTransform * vec3( METALNESSMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_ROUGHNESSMAP - vRoughnessMapUv = ( roughnessMapTransform * vec3( ROUGHNESSMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_ANISOTROPYMAP - vAnisotropyMapUv = ( anisotropyMapTransform * vec3( ANISOTROPYMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_CLEARCOATMAP - vClearcoatMapUv = ( clearcoatMapTransform * vec3( CLEARCOATMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_CLEARCOAT_NORMALMAP - vClearcoatNormalMapUv = ( clearcoatNormalMapTransform * vec3( CLEARCOAT_NORMALMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_CLEARCOAT_ROUGHNESSMAP - vClearcoatRoughnessMapUv = ( clearcoatRoughnessMapTransform * vec3( CLEARCOAT_ROUGHNESSMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_IRIDESCENCEMAP - vIridescenceMapUv = ( iridescenceMapTransform * vec3( IRIDESCENCEMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_IRIDESCENCE_THICKNESSMAP - vIridescenceThicknessMapUv = ( iridescenceThicknessMapTransform * vec3( IRIDESCENCE_THICKNESSMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_SHEEN_COLORMAP - vSheenColorMapUv = ( sheenColorMapTransform * vec3( SHEEN_COLORMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_SHEEN_ROUGHNESSMAP - vSheenRoughnessMapUv = ( sheenRoughnessMapTransform * vec3( SHEEN_ROUGHNESSMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_SPECULARMAP - vSpecularMapUv = ( specularMapTransform * vec3( SPECULARMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_SPECULAR_COLORMAP - vSpecularColorMapUv = ( specularColorMapTransform * vec3( SPECULAR_COLORMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_SPECULAR_INTENSITYMAP - vSpecularIntensityMapUv = ( specularIntensityMapTransform * vec3( SPECULAR_INTENSITYMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_TRANSMISSIONMAP - vTransmissionMapUv = ( transmissionMapTransform * vec3( TRANSMISSIONMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_THICKNESSMAP - vThicknessMapUv = ( thicknessMapTransform * vec3( THICKNESSMAP_UV, 1 ) ).xy; -#endif`,K_=`#if defined( USE_ENVMAP ) || defined( DISTANCE ) || defined ( USE_SHADOWMAP ) || defined ( USE_TRANSMISSION ) || NUM_SPOT_LIGHT_COORDS > 0 - vec4 worldPosition = vec4( transformed, 1.0 ); - #ifdef USE_BATCHING - worldPosition = batchingMatrix * worldPosition; - #endif - #ifdef USE_INSTANCING - worldPosition = instanceMatrix * worldPosition; - #endif - worldPosition = modelMatrix * worldPosition; -#endif`;const $_=`varying vec2 vUv; -uniform mat3 uvTransform; -void main() { - vUv = ( uvTransform * vec3( uv, 1 ) ).xy; - gl_Position = vec4( position.xy, 1.0, 1.0 ); -}`,j_=`uniform sampler2D t2D; -uniform float backgroundIntensity; -varying vec2 vUv; -void main() { - vec4 texColor = texture2D( t2D, vUv ); - #ifdef DECODE_VIDEO_TEXTURE - texColor = vec4( mix( pow( texColor.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), texColor.rgb * 0.0773993808, vec3( lessThanEqual( texColor.rgb, vec3( 0.04045 ) ) ) ), texColor.w ); - #endif - texColor.rgb *= backgroundIntensity; - gl_FragColor = texColor; - #include - #include -}`,Q_=`varying vec3 vWorldDirection; -#include -void main() { - vWorldDirection = transformDirection( position, modelMatrix ); - #include - #include - gl_Position.z = gl_Position.w; -}`,ev=`#ifdef ENVMAP_TYPE_CUBE - uniform samplerCube envMap; -#elif defined( ENVMAP_TYPE_CUBE_UV ) - uniform sampler2D envMap; -#endif -uniform float flipEnvMap; -uniform float backgroundBlurriness; -uniform float backgroundIntensity; -uniform mat3 backgroundRotation; -varying vec3 vWorldDirection; -#include -void main() { - #ifdef ENVMAP_TYPE_CUBE - vec4 texColor = textureCube( envMap, backgroundRotation * vec3( flipEnvMap * vWorldDirection.x, vWorldDirection.yz ) ); - #elif defined( ENVMAP_TYPE_CUBE_UV ) - vec4 texColor = textureCubeUV( envMap, backgroundRotation * vWorldDirection, backgroundBlurriness ); - #else - vec4 texColor = vec4( 0.0, 0.0, 0.0, 1.0 ); - #endif - texColor.rgb *= backgroundIntensity; - gl_FragColor = texColor; - #include - #include -}`,tv=`varying vec3 vWorldDirection; -#include -void main() { - vWorldDirection = transformDirection( position, modelMatrix ); - #include - #include - gl_Position.z = gl_Position.w; -}`,nv=`uniform samplerCube tCube; -uniform float tFlip; -uniform float opacity; -varying vec3 vWorldDirection; -void main() { - vec4 texColor = textureCube( tCube, vec3( tFlip * vWorldDirection.x, vWorldDirection.yz ) ); - gl_FragColor = texColor; - gl_FragColor.a *= opacity; - #include - #include -}`,iv=`#include -#include -#include -#include -#include -#include -#include -#include -varying vec2 vHighPrecisionZW; -void main() { - #include - #include - #include - #include - #ifdef USE_DISPLACEMENTMAP - #include - #include - #include - #endif - #include - #include - #include - #include - #include - #include - #include - vHighPrecisionZW = gl_Position.zw; -}`,sv=`#if DEPTH_PACKING == 3200 - uniform float opacity; -#endif -#include -#include -#include -#include -#include -#include -#include -#include -#include -varying vec2 vHighPrecisionZW; -void main() { - vec4 diffuseColor = vec4( 1.0 ); - #include - #if DEPTH_PACKING == 3200 - diffuseColor.a = opacity; - #endif - #include - #include - #include - #include - #include - float fragCoordZ = 0.5 * vHighPrecisionZW[0] / vHighPrecisionZW[1] + 0.5; - #if DEPTH_PACKING == 3200 - gl_FragColor = vec4( vec3( 1.0 - fragCoordZ ), opacity ); - #elif DEPTH_PACKING == 3201 - gl_FragColor = packDepthToRGBA( fragCoordZ ); - #elif DEPTH_PACKING == 3202 - gl_FragColor = vec4( packDepthToRGB( fragCoordZ ), 1.0 ); - #elif DEPTH_PACKING == 3203 - gl_FragColor = vec4( packDepthToRG( fragCoordZ ), 0.0, 1.0 ); - #endif -}`,rv=`#define DISTANCE -varying vec3 vWorldPosition; -#include -#include -#include -#include -#include -#include -#include -void main() { - #include - #include - #include - #include - #ifdef USE_DISPLACEMENTMAP - #include - #include - #include - #endif - #include - #include - #include - #include - #include - #include - #include - vWorldPosition = worldPosition.xyz; -}`,av=`#define DISTANCE -uniform vec3 referencePosition; -uniform float nearDistance; -uniform float farDistance; -varying vec3 vWorldPosition; -#include -#include -#include -#include -#include -#include -#include -#include -void main () { - vec4 diffuseColor = vec4( 1.0 ); - #include - #include - #include - #include - #include - float dist = length( vWorldPosition - referencePosition ); - dist = ( dist - nearDistance ) / ( farDistance - nearDistance ); - dist = saturate( dist ); - gl_FragColor = packDepthToRGBA( dist ); -}`,ov=`varying vec3 vWorldDirection; -#include -void main() { - vWorldDirection = transformDirection( position, modelMatrix ); - #include - #include -}`,lv=`uniform sampler2D tEquirect; -varying vec3 vWorldDirection; -#include -void main() { - vec3 direction = normalize( vWorldDirection ); - vec2 sampleUV = equirectUv( direction ); - gl_FragColor = texture2D( tEquirect, sampleUV ); - #include - #include -}`,cv=`uniform float scale; -attribute float lineDistance; -varying float vLineDistance; -#include -#include -#include -#include -#include -#include -#include -void main() { - vLineDistance = scale * lineDistance; - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include -}`,hv=`uniform vec3 diffuse; -uniform float opacity; -uniform float dashSize; -uniform float totalSize; -varying float vLineDistance; -#include -#include -#include -#include -#include -#include -#include -void main() { - vec4 diffuseColor = vec4( diffuse, opacity ); - #include - if ( mod( vLineDistance, totalSize ) > dashSize ) { - discard; - } - vec3 outgoingLight = vec3( 0.0 ); - #include - #include - #include - outgoingLight = diffuseColor.rgb; - #include - #include - #include - #include - #include -}`,uv=`#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -void main() { - #include - #include - #include - #include - #include - #if defined ( USE_ENVMAP ) || defined ( USE_SKINNING ) - #include - #include - #include - #include - #include - #endif - #include - #include - #include - #include - #include - #include - #include - #include - #include -}`,dv=`uniform vec3 diffuse; -uniform float opacity; -#ifndef FLAT_SHADED - varying vec3 vNormal; -#endif -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -void main() { - vec4 diffuseColor = vec4( diffuse, opacity ); - #include - #include - #include - #include - #include - #include - #include - #include - ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); - #ifdef USE_LIGHTMAP - vec4 lightMapTexel = texture2D( lightMap, vLightMapUv ); - reflectedLight.indirectDiffuse += lightMapTexel.rgb * lightMapIntensity * RECIPROCAL_PI; - #else - reflectedLight.indirectDiffuse += vec3( 1.0 ); - #endif - #include - reflectedLight.indirectDiffuse *= diffuseColor.rgb; - vec3 outgoingLight = reflectedLight.indirectDiffuse; - #include - #include - #include - #include - #include - #include - #include -}`,fv=`#define LAMBERT -varying vec3 vViewPosition; -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -void main() { - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - vViewPosition = - mvPosition.xyz; - #include - #include - #include - #include -}`,pv=`#define LAMBERT -uniform vec3 diffuse; -uniform vec3 emissive; -uniform float opacity; -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -void main() { - vec4 diffuseColor = vec4( diffuse, opacity ); - #include - ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); - vec3 totalEmissiveRadiance = emissive; - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance; - #include - #include - #include - #include - #include - #include - #include -}`,mv=`#define MATCAP -varying vec3 vViewPosition; -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -void main() { - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - vViewPosition = - mvPosition.xyz; -}`,gv=`#define MATCAP -uniform vec3 diffuse; -uniform float opacity; -uniform sampler2D matcap; -varying vec3 vViewPosition; -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -void main() { - vec4 diffuseColor = vec4( diffuse, opacity ); - #include - #include - #include - #include - #include - #include - #include - #include - #include - vec3 viewDir = normalize( vViewPosition ); - vec3 x = normalize( vec3( viewDir.z, 0.0, - viewDir.x ) ); - vec3 y = cross( viewDir, x ); - vec2 uv = vec2( dot( x, normal ), dot( y, normal ) ) * 0.495 + 0.5; - #ifdef USE_MATCAP - vec4 matcapColor = texture2D( matcap, uv ); - #else - vec4 matcapColor = vec4( vec3( mix( 0.2, 0.8, uv.y ) ), 1.0 ); - #endif - vec3 outgoingLight = diffuseColor.rgb * matcapColor.rgb; - #include - #include - #include - #include - #include - #include -}`,_v=`#define NORMAL -#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP_TANGENTSPACE ) - varying vec3 vViewPosition; -#endif -#include -#include -#include -#include -#include -#include -#include -#include -#include -void main() { - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include -#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP_TANGENTSPACE ) - vViewPosition = - mvPosition.xyz; -#endif -}`,vv=`#define NORMAL -uniform float opacity; -#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP_TANGENTSPACE ) - varying vec3 vViewPosition; -#endif -#include -#include -#include -#include -#include -#include -#include -void main() { - vec4 diffuseColor = vec4( 0.0, 0.0, 0.0, opacity ); - #include - #include - #include - #include - gl_FragColor = vec4( packNormalToRGB( normal ), diffuseColor.a ); - #ifdef OPAQUE - gl_FragColor.a = 1.0; - #endif -}`,xv=`#define PHONG -varying vec3 vViewPosition; -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -void main() { - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - vViewPosition = - mvPosition.xyz; - #include - #include - #include - #include -}`,yv=`#define PHONG -uniform vec3 diffuse; -uniform vec3 emissive; -uniform vec3 specular; -uniform float shininess; -uniform float opacity; -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -void main() { - vec4 diffuseColor = vec4( diffuse, opacity ); - #include - ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); - vec3 totalEmissiveRadiance = emissive; - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance; - #include - #include - #include - #include - #include - #include - #include -}`,Mv=`#define STANDARD -varying vec3 vViewPosition; -#ifdef USE_TRANSMISSION - varying vec3 vWorldPosition; -#endif -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -void main() { - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - vViewPosition = - mvPosition.xyz; - #include - #include - #include -#ifdef USE_TRANSMISSION - vWorldPosition = worldPosition.xyz; -#endif -}`,bv=`#define STANDARD -#ifdef PHYSICAL - #define IOR - #define USE_SPECULAR -#endif -uniform vec3 diffuse; -uniform vec3 emissive; -uniform float roughness; -uniform float metalness; -uniform float opacity; -#ifdef IOR - uniform float ior; -#endif -#ifdef USE_SPECULAR - uniform float specularIntensity; - uniform vec3 specularColor; - #ifdef USE_SPECULAR_COLORMAP - uniform sampler2D specularColorMap; - #endif - #ifdef USE_SPECULAR_INTENSITYMAP - uniform sampler2D specularIntensityMap; - #endif -#endif -#ifdef USE_CLEARCOAT - uniform float clearcoat; - uniform float clearcoatRoughness; -#endif -#ifdef USE_DISPERSION - uniform float dispersion; -#endif -#ifdef USE_IRIDESCENCE - uniform float iridescence; - uniform float iridescenceIOR; - uniform float iridescenceThicknessMinimum; - uniform float iridescenceThicknessMaximum; -#endif -#ifdef USE_SHEEN - uniform vec3 sheenColor; - uniform float sheenRoughness; - #ifdef USE_SHEEN_COLORMAP - uniform sampler2D sheenColorMap; - #endif - #ifdef USE_SHEEN_ROUGHNESSMAP - uniform sampler2D sheenRoughnessMap; - #endif -#endif -#ifdef USE_ANISOTROPY - uniform vec2 anisotropyVector; - #ifdef USE_ANISOTROPYMAP - uniform sampler2D anisotropyMap; - #endif -#endif -varying vec3 vViewPosition; -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -void main() { - vec4 diffuseColor = vec4( diffuse, opacity ); - #include - ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); - vec3 totalEmissiveRadiance = emissive; - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - vec3 totalDiffuse = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse; - vec3 totalSpecular = reflectedLight.directSpecular + reflectedLight.indirectSpecular; - #include - vec3 outgoingLight = totalDiffuse + totalSpecular + totalEmissiveRadiance; - #ifdef USE_SHEEN - float sheenEnergyComp = 1.0 - 0.157 * max3( material.sheenColor ); - outgoingLight = outgoingLight * sheenEnergyComp + sheenSpecularDirect + sheenSpecularIndirect; - #endif - #ifdef USE_CLEARCOAT - float dotNVcc = saturate( dot( geometryClearcoatNormal, geometryViewDir ) ); - vec3 Fcc = F_Schlick( material.clearcoatF0, material.clearcoatF90, dotNVcc ); - outgoingLight = outgoingLight * ( 1.0 - material.clearcoat * Fcc ) + ( clearcoatSpecularDirect + clearcoatSpecularIndirect ) * material.clearcoat; - #endif - #include - #include - #include - #include - #include - #include -}`,Sv=`#define TOON -varying vec3 vViewPosition; -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -void main() { - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - vViewPosition = - mvPosition.xyz; - #include - #include - #include -}`,wv=`#define TOON -uniform vec3 diffuse; -uniform vec3 emissive; -uniform float opacity; -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -void main() { - vec4 diffuseColor = vec4( diffuse, opacity ); - #include - ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); - vec3 totalEmissiveRadiance = emissive; - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance; - #include - #include - #include - #include - #include - #include -}`,Ev=`uniform float size; -uniform float scale; -#include -#include -#include -#include -#include -#include -#ifdef USE_POINTS_UV - varying vec2 vUv; - uniform mat3 uvTransform; -#endif -void main() { - #ifdef USE_POINTS_UV - vUv = ( uvTransform * vec3( uv, 1 ) ).xy; - #endif - #include - #include - #include - #include - #include - #include - gl_PointSize = size; - #ifdef USE_SIZEATTENUATION - bool isPerspective = isPerspectiveMatrix( projectionMatrix ); - if ( isPerspective ) gl_PointSize *= ( scale / - mvPosition.z ); - #endif - #include - #include - #include - #include -}`,Tv=`uniform vec3 diffuse; -uniform float opacity; -#include -#include -#include -#include -#include -#include -#include -#include -void main() { - vec4 diffuseColor = vec4( diffuse, opacity ); - #include - vec3 outgoingLight = vec3( 0.0 ); - #include - #include - #include - #include - #include - outgoingLight = diffuseColor.rgb; - #include - #include - #include - #include - #include -}`,Av=`#include -#include -#include -#include -#include -#include -#include -void main() { - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include -}`,Cv=`uniform vec3 color; -uniform float opacity; -#include -#include -#include -#include -#include -#include -#include -#include -void main() { - #include - gl_FragColor = vec4( color, opacity * ( 1.0 - getShadowMask() ) ); - #include - #include - #include -}`,Rv=`uniform float rotation; -uniform vec2 center; -#include -#include -#include -#include -#include -void main() { - #include - vec4 mvPosition = modelViewMatrix[ 3 ]; - vec2 scale = vec2( length( modelMatrix[ 0 ].xyz ), length( modelMatrix[ 1 ].xyz ) ); - #ifndef USE_SIZEATTENUATION - bool isPerspective = isPerspectiveMatrix( projectionMatrix ); - if ( isPerspective ) scale *= - mvPosition.z; - #endif - vec2 alignedPosition = ( position.xy - ( center - vec2( 0.5 ) ) ) * scale; - vec2 rotatedPosition; - rotatedPosition.x = cos( rotation ) * alignedPosition.x - sin( rotation ) * alignedPosition.y; - rotatedPosition.y = sin( rotation ) * alignedPosition.x + cos( rotation ) * alignedPosition.y; - mvPosition.xy += rotatedPosition; - gl_Position = projectionMatrix * mvPosition; - #include - #include - #include -}`,Iv=`uniform vec3 diffuse; -uniform float opacity; -#include -#include -#include -#include -#include -#include -#include -#include -#include -void main() { - vec4 diffuseColor = vec4( diffuse, opacity ); - #include - vec3 outgoingLight = vec3( 0.0 ); - #include - #include - #include - #include - #include - outgoingLight = diffuseColor.rgb; - #include - #include - #include - #include -}`,st={alphahash_fragment:jg,alphahash_pars_fragment:Qg,alphamap_fragment:e0,alphamap_pars_fragment:t0,alphatest_fragment:n0,alphatest_pars_fragment:i0,aomap_fragment:s0,aomap_pars_fragment:r0,batching_pars_vertex:a0,batching_vertex:o0,begin_vertex:l0,beginnormal_vertex:c0,bsdfs:h0,iridescence_fragment:u0,bumpmap_pars_fragment:d0,clipping_planes_fragment:f0,clipping_planes_pars_fragment:p0,clipping_planes_pars_vertex:m0,clipping_planes_vertex:g0,color_fragment:_0,color_pars_fragment:v0,color_pars_vertex:x0,color_vertex:y0,common:M0,cube_uv_reflection_fragment:b0,defaultnormal_vertex:S0,displacementmap_pars_vertex:w0,displacementmap_vertex:E0,emissivemap_fragment:T0,emissivemap_pars_fragment:A0,colorspace_fragment:C0,colorspace_pars_fragment:R0,envmap_fragment:I0,envmap_common_pars_fragment:P0,envmap_pars_fragment:D0,envmap_pars_vertex:L0,envmap_physical_pars_fragment:W0,envmap_vertex:U0,fog_vertex:N0,fog_pars_vertex:F0,fog_fragment:O0,fog_pars_fragment:B0,gradientmap_pars_fragment:z0,lightmap_pars_fragment:k0,lights_lambert_fragment:V0,lights_lambert_pars_fragment:H0,lights_pars_begin:G0,lights_toon_fragment:X0,lights_toon_pars_fragment:q0,lights_phong_fragment:Y0,lights_phong_pars_fragment:Z0,lights_physical_fragment:J0,lights_physical_pars_fragment:K0,lights_fragment_begin:$0,lights_fragment_maps:j0,lights_fragment_end:Q0,logdepthbuf_fragment:e_,logdepthbuf_pars_fragment:t_,logdepthbuf_pars_vertex:n_,logdepthbuf_vertex:i_,map_fragment:s_,map_pars_fragment:r_,map_particle_fragment:a_,map_particle_pars_fragment:o_,metalnessmap_fragment:l_,metalnessmap_pars_fragment:c_,morphinstance_vertex:h_,morphcolor_vertex:u_,morphnormal_vertex:d_,morphtarget_pars_vertex:f_,morphtarget_vertex:p_,normal_fragment_begin:m_,normal_fragment_maps:g_,normal_pars_fragment:__,normal_pars_vertex:v_,normal_vertex:x_,normalmap_pars_fragment:y_,clearcoat_normal_fragment_begin:M_,clearcoat_normal_fragment_maps:b_,clearcoat_pars_fragment:S_,iridescence_pars_fragment:w_,opaque_fragment:E_,packing:T_,premultiplied_alpha_fragment:A_,project_vertex:C_,dithering_fragment:R_,dithering_pars_fragment:I_,roughnessmap_fragment:P_,roughnessmap_pars_fragment:D_,shadowmap_pars_fragment:L_,shadowmap_pars_vertex:U_,shadowmap_vertex:N_,shadowmask_pars_fragment:F_,skinbase_vertex:O_,skinning_pars_vertex:B_,skinning_vertex:z_,skinnormal_vertex:k_,specularmap_fragment:V_,specularmap_pars_fragment:H_,tonemapping_fragment:G_,tonemapping_pars_fragment:W_,transmission_fragment:X_,transmission_pars_fragment:q_,uv_pars_fragment:Y_,uv_pars_vertex:Z_,uv_vertex:J_,worldpos_vertex:K_,background_vert:$_,background_frag:j_,backgroundCube_vert:Q_,backgroundCube_frag:ev,cube_vert:tv,cube_frag:nv,depth_vert:iv,depth_frag:sv,distanceRGBA_vert:rv,distanceRGBA_frag:av,equirect_vert:ov,equirect_frag:lv,linedashed_vert:cv,linedashed_frag:hv,meshbasic_vert:uv,meshbasic_frag:dv,meshlambert_vert:fv,meshlambert_frag:pv,meshmatcap_vert:mv,meshmatcap_frag:gv,meshnormal_vert:_v,meshnormal_frag:vv,meshphong_vert:xv,meshphong_frag:yv,meshphysical_vert:Mv,meshphysical_frag:bv,meshtoon_vert:Sv,meshtoon_frag:wv,points_vert:Ev,points_frag:Tv,shadow_vert:Av,shadow_frag:Cv,sprite_vert:Rv,sprite_frag:Iv},Te={common:{diffuse:{value:new ne(16777215)},opacity:{value:1},map:{value:null},mapTransform:{value:new tt},alphaMap:{value:null},alphaMapTransform:{value:new tt},alphaTest:{value:0}},specularmap:{specularMap:{value:null},specularMapTransform:{value:new tt}},envmap:{envMap:{value:null},envMapRotation:{value:new tt},flipEnvMap:{value:-1},reflectivity:{value:1},ior:{value:1.5},refractionRatio:{value:.98}},aomap:{aoMap:{value:null},aoMapIntensity:{value:1},aoMapTransform:{value:new tt}},lightmap:{lightMap:{value:null},lightMapIntensity:{value:1},lightMapTransform:{value:new tt}},bumpmap:{bumpMap:{value:null},bumpMapTransform:{value:new tt},bumpScale:{value:1}},normalmap:{normalMap:{value:null},normalMapTransform:{value:new tt},normalScale:{value:new j(1,1)}},displacementmap:{displacementMap:{value:null},displacementMapTransform:{value:new tt},displacementScale:{value:1},displacementBias:{value:0}},emissivemap:{emissiveMap:{value:null},emissiveMapTransform:{value:new tt}},metalnessmap:{metalnessMap:{value:null},metalnessMapTransform:{value:new tt}},roughnessmap:{roughnessMap:{value:null},roughnessMapTransform:{value:new tt}},gradientmap:{gradientMap:{value:null}},fog:{fogDensity:{value:25e-5},fogNear:{value:1},fogFar:{value:2e3},fogColor:{value:new ne(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 ne(16777215)},opacity:{value:1},size:{value:1},scale:{value:1},map:{value:null},alphaMap:{value:null},alphaMapTransform:{value:new tt},alphaTest:{value:0},uvTransform:{value:new tt}},sprite:{diffuse:{value:new ne(16777215)},opacity:{value:1},center:{value:new j(.5,.5)},rotation:{value:0},map:{value:null},mapTransform:{value:new tt},alphaMap:{value:null},alphaMapTransform:{value:new tt},alphaTest:{value:0}}},On={basic:{uniforms:nn([Te.common,Te.specularmap,Te.envmap,Te.aomap,Te.lightmap,Te.fog]),vertexShader:st.meshbasic_vert,fragmentShader:st.meshbasic_frag},lambert:{uniforms:nn([Te.common,Te.specularmap,Te.envmap,Te.aomap,Te.lightmap,Te.emissivemap,Te.bumpmap,Te.normalmap,Te.displacementmap,Te.fog,Te.lights,{emissive:{value:new ne(0)}}]),vertexShader:st.meshlambert_vert,fragmentShader:st.meshlambert_frag},phong:{uniforms:nn([Te.common,Te.specularmap,Te.envmap,Te.aomap,Te.lightmap,Te.emissivemap,Te.bumpmap,Te.normalmap,Te.displacementmap,Te.fog,Te.lights,{emissive:{value:new ne(0)},specular:{value:new ne(1118481)},shininess:{value:30}}]),vertexShader:st.meshphong_vert,fragmentShader:st.meshphong_frag},standard:{uniforms:nn([Te.common,Te.envmap,Te.aomap,Te.lightmap,Te.emissivemap,Te.bumpmap,Te.normalmap,Te.displacementmap,Te.roughnessmap,Te.metalnessmap,Te.fog,Te.lights,{emissive:{value:new ne(0)},roughness:{value:1},metalness:{value:0},envMapIntensity:{value:1}}]),vertexShader:st.meshphysical_vert,fragmentShader:st.meshphysical_frag},toon:{uniforms:nn([Te.common,Te.aomap,Te.lightmap,Te.emissivemap,Te.bumpmap,Te.normalmap,Te.displacementmap,Te.gradientmap,Te.fog,Te.lights,{emissive:{value:new ne(0)}}]),vertexShader:st.meshtoon_vert,fragmentShader:st.meshtoon_frag},matcap:{uniforms:nn([Te.common,Te.bumpmap,Te.normalmap,Te.displacementmap,Te.fog,{matcap:{value:null}}]),vertexShader:st.meshmatcap_vert,fragmentShader:st.meshmatcap_frag},points:{uniforms:nn([Te.points,Te.fog]),vertexShader:st.points_vert,fragmentShader:st.points_frag},dashed:{uniforms:nn([Te.common,Te.fog,{scale:{value:1},dashSize:{value:1},totalSize:{value:2}}]),vertexShader:st.linedashed_vert,fragmentShader:st.linedashed_frag},depth:{uniforms:nn([Te.common,Te.displacementmap]),vertexShader:st.depth_vert,fragmentShader:st.depth_frag},normal:{uniforms:nn([Te.common,Te.bumpmap,Te.normalmap,Te.displacementmap,{opacity:{value:1}}]),vertexShader:st.meshnormal_vert,fragmentShader:st.meshnormal_frag},sprite:{uniforms:nn([Te.sprite,Te.fog]),vertexShader:st.sprite_vert,fragmentShader:st.sprite_frag},background:{uniforms:{uvTransform:{value:new tt},t2D:{value:null},backgroundIntensity:{value:1}},vertexShader:st.background_vert,fragmentShader:st.background_frag},backgroundCube:{uniforms:{envMap:{value:null},flipEnvMap:{value:-1},backgroundBlurriness:{value:0},backgroundIntensity:{value:1},backgroundRotation:{value:new tt}},vertexShader:st.backgroundCube_vert,fragmentShader:st.backgroundCube_frag},cube:{uniforms:{tCube:{value:null},tFlip:{value:-1},opacity:{value:1}},vertexShader:st.cube_vert,fragmentShader:st.cube_frag},equirect:{uniforms:{tEquirect:{value:null}},vertexShader:st.equirect_vert,fragmentShader:st.equirect_frag},distanceRGBA:{uniforms:nn([Te.common,Te.displacementmap,{referencePosition:{value:new A},nearDistance:{value:1},farDistance:{value:1e3}}]),vertexShader:st.distanceRGBA_vert,fragmentShader:st.distanceRGBA_frag},shadow:{uniforms:nn([Te.lights,Te.fog,{color:{value:new ne(0)},opacity:{value:1}}]),vertexShader:st.shadow_vert,fragmentShader:st.shadow_frag}};On.physical={uniforms:nn([On.standard.uniforms,{clearcoat:{value:0},clearcoatMap:{value:null},clearcoatMapTransform:{value:new tt},clearcoatNormalMap:{value:null},clearcoatNormalMapTransform:{value:new tt},clearcoatNormalScale:{value:new j(1,1)},clearcoatRoughness:{value:0},clearcoatRoughnessMap:{value:null},clearcoatRoughnessMapTransform:{value:new tt},dispersion:{value:0},iridescence:{value:0},iridescenceMap:{value:null},iridescenceMapTransform:{value:new tt},iridescenceIOR:{value:1.3},iridescenceThicknessMinimum:{value:100},iridescenceThicknessMaximum:{value:400},iridescenceThicknessMap:{value:null},iridescenceThicknessMapTransform:{value:new tt},sheen:{value:0},sheenColor:{value:new ne(0)},sheenColorMap:{value:null},sheenColorMapTransform:{value:new tt},sheenRoughness:{value:1},sheenRoughnessMap:{value:null},sheenRoughnessMapTransform:{value:new tt},transmission:{value:0},transmissionMap:{value:null},transmissionMapTransform:{value:new tt},transmissionSamplerSize:{value:new j},transmissionSamplerMap:{value:null},thickness:{value:0},thicknessMap:{value:null},thicknessMapTransform:{value:new tt},attenuationDistance:{value:0},attenuationColor:{value:new ne(0)},specularColor:{value:new ne(1,1,1)},specularColorMap:{value:null},specularColorMapTransform:{value:new tt},specularIntensity:{value:1},specularIntensityMap:{value:null},specularIntensityMapTransform:{value:new tt},anisotropyVector:{value:new j},anisotropyMap:{value:null},anisotropyMapTransform:{value:new tt}}]),vertexShader:st.meshphysical_vert,fragmentShader:st.meshphysical_frag};const Ba={r:0,b:0,g:0},Yi=new In,Pv=new Ze;function Dv(r,e,t,n,i,s,a){const o=new ne(0);let l=s===!0?0:1,c,h,u=null,f=0,d=null;function p(v){let x=v.isScene===!0?v.background:null;return x&&x.isTexture&&(x=(v.backgroundBlurriness>0?t:e).get(x)),x}function _(v){let x=!1;const w=p(v);w===null?m(o,l):w&&w.isColor&&(m(w,1),x=!0);const T=r.xr.getEnvironmentBlendMode();T==="additive"?n.buffers.color.setClear(0,0,0,1,a):T==="alpha-blend"&&n.buffers.color.setClear(0,0,0,0,a),(r.autoClear||x)&&(n.buffers.depth.setTest(!0),n.buffers.depth.setMask(!0),n.buffers.color.setMask(!0),r.clear(r.autoClearColor,r.autoClearDepth,r.autoClearStencil))}function g(v,x){const w=p(x);w&&(w.isCubeTexture||w.mapping===qr)?(h===void 0&&(h=new yt(new or(1,1,1),new Xt({name:"BackgroundCubeMaterial",uniforms:tr(On.backgroundCube.uniforms),vertexShader:On.backgroundCube.vertexShader,fragmentShader:On.backgroundCube.fragmentShader,side:fn,depthTest:!1,depthWrite:!1,fog:!1})),h.geometry.deleteAttribute("normal"),h.geometry.deleteAttribute("uv"),h.onBeforeRender=function(T,C,P){this.matrixWorld.copyPosition(P.matrixWorld)},Object.defineProperty(h.material,"envMap",{get:function(){return this.uniforms.envMap.value}}),i.update(h)),Yi.copy(x.backgroundRotation),Yi.x*=-1,Yi.y*=-1,Yi.z*=-1,w.isCubeTexture&&w.isRenderTargetTexture===!1&&(Yi.y*=-1,Yi.z*=-1),h.material.uniforms.envMap.value=w,h.material.uniforms.flipEnvMap.value=w.isCubeTexture&&w.isRenderTargetTexture===!1?-1:1,h.material.uniforms.backgroundBlurriness.value=x.backgroundBlurriness,h.material.uniforms.backgroundIntensity.value=x.backgroundIntensity,h.material.uniforms.backgroundRotation.value.setFromMatrix4(Pv.makeRotationFromEuler(Yi)),h.material.toneMapped=ht.getTransfer(w.colorSpace)!==vt,(u!==w||f!==w.version||d!==r.toneMapping)&&(h.material.needsUpdate=!0,u=w,f=w.version,d=r.toneMapping),h.layers.enableAll(),v.unshift(h,h.geometry,h.material,0,0,null)):w&&w.isTexture&&(c===void 0&&(c=new yt(new lr(2,2),new Xt({name:"BackgroundMaterial",uniforms:tr(On.background.uniforms),vertexShader:On.background.vertexShader,fragmentShader:On.background.fragmentShader,side:Ci,depthTest:!1,depthWrite:!1,fog:!1})),c.geometry.deleteAttribute("normal"),Object.defineProperty(c.material,"map",{get:function(){return this.uniforms.t2D.value}}),i.update(c)),c.material.uniforms.t2D.value=w,c.material.uniforms.backgroundIntensity.value=x.backgroundIntensity,c.material.toneMapped=ht.getTransfer(w.colorSpace)!==vt,w.matrixAutoUpdate===!0&&w.updateMatrix(),c.material.uniforms.uvTransform.value.copy(w.matrix),(u!==w||f!==w.version||d!==r.toneMapping)&&(c.material.needsUpdate=!0,u=w,f=w.version,d=r.toneMapping),c.layers.enableAll(),v.unshift(c,c.geometry,c.material,0,0,null))}function m(v,x){v.getRGB(Ba,Ad(r)),n.buffers.color.setClear(Ba.r,Ba.g,Ba.b,x,a)}function y(){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(v,x=1){o.set(v),l=x,m(o,l)},getClearAlpha:function(){return l},setClearAlpha:function(v){l=v,m(o,l)},render:_,addToRenderList:g,dispose:y}}function Lv(r,e){const t=r.getParameter(r.MAX_VERTEX_ATTRIBS),n={},i=f(null);let s=i,a=!1;function o(M,D,G,F,V){let J=!1;const q=u(F,G,D);s!==q&&(s=q,c(s.object)),J=d(M,F,G,V),J&&p(M,F,G,V),V!==null&&e.update(V,r.ELEMENT_ARRAY_BUFFER),(J||a)&&(a=!1,x(M,D,G,F),V!==null&&r.bindBuffer(r.ELEMENT_ARRAY_BUFFER,e.get(V).buffer))}function l(){return r.createVertexArray()}function c(M){return r.bindVertexArray(M)}function h(M){return r.deleteVertexArray(M)}function u(M,D,G){const F=G.wireframe===!0;let V=n[M.id];V===void 0&&(V={},n[M.id]=V);let J=V[D.id];J===void 0&&(J={},V[D.id]=J);let q=J[F];return q===void 0&&(q=f(l()),J[F]=q),q}function f(M){const D=[],G=[],F=[];for(let V=0;V=0){const be=V[Z];let Re=J[Z];if(Re===void 0&&(Z==="instanceMatrix"&&M.instanceMatrix&&(Re=M.instanceMatrix),Z==="instanceColor"&&M.instanceColor&&(Re=M.instanceColor)),be===void 0||be.attribute!==Re||Re&&be.data!==Re.data)return!0;q++}return s.attributesNum!==q||s.index!==F}function p(M,D,G,F){const V={},J=D.attributes;let q=0;const se=G.getAttributes();for(const Z in se)if(se[Z].location>=0){let be=J[Z];be===void 0&&(Z==="instanceMatrix"&&M.instanceMatrix&&(be=M.instanceMatrix),Z==="instanceColor"&&M.instanceColor&&(be=M.instanceColor));const Re={};Re.attribute=be,be&&be.data&&(Re.data=be.data),V[Z]=Re,q++}s.attributes=V,s.attributesNum=q,s.index=F}function _(){const M=s.newAttributes;for(let D=0,G=M.length;D=0){let _e=V[se];if(_e===void 0&&(se==="instanceMatrix"&&M.instanceMatrix&&(_e=M.instanceMatrix),se==="instanceColor"&&M.instanceColor&&(_e=M.instanceColor)),_e!==void 0){const be=_e.normalized,Re=_e.itemSize,Ge=e.get(_e);if(Ge===void 0)continue;const it=Ge.buffer,Q=Ge.type,ue=Ge.bytesPerElement,Ie=Q===r.INT||Q===r.UNSIGNED_INT||_e.gpuType===Pc;if(_e.isInterleavedBufferAttribute){const pe=_e.data,Oe=pe.stride,We=_e.offset;if(pe.isInstancedInterleavedBuffer){for(let ze=0;ze0&&r.getShaderPrecisionFormat(r.FRAGMENT_SHADER,r.HIGH_FLOAT).precision>0)return"highp";C="mediump"}return C==="mediump"&&r.getShaderPrecisionFormat(r.VERTEX_SHADER,r.MEDIUM_FLOAT).precision>0&&r.getShaderPrecisionFormat(r.FRAGMENT_SHADER,r.MEDIUM_FLOAT).precision>0?"mediump":"lowp"}let c=t.precision!==void 0?t.precision:"highp";const h=l(c);h!==c&&(console.warn("THREE.WebGLRenderer:",c,"not supported, using",h,"instead."),c=h);const u=t.logarithmicDepthBuffer===!0,f=t.reverseDepthBuffer===!0&&e.has("EXT_clip_control"),d=r.getParameter(r.MAX_TEXTURE_IMAGE_UNITS),p=r.getParameter(r.MAX_VERTEX_TEXTURE_IMAGE_UNITS),_=r.getParameter(r.MAX_TEXTURE_SIZE),g=r.getParameter(r.MAX_CUBE_MAP_TEXTURE_SIZE),m=r.getParameter(r.MAX_VERTEX_ATTRIBS),y=r.getParameter(r.MAX_VERTEX_UNIFORM_VECTORS),v=r.getParameter(r.MAX_VARYING_VECTORS),x=r.getParameter(r.MAX_FRAGMENT_UNIFORM_VECTORS),w=p>0,T=r.getParameter(r.MAX_SAMPLES);return{isWebGL2:!0,getMaxAnisotropy:s,getMaxPrecision:l,textureFormatReadable:a,textureTypeReadable:o,precision:c,logarithmicDepthBuffer:u,reverseDepthBuffer:f,maxTextures:d,maxVertexTextures:p,maxTextureSize:_,maxCubemapSize:g,maxAttributes:m,maxVertexUniforms:y,maxVaryings:v,maxFragmentUniforms:x,vertexTextures:w,maxSamples:T}}function Fv(r){const e=this;let t=null,n=0,i=!1,s=!1;const a=new Si,o=new tt,l={value:null,needsUpdate:!1};this.uniform=l,this.numPlanes=0,this.numIntersection=0,this.init=function(u,f){const d=u.length!==0||f||n!==0||i;return i=f,n=u.length,d},this.beginShadows=function(){s=!0,h(null)},this.endShadows=function(){s=!1},this.setGlobalState=function(u,f){t=h(u,f,0)},this.setState=function(u,f,d){const p=u.clippingPlanes,_=u.clipIntersection,g=u.clipShadows,m=r.get(u);if(!i||p===null||p.length===0||s&&!g)s?h(null):c();else{const y=s?0:n,v=y*4;let x=m.clippingState||null;l.value=x,x=h(p,f,v,d);for(let w=0;w!==v;++w)x[w]=t[w];m.clippingState=x,this.numIntersection=_?this.numPlanes:0,this.numPlanes+=y}};function c(){l.value!==t&&(l.value=t,l.needsUpdate=n>0),e.numPlanes=n,e.numIntersection=0}function h(u,f,d,p){const _=u!==null?u.length:0;let g=null;if(_!==0){if(g=l.value,p!==!0||g===null){const m=d+_*4,y=f.matrixWorldInverse;o.getNormalMatrix(y),(g===null||g.length0){const c=new tm(l.height);return c.fromEquirectangularTexture(r,a),e.set(a,c),a.addEventListener("dispose",i),t(c.texture,a.mapping)}else return null}}return a}function i(a){const o=a.target;o.removeEventListener("dispose",i);const l=e.get(o);l!==void 0&&(e.delete(o),l.dispose())}function s(){e=new WeakMap}return{get:n,dispose:s}}const Gs=4,Pu=[.125,.215,.35,.446,.526,.582],es=20,vl=new Lo,Du=new ne;let xl=null,yl=0,Ml=0,bl=!1;const $i=(1+Math.sqrt(5))/2,Us=1/$i,Lu=[new A(-$i,Us,0),new A($i,Us,0),new A(-Us,0,$i),new A(Us,0,$i),new A(0,$i,-Us),new A(0,$i,Us),new A(-1,1,-1),new A(1,1,-1),new A(-1,1,1),new A(1,1,1)];class Uu{constructor(e){this._renderer=e,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(e,t=0,n=.1,i=100){xl=this._renderer.getRenderTarget(),yl=this._renderer.getActiveCubeFace(),Ml=this._renderer.getActiveMipmapLevel(),bl=this._renderer.xr.enabled,this._renderer.xr.enabled=!1,this._setSize(256);const s=this._allocateTargets();return s.depthBuffer=!0,this._sceneToCubeUV(e,n,i,s),t>0&&this._blur(s,0,0,t),this._applyPMREM(s),this._cleanup(s),s}fromEquirectangular(e,t=null){return this._fromTexture(e,t)}fromCubemap(e,t=null){return this._fromTexture(e,t)}compileCubemapShader(){this._cubemapMaterial===null&&(this._cubemapMaterial=Ou(),this._compileMaterial(this._cubemapMaterial))}compileEquirectangularShader(){this._equirectMaterial===null&&(this._equirectMaterial=Fu(),this._compileMaterial(this._equirectMaterial))}dispose(){this._dispose(),this._cubemapMaterial!==null&&this._cubemapMaterial.dispose(),this._equirectMaterial!==null&&this._equirectMaterial.dispose()}_setSize(e){this._lodMax=Math.floor(Math.log2(e)),this._cubeSize=Math.pow(2,this._lodMax)}_dispose(){this._blurMaterial!==null&&this._blurMaterial.dispose(),this._pingPongRenderTarget!==null&&this._pingPongRenderTarget.dispose();for(let e=0;e2?v:0,v,v),h.setRenderTarget(i),_&&h.render(p,o),h.render(e,o)}p.geometry.dispose(),p.material.dispose(),h.toneMapping=f,h.autoClear=u,e.background=g}_textureToCubeUV(e,t){const n=this._renderer,i=e.mapping===Ri||e.mapping===os;i?(this._cubemapMaterial===null&&(this._cubemapMaterial=Ou()),this._cubemapMaterial.uniforms.flipEnvMap.value=e.isRenderTargetTexture===!1?-1:1):this._equirectMaterial===null&&(this._equirectMaterial=Fu());const s=i?this._cubemapMaterial:this._equirectMaterial,a=new yt(this._lodPlanes[0],s),o=s.uniforms;o.envMap.value=e;const l=this._cubeSize;za(t,0,0,3*l,2*l),n.setRenderTarget(t),n.render(a,vl)}_applyPMREM(e){const t=this._renderer,n=t.autoClear;t.autoClear=!1;const i=this._lodPlanes.length;for(let s=1;ses&&console.warn(`sigmaRadians, ${s}, is too large and will clip, as it requested ${g} samples when the maximum is set to ${es}`);const m=[];let y=0;for(let C=0;Cv-Gs?i-v+Gs:0),T=4*(this._cubeSize-x);za(t,w,T,3*x,2*x),l.setRenderTarget(t),l.render(u,vl)}}function Bv(r){const e=[],t=[],n=[];let i=r;const s=r-Gs+1+Pu.length;for(let a=0;ar-Gs?l=Pu[a-r+Gs-1]:a===0&&(l=0),n.push(l);const c=1/(o-2),h=-c,u=1+c,f=[h,h,u,h,u,u,h,h,u,u,h,u],d=6,p=6,_=3,g=2,m=1,y=new Float32Array(_*p*d),v=new Float32Array(g*p*d),x=new Float32Array(m*p*d);for(let T=0;T2?0:-1,S=[C,P,0,C+2/3,P,0,C+2/3,P+1,0,C,P,0,C+2/3,P+1,0,C,P+1,0];y.set(S,_*p*T),v.set(f,g*p*T);const M=[T,T,T,T,T,T];x.set(M,m*p*T)}const w=new qe;w.setAttribute("position",new rt(y,_)),w.setAttribute("uv",new rt(v,g)),w.setAttribute("faceIndex",new rt(x,m)),e.push(w),i>Gs&&i--}return{lodPlanes:e,sizeLods:t,sigmas:n}}function Nu(r,e,t){const n=new Sn(r,e,t);return n.texture.mapping=qr,n.texture.name="PMREM.cubeUv",n.scissorTest=!0,n}function za(r,e,t,n,i){r.viewport.set(e,t,n,i),r.scissor.set(e,t,n,i)}function zv(r,e,t){const n=new Float32Array(es),i=new A(0,1,0);return new Xt({name:"SphericalGaussianBlur",defines:{n:es,CUBEUV_TEXEL_WIDTH:1/e,CUBEUV_TEXEL_HEIGHT:1/t,CUBEUV_MAX_MIP:`${r}.0`},uniforms:{envMap:{value:null},samples:{value:1},weights:{value:n},latitudinal:{value:!1},dTheta:{value:0},mipInt:{value:0},poleAxis:{value:i}},vertexShader:dh(),fragmentShader:` - - precision mediump float; - precision mediump int; - - varying vec3 vOutputDirection; - - uniform sampler2D envMap; - uniform int samples; - uniform float weights[ n ]; - uniform bool latitudinal; - uniform float dTheta; - uniform float mipInt; - uniform vec3 poleAxis; - - #define ENVMAP_TYPE_CUBE_UV - #include - - vec3 getSample( float theta, vec3 axis ) { - - float cosTheta = cos( theta ); - // Rodrigues' axis-angle rotation - vec3 sampleDirection = vOutputDirection * cosTheta - + cross( axis, vOutputDirection ) * sin( theta ) - + axis * dot( axis, vOutputDirection ) * ( 1.0 - cosTheta ); - - return bilinearCubeUV( envMap, sampleDirection, mipInt ); - - } - - void main() { - - vec3 axis = latitudinal ? poleAxis : cross( poleAxis, vOutputDirection ); - - if ( all( equal( axis, vec3( 0.0 ) ) ) ) { - - axis = vec3( vOutputDirection.z, 0.0, - vOutputDirection.x ); - - } - - axis = normalize( axis ); - - gl_FragColor = vec4( 0.0, 0.0, 0.0, 1.0 ); - gl_FragColor.rgb += weights[ 0 ] * getSample( 0.0, axis ); - - for ( int i = 1; i < n; i++ ) { - - if ( i >= samples ) { - - break; - - } - - float theta = dTheta * float( i ); - gl_FragColor.rgb += weights[ i ] * getSample( -1.0 * theta, axis ); - gl_FragColor.rgb += weights[ i ] * getSample( theta, axis ); - - } - - } - `,blending:ii,depthTest:!1,depthWrite:!1})}function Fu(){return new Xt({name:"EquirectangularToCubeUV",uniforms:{envMap:{value:null}},vertexShader:dh(),fragmentShader:` - - precision mediump float; - precision mediump int; - - varying vec3 vOutputDirection; - - uniform sampler2D envMap; - - #include - - void main() { - - vec3 outputDirection = normalize( vOutputDirection ); - vec2 uv = equirectUv( outputDirection ); - - gl_FragColor = vec4( texture2D ( envMap, uv ).rgb, 1.0 ); - - } - `,blending:ii,depthTest:!1,depthWrite:!1})}function Ou(){return new Xt({name:"CubemapToCubeUV",uniforms:{envMap:{value:null},flipEnvMap:{value:-1}},vertexShader:dh(),fragmentShader:` - - precision mediump float; - precision mediump int; - - uniform float flipEnvMap; - - varying vec3 vOutputDirection; - - uniform samplerCube envMap; - - void main() { - - gl_FragColor = textureCube( envMap, vec3( flipEnvMap * vOutputDirection.x, vOutputDirection.yz ) ); - - } - `,blending:ii,depthTest:!1,depthWrite:!1})}function dh(){return` - - precision mediump float; - precision mediump int; - - attribute float faceIndex; - - varying vec3 vOutputDirection; - - // RH coordinate system; PMREM face-indexing convention - vec3 getDirection( vec2 uv, float face ) { - - uv = 2.0 * uv - 1.0; - - vec3 direction = vec3( uv, 1.0 ); - - if ( face == 0.0 ) { - - direction = direction.zyx; // ( 1, v, u ) pos x - - } else if ( face == 1.0 ) { - - direction = direction.xzy; - direction.xz *= -1.0; // ( -u, 1, -v ) pos y - - } else if ( face == 2.0 ) { - - direction.x *= -1.0; // ( -u, v, 1 ) pos z - - } else if ( face == 3.0 ) { - - direction = direction.zyx; - direction.xz *= -1.0; // ( -1, v, -u ) neg x - - } else if ( face == 4.0 ) { - - direction = direction.xzy; - direction.xy *= -1.0; // ( -u, -1, v ) neg y - - } else if ( face == 5.0 ) { - - direction.z *= -1.0; // ( u, v, -1 ) neg z - - } - - return direction; - - } - - void main() { - - vOutputDirection = getDirection( uv, faceIndex ); - gl_Position = vec4( position, 1.0 ); - - } - `}function kv(r){let e=new WeakMap,t=null;function n(o){if(o&&o.isTexture){const l=o.mapping,c=l===io||l===so,h=l===Ri||l===os;if(c||h){let u=e.get(o);const f=u!==void 0?u.texture.pmremVersion:0;if(o.isRenderTargetTexture&&o.pmremVersion!==f)return t===null&&(t=new Uu(r)),u=c?t.fromEquirectangular(o,u):t.fromCubemap(o,u),u.texture.pmremVersion=o.pmremVersion,e.set(o,u),u.texture;if(u!==void 0)return u.texture;{const d=o.image;return c&&d&&d.height>0||h&&d&&i(d)?(t===null&&(t=new Uu(r)),u=c?t.fromEquirectangular(o):t.fromCubemap(o),u.texture.pmremVersion=o.pmremVersion,e.set(o,u),o.addEventListener("dispose",s),u.texture):null}}}return o}function i(o){let l=0;const c=6;for(let h=0;he.maxTextureSize&&(w=Math.ceil(x/e.maxTextureSize),x=e.maxTextureSize);const T=new Float32Array(x*w*4*u),C=new wo(T,x,w,u);C.type=Mn,C.needsUpdate=!0;const P=v*4;for(let M=0;M0)return r;const i=e*t;let s=zu[i];if(s===void 0&&(s=new Float32Array(i),zu[i]=s),e!==0){n.toArray(s,0);for(let a=1,o=0;a!==e;++a)o+=t,r[a].toArray(s,o)}return s}function Ot(r,e){if(r.length!==e.length)return!1;for(let t=0,n=r.length;t":" "} ${o}: ${t[a]}`)}return n.join(` -`)}const qu=new tt;function kx(r){ht._getMatrix(qu,ht.workingColorSpace,r);const e=`mat3( ${qu.elements.map(t=>t.toFixed(4))} )`;switch(ht.getTransfer(r)){case co:return[e,"LinearTransferOETF"];case vt:return[e,"sRGBTransferOETF"];default:return console.warn("THREE.WebGLProgram: Unsupported color space: ",r),[e,"LinearTransferOETF"]}}function Yu(r,e,t){const n=r.getShaderParameter(e,r.COMPILE_STATUS),i=r.getShaderInfoLog(e).trim();if(n&&i==="")return"";const s=/ERROR: 0:(\d+)/.exec(i);if(s){const a=parseInt(s[1]);return t.toUpperCase()+` - -`+i+` - -`+zx(r.getShaderSource(e),a)}else return i}function Vx(r,e){const t=kx(e);return[`vec4 ${r}( vec4 value ) {`,` return ${t[1]}( vec4( value.rgb * ${t[0]}, value.a ) );`,"}"].join(` -`)}function Hx(r,e){let t;switch(e){case Jf:t="Linear";break;case Kf:t="Reinhard";break;case $f:t="Cineon";break;case cd:t="ACESFilmic";break;case Qf:t="AgX";break;case ep:t="Neutral";break;case jf:t="Custom";break;default:console.warn("THREE.WebGLProgram: Unsupported toneMapping:",e),t="Linear"}return"vec3 "+r+"( vec3 color ) { return "+t+"ToneMapping( color ); }"}const ka=new A;function Gx(){ht.getLuminanceCoefficients(ka);const r=ka.x.toFixed(4),e=ka.y.toFixed(4),t=ka.z.toFixed(4);return["float luminance( const in vec3 rgb ) {",` const vec3 weights = vec3( ${r}, ${e}, ${t} );`," return dot( weights, rgb );","}"].join(` -`)}function Wx(r){return[r.extensionClipCullDistance?"#extension GL_ANGLE_clip_cull_distance : require":"",r.extensionMultiDraw?"#extension GL_ANGLE_multi_draw : require":""].filter(Ir).join(` -`)}function Xx(r){const e=[];for(const t in r){const n=r[t];n!==!1&&e.push("#define "+t+" "+n)}return e.join(` -`)}function qx(r,e){const t={},n=r.getProgramParameter(e,r.ACTIVE_ATTRIBUTES);for(let i=0;i/gm;function Mc(r){return r.replace(Yx,Jx)}const Zx=new Map;function Jx(r,e){let t=st[e];if(t===void 0){const n=Zx.get(e);if(n!==void 0)t=st[n],console.warn('THREE.WebGLRenderer: Shader chunk "%s" has been deprecated. Use "%s" instead.',e,n);else throw new Error("Can not resolve #include <"+e+">")}return Mc(t)}const Kx=/#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 Ku(r){return r.replace(Kx,$x)}function $x(r,e,t,n){let i="";for(let s=parseInt(e);s0&&(g+=` -`),m=["#define SHADER_TYPE "+t.shaderType,"#define SHADER_NAME "+t.shaderName,p].filter(Ir).join(` -`),m.length>0&&(m+=` -`)):(g=[$u(t),"#define SHADER_TYPE "+t.shaderType,"#define SHADER_NAME "+t.shaderName,p,t.extensionClipCullDistance?"#define USE_CLIP_DISTANCE":"",t.batching?"#define USE_BATCHING":"",t.batchingColor?"#define USE_BATCHING_COLOR":"",t.instancing?"#define USE_INSTANCING":"",t.instancingColor?"#define USE_INSTANCING_COLOR":"",t.instancingMorph?"#define USE_INSTANCING_MORPH":"",t.useFog&&t.fog?"#define USE_FOG":"",t.useFog&&t.fogExp2?"#define FOG_EXP2":"",t.map?"#define USE_MAP":"",t.envMap?"#define USE_ENVMAP":"",t.envMap?"#define "+h:"",t.lightMap?"#define USE_LIGHTMAP":"",t.aoMap?"#define USE_AOMAP":"",t.bumpMap?"#define USE_BUMPMAP":"",t.normalMap?"#define USE_NORMALMAP":"",t.normalMapObjectSpace?"#define USE_NORMALMAP_OBJECTSPACE":"",t.normalMapTangentSpace?"#define USE_NORMALMAP_TANGENTSPACE":"",t.displacementMap?"#define USE_DISPLACEMENTMAP":"",t.emissiveMap?"#define USE_EMISSIVEMAP":"",t.anisotropy?"#define USE_ANISOTROPY":"",t.anisotropyMap?"#define USE_ANISOTROPYMAP":"",t.clearcoatMap?"#define USE_CLEARCOATMAP":"",t.clearcoatRoughnessMap?"#define USE_CLEARCOAT_ROUGHNESSMAP":"",t.clearcoatNormalMap?"#define USE_CLEARCOAT_NORMALMAP":"",t.iridescenceMap?"#define USE_IRIDESCENCEMAP":"",t.iridescenceThicknessMap?"#define USE_IRIDESCENCE_THICKNESSMAP":"",t.specularMap?"#define USE_SPECULARMAP":"",t.specularColorMap?"#define USE_SPECULAR_COLORMAP":"",t.specularIntensityMap?"#define USE_SPECULAR_INTENSITYMAP":"",t.roughnessMap?"#define USE_ROUGHNESSMAP":"",t.metalnessMap?"#define USE_METALNESSMAP":"",t.alphaMap?"#define USE_ALPHAMAP":"",t.alphaHash?"#define USE_ALPHAHASH":"",t.transmission?"#define USE_TRANSMISSION":"",t.transmissionMap?"#define USE_TRANSMISSIONMAP":"",t.thicknessMap?"#define USE_THICKNESSMAP":"",t.sheenColorMap?"#define USE_SHEEN_COLORMAP":"",t.sheenRoughnessMap?"#define USE_SHEEN_ROUGHNESSMAP":"",t.mapUv?"#define MAP_UV "+t.mapUv:"",t.alphaMapUv?"#define ALPHAMAP_UV "+t.alphaMapUv:"",t.lightMapUv?"#define LIGHTMAP_UV "+t.lightMapUv:"",t.aoMapUv?"#define AOMAP_UV "+t.aoMapUv:"",t.emissiveMapUv?"#define EMISSIVEMAP_UV "+t.emissiveMapUv:"",t.bumpMapUv?"#define BUMPMAP_UV "+t.bumpMapUv:"",t.normalMapUv?"#define NORMALMAP_UV "+t.normalMapUv:"",t.displacementMapUv?"#define DISPLACEMENTMAP_UV "+t.displacementMapUv:"",t.metalnessMapUv?"#define METALNESSMAP_UV "+t.metalnessMapUv:"",t.roughnessMapUv?"#define ROUGHNESSMAP_UV "+t.roughnessMapUv:"",t.anisotropyMapUv?"#define ANISOTROPYMAP_UV "+t.anisotropyMapUv:"",t.clearcoatMapUv?"#define CLEARCOATMAP_UV "+t.clearcoatMapUv:"",t.clearcoatNormalMapUv?"#define CLEARCOAT_NORMALMAP_UV "+t.clearcoatNormalMapUv:"",t.clearcoatRoughnessMapUv?"#define CLEARCOAT_ROUGHNESSMAP_UV "+t.clearcoatRoughnessMapUv:"",t.iridescenceMapUv?"#define IRIDESCENCEMAP_UV "+t.iridescenceMapUv:"",t.iridescenceThicknessMapUv?"#define IRIDESCENCE_THICKNESSMAP_UV "+t.iridescenceThicknessMapUv:"",t.sheenColorMapUv?"#define SHEEN_COLORMAP_UV "+t.sheenColorMapUv:"",t.sheenRoughnessMapUv?"#define SHEEN_ROUGHNESSMAP_UV "+t.sheenRoughnessMapUv:"",t.specularMapUv?"#define SPECULARMAP_UV "+t.specularMapUv:"",t.specularColorMapUv?"#define SPECULAR_COLORMAP_UV "+t.specularColorMapUv:"",t.specularIntensityMapUv?"#define SPECULAR_INTENSITYMAP_UV "+t.specularIntensityMapUv:"",t.transmissionMapUv?"#define TRANSMISSIONMAP_UV "+t.transmissionMapUv:"",t.thicknessMapUv?"#define THICKNESSMAP_UV "+t.thicknessMapUv:"",t.vertexTangents&&t.flatShading===!1?"#define USE_TANGENT":"",t.vertexColors?"#define USE_COLOR":"",t.vertexAlphas?"#define USE_COLOR_ALPHA":"",t.vertexUv1s?"#define USE_UV1":"",t.vertexUv2s?"#define USE_UV2":"",t.vertexUv3s?"#define USE_UV3":"",t.pointsUvs?"#define USE_POINTS_UV":"",t.flatShading?"#define FLAT_SHADED":"",t.skinning?"#define USE_SKINNING":"",t.morphTargets?"#define USE_MORPHTARGETS":"",t.morphNormals&&t.flatShading===!1?"#define USE_MORPHNORMALS":"",t.morphColors?"#define USE_MORPHCOLORS":"",t.morphTargetsCount>0?"#define MORPHTARGETS_TEXTURE_STRIDE "+t.morphTextureStride:"",t.morphTargetsCount>0?"#define MORPHTARGETS_COUNT "+t.morphTargetsCount:"",t.doubleSided?"#define DOUBLE_SIDED":"",t.flipSided?"#define FLIP_SIDED":"",t.shadowMapEnabled?"#define USE_SHADOWMAP":"",t.shadowMapEnabled?"#define "+l:"",t.sizeAttenuation?"#define USE_SIZEATTENUATION":"",t.numLightProbes>0?"#define USE_LIGHT_PROBES":"",t.logarithmicDepthBuffer?"#define USE_LOGDEPTHBUF":"",t.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(Ir).join(` -`),m=[$u(t),"#define SHADER_TYPE "+t.shaderType,"#define SHADER_NAME "+t.shaderName,p,t.useFog&&t.fog?"#define USE_FOG":"",t.useFog&&t.fogExp2?"#define FOG_EXP2":"",t.alphaToCoverage?"#define ALPHA_TO_COVERAGE":"",t.map?"#define USE_MAP":"",t.matcap?"#define USE_MATCAP":"",t.envMap?"#define USE_ENVMAP":"",t.envMap?"#define "+c:"",t.envMap?"#define "+h:"",t.envMap?"#define "+u:"",f?"#define CUBEUV_TEXEL_WIDTH "+f.texelWidth:"",f?"#define CUBEUV_TEXEL_HEIGHT "+f.texelHeight:"",f?"#define CUBEUV_MAX_MIP "+f.maxMip+".0":"",t.lightMap?"#define USE_LIGHTMAP":"",t.aoMap?"#define USE_AOMAP":"",t.bumpMap?"#define USE_BUMPMAP":"",t.normalMap?"#define USE_NORMALMAP":"",t.normalMapObjectSpace?"#define USE_NORMALMAP_OBJECTSPACE":"",t.normalMapTangentSpace?"#define USE_NORMALMAP_TANGENTSPACE":"",t.emissiveMap?"#define USE_EMISSIVEMAP":"",t.anisotropy?"#define USE_ANISOTROPY":"",t.anisotropyMap?"#define USE_ANISOTROPYMAP":"",t.clearcoat?"#define USE_CLEARCOAT":"",t.clearcoatMap?"#define USE_CLEARCOATMAP":"",t.clearcoatRoughnessMap?"#define USE_CLEARCOAT_ROUGHNESSMAP":"",t.clearcoatNormalMap?"#define USE_CLEARCOAT_NORMALMAP":"",t.dispersion?"#define USE_DISPERSION":"",t.iridescence?"#define USE_IRIDESCENCE":"",t.iridescenceMap?"#define USE_IRIDESCENCEMAP":"",t.iridescenceThicknessMap?"#define USE_IRIDESCENCE_THICKNESSMAP":"",t.specularMap?"#define USE_SPECULARMAP":"",t.specularColorMap?"#define USE_SPECULAR_COLORMAP":"",t.specularIntensityMap?"#define USE_SPECULAR_INTENSITYMAP":"",t.roughnessMap?"#define USE_ROUGHNESSMAP":"",t.metalnessMap?"#define USE_METALNESSMAP":"",t.alphaMap?"#define USE_ALPHAMAP":"",t.alphaTest?"#define USE_ALPHATEST":"",t.alphaHash?"#define USE_ALPHAHASH":"",t.sheen?"#define USE_SHEEN":"",t.sheenColorMap?"#define USE_SHEEN_COLORMAP":"",t.sheenRoughnessMap?"#define USE_SHEEN_ROUGHNESSMAP":"",t.transmission?"#define USE_TRANSMISSION":"",t.transmissionMap?"#define USE_TRANSMISSIONMAP":"",t.thicknessMap?"#define USE_THICKNESSMAP":"",t.vertexTangents&&t.flatShading===!1?"#define USE_TANGENT":"",t.vertexColors||t.instancingColor||t.batchingColor?"#define USE_COLOR":"",t.vertexAlphas?"#define USE_COLOR_ALPHA":"",t.vertexUv1s?"#define USE_UV1":"",t.vertexUv2s?"#define USE_UV2":"",t.vertexUv3s?"#define USE_UV3":"",t.pointsUvs?"#define USE_POINTS_UV":"",t.gradientMap?"#define USE_GRADIENTMAP":"",t.flatShading?"#define FLAT_SHADED":"",t.doubleSided?"#define DOUBLE_SIDED":"",t.flipSided?"#define FLIP_SIDED":"",t.shadowMapEnabled?"#define USE_SHADOWMAP":"",t.shadowMapEnabled?"#define "+l:"",t.premultipliedAlpha?"#define PREMULTIPLIED_ALPHA":"",t.numLightProbes>0?"#define USE_LIGHT_PROBES":"",t.decodeVideoTexture?"#define DECODE_VIDEO_TEXTURE":"",t.decodeVideoTextureEmissive?"#define DECODE_VIDEO_TEXTURE_EMISSIVE":"",t.logarithmicDepthBuffer?"#define USE_LOGDEPTHBUF":"",t.reverseDepthBuffer?"#define USE_REVERSEDEPTHBUF":"","uniform mat4 viewMatrix;","uniform vec3 cameraPosition;","uniform bool isOrthographic;",t.toneMapping!==Ti?"#define TONE_MAPPING":"",t.toneMapping!==Ti?st.tonemapping_pars_fragment:"",t.toneMapping!==Ti?Hx("toneMapping",t.toneMapping):"",t.dithering?"#define DITHERING":"",t.opaque?"#define OPAQUE":"",st.colorspace_pars_fragment,Vx("linearToOutputTexel",t.outputColorSpace),Gx(),t.useDepthPacking?"#define DEPTH_PACKING "+t.depthPacking:"",` -`].filter(Ir).join(` -`)),a=Mc(a),a=Zu(a,t),a=Ju(a,t),o=Mc(o),o=Zu(o,t),o=Ju(o,t),a=Ku(a),o=Ku(o),t.isRawShaderMaterial!==!0&&(y=`#version 300 es -`,g=[d,"#define attribute in","#define varying out","#define texture2D texture"].join(` -`)+` -`+g,m=["#define varying in",t.glslVersion===Mh?"":"layout(location = 0) out highp vec4 pc_fragColor;",t.glslVersion===Mh?"":"#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);const v=y+g+a,x=y+m+o,w=Xu(i,i.VERTEX_SHADER,v),T=Xu(i,i.FRAGMENT_SHADER,x);i.attachShader(_,w),i.attachShader(_,T),t.index0AttributeName!==void 0?i.bindAttribLocation(_,0,t.index0AttributeName):t.morphTargets===!0&&i.bindAttribLocation(_,0,"position"),i.linkProgram(_);function C(D){if(r.debug.checkShaderErrors){const G=i.getProgramInfoLog(_).trim(),F=i.getShaderInfoLog(w).trim(),V=i.getShaderInfoLog(T).trim();let J=!0,q=!0;if(i.getProgramParameter(_,i.LINK_STATUS)===!1)if(J=!1,typeof r.debug.onShaderError=="function")r.debug.onShaderError(i,_,w,T);else{const se=Yu(i,w,"vertex"),Z=Yu(i,T,"fragment");console.error("THREE.WebGLProgram: Shader Error "+i.getError()+" - VALIDATE_STATUS "+i.getProgramParameter(_,i.VALIDATE_STATUS)+` - -Material Name: `+D.name+` -Material Type: `+D.type+` - -Program Info Log: `+G+` -`+se+` -`+Z)}else G!==""?console.warn("THREE.WebGLProgram: Program Info Log:",G):(F===""||V==="")&&(q=!1);q&&(D.diagnostics={runnable:J,programLog:G,vertexShader:{log:F,prefix:g},fragmentShader:{log:V,prefix:m}})}i.deleteShader(w),i.deleteShader(T),P=new $a(i,_),S=qx(i,_)}let P;this.getUniforms=function(){return P===void 0&&C(this),P};let S;this.getAttributes=function(){return S===void 0&&C(this),S};let M=t.rendererExtensionParallelShaderCompile===!1;return this.isReady=function(){return M===!1&&(M=i.getProgramParameter(_,Ox)),M},this.destroy=function(){n.releaseStatesOfProgram(this),i.deleteProgram(_),this.program=void 0},this.type=t.shaderType,this.name=t.shaderName,this.id=Bx++,this.cacheKey=e,this.usedTimes=1,this.program=_,this.vertexShader=w,this.fragmentShader=T,this}let sy=0;class ry{constructor(){this.shaderCache=new Map,this.materialCache=new Map}update(e){const t=e.vertexShader,n=e.fragmentShader,i=this._getShaderStage(t),s=this._getShaderStage(n),a=this._getShaderCacheForMaterial(e);return a.has(i)===!1&&(a.add(i),i.usedTimes++),a.has(s)===!1&&(a.add(s),s.usedTimes++),this}remove(e){const t=this.materialCache.get(e);for(const n of t)n.usedTimes--,n.usedTimes===0&&this.shaderCache.delete(n.code);return this.materialCache.delete(e),this}getVertexShaderID(e){return this._getShaderStage(e.vertexShader).id}getFragmentShaderID(e){return this._getShaderStage(e.fragmentShader).id}dispose(){this.shaderCache.clear(),this.materialCache.clear()}_getShaderCacheForMaterial(e){const t=this.materialCache;let n=t.get(e);return n===void 0&&(n=new Set,t.set(e,n)),n}_getShaderStage(e){const t=this.shaderCache;let n=t.get(e);return n===void 0&&(n=new ay(e),t.set(e,n)),n}}class ay{constructor(e){this.id=sy++,this.code=e,this.usedTimes=0}}function oy(r,e,t,n,i,s,a){const o=new Vc,l=new ry,c=new Set,h=[],u=i.logarithmicDepthBuffer,f=i.vertexTextures;let d=i.precision;const p={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 g(S,M,D,G,F){const V=G.fog,J=F.geometry,q=S.isMeshStandardMaterial?G.environment:null,se=(S.isMeshStandardMaterial?t:e).get(S.envMap||q),Z=se&&se.mapping===qr?se.image.height:null,_e=p[S.type];S.precision!==null&&(d=i.getMaxPrecision(S.precision),d!==S.precision&&console.warn("THREE.WebGLProgram.getParameters:",S.precision,"not supported, using",d,"instead."));const be=J.morphAttributes.position||J.morphAttributes.normal||J.morphAttributes.color,Re=be!==void 0?be.length:0;let Ge=0;J.morphAttributes.position!==void 0&&(Ge=1),J.morphAttributes.normal!==void 0&&(Ge=2),J.morphAttributes.color!==void 0&&(Ge=3);let it,Q,ue,Ie;if(_e){const at=On[_e];it=at.vertexShader,Q=at.fragmentShader}else it=S.vertexShader,Q=S.fragmentShader,l.update(S),ue=l.getVertexShaderID(S),Ie=l.getFragmentShaderID(S);const pe=r.getRenderTarget(),Oe=r.state.buffers.depth.getReversed(),We=F.isInstancedMesh===!0,ze=F.isBatchedMesh===!0,Je=!!S.map,te=!!S.matcap,de=!!se,I=!!S.aoMap,De=!!S.lightMap,re=!!S.bumpMap,Ae=!!S.normalMap,me=!!S.displacementMap,Ve=!!S.emissiveMap,Se=!!S.metalnessMap,R=!!S.roughnessMap,b=S.anisotropy>0,H=S.clearcoat>0,k=S.dispersion>0,X=S.iridescence>0,W=S.sheen>0,ce=S.transmission>0,oe=b&&!!S.anisotropyMap,ge=H&&!!S.clearcoatMap,Be=H&&!!S.clearcoatNormalMap,$=H&&!!S.clearcoatRoughnessMap,ve=X&&!!S.iridescenceMap,Y=X&&!!S.iridescenceThicknessMap,ie=W&&!!S.sheenColorMap,ae=W&&!!S.sheenRoughnessMap,Ne=!!S.specularMap,Pe=!!S.specularColorMap,Ke=!!S.specularIntensityMap,U=ce&&!!S.transmissionMap,ye=ce&&!!S.thicknessMap,K=!!S.gradientMap,ee=!!S.alphaMap,we=S.alphaTest>0,Me=!!S.alphaHash,Ye=!!S.extensions;let Mt=Ti;S.toneMapped&&(pe===null||pe.isXRRenderTarget===!0)&&(Mt=r.toneMapping);const St={shaderID:_e,shaderType:S.type,shaderName:S.name,vertexShader:it,fragmentShader:Q,defines:S.defines,customVertexShaderID:ue,customFragmentShaderID:Ie,isRawShaderMaterial:S.isRawShaderMaterial===!0,glslVersion:S.glslVersion,precision:d,batching:ze,batchingColor:ze&&F._colorsTexture!==null,instancing:We,instancingColor:We&&F.instanceColor!==null,instancingMorph:We&&F.morphTexture!==null,supportsVertexTextures:f,outputColorSpace:pe===null?r.outputColorSpace:pe.isXRRenderTarget===!0?pe.texture.colorSpace:Qs,alphaToCoverage:!!S.alphaToCoverage,map:Je,matcap:te,envMap:de,envMapMode:de&&se.mapping,envMapCubeUVHeight:Z,aoMap:I,lightMap:De,bumpMap:re,normalMap:Ae,displacementMap:f&&me,emissiveMap:Ve,normalMapObjectSpace:Ae&&S.normalMapType===op,normalMapTangentSpace:Ae&&S.normalMapType===cs,metalnessMap:Se,roughnessMap:R,anisotropy:b,anisotropyMap:oe,clearcoat:H,clearcoatMap:ge,clearcoatNormalMap:Be,clearcoatRoughnessMap:$,dispersion:k,iridescence:X,iridescenceMap:ve,iridescenceThicknessMap:Y,sheen:W,sheenColorMap:ie,sheenRoughnessMap:ae,specularMap:Ne,specularColorMap:Pe,specularIntensityMap:Ke,transmission:ce,transmissionMap:U,thicknessMap:ye,gradientMap:K,opaque:S.transparent===!1&&S.blending===qs&&S.alphaToCoverage===!1,alphaMap:ee,alphaTest:we,alphaHash:Me,combine:S.combine,mapUv:Je&&_(S.map.channel),aoMapUv:I&&_(S.aoMap.channel),lightMapUv:De&&_(S.lightMap.channel),bumpMapUv:re&&_(S.bumpMap.channel),normalMapUv:Ae&&_(S.normalMap.channel),displacementMapUv:me&&_(S.displacementMap.channel),emissiveMapUv:Ve&&_(S.emissiveMap.channel),metalnessMapUv:Se&&_(S.metalnessMap.channel),roughnessMapUv:R&&_(S.roughnessMap.channel),anisotropyMapUv:oe&&_(S.anisotropyMap.channel),clearcoatMapUv:ge&&_(S.clearcoatMap.channel),clearcoatNormalMapUv:Be&&_(S.clearcoatNormalMap.channel),clearcoatRoughnessMapUv:$&&_(S.clearcoatRoughnessMap.channel),iridescenceMapUv:ve&&_(S.iridescenceMap.channel),iridescenceThicknessMapUv:Y&&_(S.iridescenceThicknessMap.channel),sheenColorMapUv:ie&&_(S.sheenColorMap.channel),sheenRoughnessMapUv:ae&&_(S.sheenRoughnessMap.channel),specularMapUv:Ne&&_(S.specularMap.channel),specularColorMapUv:Pe&&_(S.specularColorMap.channel),specularIntensityMapUv:Ke&&_(S.specularIntensityMap.channel),transmissionMapUv:U&&_(S.transmissionMap.channel),thicknessMapUv:ye&&_(S.thicknessMap.channel),alphaMapUv:ee&&_(S.alphaMap.channel),vertexTangents:!!J.attributes.tangent&&(Ae||b),vertexColors:S.vertexColors,vertexAlphas:S.vertexColors===!0&&!!J.attributes.color&&J.attributes.color.itemSize===4,pointsUvs:F.isPoints===!0&&!!J.attributes.uv&&(Je||ee),fog:!!V,useFog:S.fog===!0,fogExp2:!!V&&V.isFogExp2,flatShading:S.flatShading===!0,sizeAttenuation:S.sizeAttenuation===!0,logarithmicDepthBuffer:u,reverseDepthBuffer:Oe,skinning:F.isSkinnedMesh===!0,morphTargets:J.morphAttributes.position!==void 0,morphNormals:J.morphAttributes.normal!==void 0,morphColors:J.morphAttributes.color!==void 0,morphTargetsCount:Re,morphTextureStride:Ge,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:r.shadowMap.enabled&&D.length>0,shadowMapType:r.shadowMap.type,toneMapping:Mt,decodeVideoTexture:Je&&S.map.isVideoTexture===!0&&ht.getTransfer(S.map.colorSpace)===vt,decodeVideoTextureEmissive:Ve&&S.emissiveMap.isVideoTexture===!0&&ht.getTransfer(S.emissiveMap.colorSpace)===vt,premultipliedAlpha:S.premultipliedAlpha,doubleSided:S.side===Bn,flipSided:S.side===fn,useDepthPacking:S.depthPacking>=0,depthPacking:S.depthPacking||0,index0AttributeName:S.index0AttributeName,extensionClipCullDistance:Ye&&S.extensions.clipCullDistance===!0&&n.has("WEBGL_clip_cull_distance"),extensionMultiDraw:(Ye&&S.extensions.multiDraw===!0||ze)&&n.has("WEBGL_multi_draw"),rendererExtensionParallelShaderCompile:n.has("KHR_parallel_shader_compile"),customProgramCacheKey:S.customProgramCacheKey()};return St.vertexUv1s=c.has(1),St.vertexUv2s=c.has(2),St.vertexUv3s=c.has(3),c.clear(),St}function m(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&&(y(M,S),v(M,S),M.push(r.outputColorSpace)),M.push(S.customProgramCacheKey),M.join()}function y(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 v(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 x(S){const M=p[S.type];let D;if(M){const G=On[M];D=fo.clone(G.uniforms)}else D=S.uniforms;return D}function w(S,M){let D;for(let G=0,F=h.length;G0?n.push(m):d.transparent===!0?i.push(m):t.push(m)}function l(u,f,d,p,_,g){const m=a(u,f,d,p,_,g);d.transmission>0?n.unshift(m):d.transparent===!0?i.unshift(m):t.unshift(m)}function c(u,f){t.length>1&&t.sort(u||cy),n.length>1&&n.sort(f||ju),i.length>1&&i.sort(f||ju)}function h(){for(let u=e,f=r.length;u=s.length?(a=new Qu,s.push(a)):a=s[i],a}function t(){r=new WeakMap}return{get:e,dispose:t}}function uy(){const r={};return{get:function(e){if(r[e.id]!==void 0)return r[e.id];let t;switch(e.type){case"DirectionalLight":t={direction:new A,color:new ne};break;case"SpotLight":t={position:new A,direction:new A,color:new ne,distance:0,coneCos:0,penumbraCos:0,decay:0};break;case"PointLight":t={position:new A,color:new ne,distance:0,decay:0};break;case"HemisphereLight":t={direction:new A,skyColor:new ne,groundColor:new ne};break;case"RectAreaLight":t={color:new ne,position:new A,halfWidth:new A,halfHeight:new A};break}return r[e.id]=t,t}}}function dy(){const r={};return{get:function(e){if(r[e.id]!==void 0)return r[e.id];let t;switch(e.type){case"DirectionalLight":t={shadowIntensity:1,shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new j};break;case"SpotLight":t={shadowIntensity:1,shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new j};break;case"PointLight":t={shadowIntensity:1,shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new j,shadowCameraNear:1,shadowCameraFar:1e3};break}return r[e.id]=t,t}}}let fy=0;function py(r,e){return(e.castShadow?2:0)-(r.castShadow?2:0)+(e.map?1:0)-(r.map?1:0)}function my(r){const e=new uy,t=dy(),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 A);const i=new A,s=new Ze,a=new Ze;function o(c){let h=0,u=0,f=0;for(let S=0;S<9;S++)n.probe[S].set(0,0,0);let d=0,p=0,_=0,g=0,m=0,y=0,v=0,x=0,w=0,T=0,C=0;c.sort(py);for(let S=0,M=c.length;S0&&(r.has("OES_texture_float_linear")===!0?(n.rectAreaLTC1=Te.LTC_FLOAT_1,n.rectAreaLTC2=Te.LTC_FLOAT_2):(n.rectAreaLTC1=Te.LTC_HALF_1,n.rectAreaLTC2=Te.LTC_HALF_2)),n.ambient[0]=h,n.ambient[1]=u,n.ambient[2]=f;const P=n.hash;(P.directionalLength!==d||P.pointLength!==p||P.spotLength!==_||P.rectAreaLength!==g||P.hemiLength!==m||P.numDirectionalShadows!==y||P.numPointShadows!==v||P.numSpotShadows!==x||P.numSpotMaps!==w||P.numLightProbes!==C)&&(n.directional.length=d,n.spot.length=_,n.rectArea.length=g,n.point.length=p,n.hemi.length=m,n.directionalShadow.length=y,n.directionalShadowMap.length=y,n.pointShadow.length=v,n.pointShadowMap.length=v,n.spotShadow.length=x,n.spotShadowMap.length=x,n.directionalShadowMatrix.length=y,n.pointShadowMatrix.length=v,n.spotLightMatrix.length=x+w-T,n.spotLightMap.length=w,n.numSpotLightShadowsWithMaps=T,n.numLightProbes=C,P.directionalLength=d,P.pointLength=p,P.spotLength=_,P.rectAreaLength=g,P.hemiLength=m,P.numDirectionalShadows=y,P.numPointShadows=v,P.numSpotShadows=x,P.numSpotMaps=w,P.numLightProbes=C,n.version=fy++)}function l(c,h){let u=0,f=0,d=0,p=0,_=0;const g=h.matrixWorldInverse;for(let m=0,y=c.length;m=a.length?(o=new ed(r),a.push(o)):o=a[s],o}function n(){e=new WeakMap}return{get:t,dispose:n}}const _y=`void main() { - gl_Position = vec4( position, 1.0 ); -}`,vy=`uniform sampler2D shadow_pass; -uniform vec2 resolution; -uniform float radius; -#include -void main() { - const float samples = float( VSM_SAMPLES ); - float mean = 0.0; - float squared_mean = 0.0; - float uvStride = samples <= 1.0 ? 0.0 : 2.0 / ( samples - 1.0 ); - float uvStart = samples <= 1.0 ? 0.0 : - 1.0; - for ( float i = 0.0; i < samples; i ++ ) { - float uvOffset = uvStart + i * uvStride; - #ifdef HORIZONTAL_PASS - vec2 distribution = unpackRGBATo2Half( texture2D( shadow_pass, ( gl_FragCoord.xy + vec2( uvOffset, 0.0 ) * radius ) / resolution ) ); - mean += distribution.x; - squared_mean += distribution.y * distribution.y + distribution.x * distribution.x; - #else - float depth = unpackRGBAToDepth( texture2D( shadow_pass, ( gl_FragCoord.xy + vec2( 0.0, uvOffset ) * radius ) / resolution ) ); - mean += depth; - squared_mean += depth * depth; - #endif - } - mean = mean / samples; - squared_mean = squared_mean / samples; - float std_dev = sqrt( squared_mean - mean * mean ); - gl_FragColor = pack2HalfToRGBA( vec2( mean, std_dev ) ); -}`;function xy(r,e,t){let n=new To;const i=new j,s=new j,a=new pt,o=new Hd({depthPacking:ap}),l=new Gd,c={},h=t.maxTextureSize,u={[Ci]:fn,[fn]:Ci,[Bn]:Bn},f=new Xt({defines:{VSM_SAMPLES:8},uniforms:{shadow_pass:{value:null},resolution:{value:new j},radius:{value:4}},vertexShader:_y,fragmentShader:vy}),d=f.clone();d.defines.HORIZONTAL_PASS=1;const p=new qe;p.setAttribute("position",new rt(new Float32Array([-1,-1,.5,3,-1,.5,-1,3,.5]),3));const _=new yt(p,f),g=this;this.enabled=!1,this.autoUpdate=!0,this.needsUpdate=!1,this.type=ld;let m=this.type;this.render=function(T,C,P){if(g.enabled===!1||g.autoUpdate===!1&&g.needsUpdate===!1||T.length===0)return;const S=r.getRenderTarget(),M=r.getActiveCubeFace(),D=r.getActiveMipmapLevel(),G=r.state;G.setBlending(ii),G.buffers.color.setClear(1,1,1,1),G.buffers.depth.setTest(!0),G.setScissorTest(!1);const F=m!==Qn&&this.type===Qn,V=m===Qn&&this.type!==Qn;for(let J=0,q=T.length;Jh||i.y>h)&&(i.x>h&&(s.x=Math.floor(h/_e.x),i.x=s.x*_e.x,Z.mapSize.x=s.x),i.y>h&&(s.y=Math.floor(h/_e.y),i.y=s.y*_e.y,Z.mapSize.y=s.y)),Z.map===null||F===!0||V===!0){const Re=this.type!==Qn?{minFilter:Qt,magFilter:Qt}:{};Z.map!==null&&Z.map.dispose(),Z.map=new Sn(i.x,i.y,Re),Z.map.texture.name=se.name+".shadowMap",Z.camera.updateProjectionMatrix()}r.setRenderTarget(Z.map),r.clear();const be=Z.getViewportCount();for(let Re=0;Re0||C.map&&C.alphaTest>0){const G=M.uuid,F=C.uuid;let V=c[G];V===void 0&&(V={},c[G]=V);let J=V[F];J===void 0&&(J=M.clone(),V[F]=J,C.addEventListener("dispose",w)),M=J}if(M.visible=C.visible,M.wireframe=C.wireframe,S===Qn?M.side=C.shadowSide!==null?C.shadowSide:C.side:M.side=C.shadowSide!==null?C.shadowSide:u[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,P.isPointLight===!0&&M.isMeshDistanceMaterial===!0){const G=r.properties.get(M);G.light=P}return M}function x(T,C,P,S,M){if(T.visible===!1)return;if(T.layers.test(C.layers)&&(T.isMesh||T.isLine||T.isPoints)&&(T.castShadow||T.receiveShadow&&M===Qn)&&(!T.frustumCulled||n.intersectsObject(T))){T.modelViewMatrix.multiplyMatrices(P.matrixWorldInverse,T.matrixWorld);const F=e.update(T),V=T.material;if(Array.isArray(V)){const J=F.groups;for(let q=0,se=J.length;q=1):Z.indexOf("OpenGL ES")!==-1&&(se=parseFloat(/^OpenGL ES (\d)/.exec(Z)[1]),q=se>=2);let _e=null,be={};const Re=r.getParameter(r.SCISSOR_BOX),Ge=r.getParameter(r.VIEWPORT),it=new pt().fromArray(Re),Q=new pt().fromArray(Ge);function ue(U,ye,K,ee){const we=new Uint8Array(4),Me=r.createTexture();r.bindTexture(U,Me),r.texParameteri(U,r.TEXTURE_MIN_FILTER,r.NEAREST),r.texParameteri(U,r.TEXTURE_MAG_FILTER,r.NEAREST);for(let Ye=0;Ye"u"?!1:/OculusBrowser/g.test(navigator.userAgent),c=new j,h=new WeakMap;let u;const f=new WeakMap;let d=!1;try{d=typeof OffscreenCanvas<"u"&&new OffscreenCanvas(1,1).getContext("2d")!==null}catch{}function p(R,b){return d?new OffscreenCanvas(R,b):Vr("canvas")}function _(R,b,H){let k=1;const X=Se(R);if((X.width>H||X.height>H)&&(k=H/Math.max(X.width,X.height)),k<1)if(typeof HTMLImageElement<"u"&&R instanceof HTMLImageElement||typeof HTMLCanvasElement<"u"&&R instanceof HTMLCanvasElement||typeof ImageBitmap<"u"&&R instanceof ImageBitmap||typeof VideoFrame<"u"&&R instanceof VideoFrame){const W=Math.floor(k*X.width),ce=Math.floor(k*X.height);u===void 0&&(u=p(W,ce));const oe=b?p(W,ce):u;return oe.width=W,oe.height=ce,oe.getContext("2d").drawImage(R,0,0,W,ce),console.warn("THREE.WebGLRenderer: Texture has been resized from ("+X.width+"x"+X.height+") to ("+W+"x"+ce+")."),oe}else return"data"in R&&console.warn("THREE.WebGLRenderer: Image in DataTexture is too big ("+X.width+"x"+X.height+")."),R;return R}function g(R){return R.generateMipmaps}function m(R){r.generateMipmap(R)}function y(R){return R.isWebGLCubeRenderTarget?r.TEXTURE_CUBE_MAP:R.isWebGL3DRenderTarget?r.TEXTURE_3D:R.isWebGLArrayRenderTarget||R.isCompressedArrayTexture?r.TEXTURE_2D_ARRAY:r.TEXTURE_2D}function v(R,b,H,k,X=!1){if(R!==null){if(r[R]!==void 0)return r[R];console.warn("THREE.WebGLRenderer: Attempt to use non-existing WebGL internal format '"+R+"'")}let W=b;if(b===r.RED&&(H===r.FLOAT&&(W=r.R32F),H===r.HALF_FLOAT&&(W=r.R16F),H===r.UNSIGNED_BYTE&&(W=r.R8)),b===r.RED_INTEGER&&(H===r.UNSIGNED_BYTE&&(W=r.R8UI),H===r.UNSIGNED_SHORT&&(W=r.R16UI),H===r.UNSIGNED_INT&&(W=r.R32UI),H===r.BYTE&&(W=r.R8I),H===r.SHORT&&(W=r.R16I),H===r.INT&&(W=r.R32I)),b===r.RG&&(H===r.FLOAT&&(W=r.RG32F),H===r.HALF_FLOAT&&(W=r.RG16F),H===r.UNSIGNED_BYTE&&(W=r.RG8)),b===r.RG_INTEGER&&(H===r.UNSIGNED_BYTE&&(W=r.RG8UI),H===r.UNSIGNED_SHORT&&(W=r.RG16UI),H===r.UNSIGNED_INT&&(W=r.RG32UI),H===r.BYTE&&(W=r.RG8I),H===r.SHORT&&(W=r.RG16I),H===r.INT&&(W=r.RG32I)),b===r.RGB_INTEGER&&(H===r.UNSIGNED_BYTE&&(W=r.RGB8UI),H===r.UNSIGNED_SHORT&&(W=r.RGB16UI),H===r.UNSIGNED_INT&&(W=r.RGB32UI),H===r.BYTE&&(W=r.RGB8I),H===r.SHORT&&(W=r.RGB16I),H===r.INT&&(W=r.RGB32I)),b===r.RGBA_INTEGER&&(H===r.UNSIGNED_BYTE&&(W=r.RGBA8UI),H===r.UNSIGNED_SHORT&&(W=r.RGBA16UI),H===r.UNSIGNED_INT&&(W=r.RGBA32UI),H===r.BYTE&&(W=r.RGBA8I),H===r.SHORT&&(W=r.RGBA16I),H===r.INT&&(W=r.RGBA32I)),b===r.RGB&&H===r.UNSIGNED_INT_5_9_9_9_REV&&(W=r.RGB9_E5),b===r.RGBA){const ce=X?co:ht.getTransfer(k);H===r.FLOAT&&(W=r.RGBA32F),H===r.HALF_FLOAT&&(W=r.RGBA16F),H===r.UNSIGNED_BYTE&&(W=ce===vt?r.SRGB8_ALPHA8:r.RGBA8),H===r.UNSIGNED_SHORT_4_4_4_4&&(W=r.RGBA4),H===r.UNSIGNED_SHORT_5_5_5_1&&(W=r.RGB5_A1)}return(W===r.R16F||W===r.R32F||W===r.RG16F||W===r.RG32F||W===r.RGBA16F||W===r.RGBA32F)&&e.get("EXT_color_buffer_float"),W}function x(R,b){let H;return R?b===null||b===Ii||b===$s?H=r.DEPTH24_STENCIL8:b===Mn?H=r.DEPTH32F_STENCIL8:b===kr&&(H=r.DEPTH24_STENCIL8,console.warn("DepthTexture: 16 bit depth attachment is not supported with stencil. Using 24-bit attachment.")):b===null||b===Ii||b===$s?H=r.DEPTH_COMPONENT24:b===Mn?H=r.DEPTH_COMPONENT32F:b===kr&&(H=r.DEPTH_COMPONENT16),H}function w(R,b){return g(R)===!0||R.isFramebufferTexture&&R.minFilter!==Qt&&R.minFilter!==Vt?Math.log2(Math.max(b.width,b.height))+1:R.mipmaps!==void 0&&R.mipmaps.length>0?R.mipmaps.length:R.isCompressedTexture&&Array.isArray(R.image)?b.mipmaps.length:1}function T(R){const b=R.target;b.removeEventListener("dispose",T),P(b),b.isVideoTexture&&h.delete(b)}function C(R){const b=R.target;b.removeEventListener("dispose",C),M(b)}function P(R){const b=n.get(R);if(b.__webglInit===void 0)return;const H=R.source,k=f.get(H);if(k){const X=k[b.__cacheKey];X.usedTimes--,X.usedTimes===0&&S(R),Object.keys(k).length===0&&f.delete(H)}n.remove(R)}function S(R){const b=n.get(R);r.deleteTexture(b.__webglTexture);const H=R.source,k=f.get(H);delete k[b.__cacheKey],a.memory.textures--}function M(R){const b=n.get(R);if(R.depthTexture&&(R.depthTexture.dispose(),n.remove(R.depthTexture)),R.isWebGLCubeRenderTarget)for(let k=0;k<6;k++){if(Array.isArray(b.__webglFramebuffer[k]))for(let X=0;X=i.maxTextures&&console.warn("THREE.WebGLTextures: Trying to use "+R+" texture units while this GPU supports only "+i.maxTextures),D+=1,R}function V(R){const b=[];return b.push(R.wrapS),b.push(R.wrapT),b.push(R.wrapR||0),b.push(R.magFilter),b.push(R.minFilter),b.push(R.anisotropy),b.push(R.internalFormat),b.push(R.format),b.push(R.type),b.push(R.generateMipmaps),b.push(R.premultiplyAlpha),b.push(R.flipY),b.push(R.unpackAlignment),b.push(R.colorSpace),b.join()}function J(R,b){const H=n.get(R);if(R.isVideoTexture&&me(R),R.isRenderTargetTexture===!1&&R.version>0&&H.__version!==R.version){const k=R.image;if(k===null)console.warn("THREE.WebGLRenderer: Texture marked for update but no image data found.");else if(k.complete===!1)console.warn("THREE.WebGLRenderer: Texture marked for update but image is incomplete");else{Q(H,R,b);return}}t.bindTexture(r.TEXTURE_2D,H.__webglTexture,r.TEXTURE0+b)}function q(R,b){const H=n.get(R);if(R.version>0&&H.__version!==R.version){Q(H,R,b);return}t.bindTexture(r.TEXTURE_2D_ARRAY,H.__webglTexture,r.TEXTURE0+b)}function se(R,b){const H=n.get(R);if(R.version>0&&H.__version!==R.version){Q(H,R,b);return}t.bindTexture(r.TEXTURE_3D,H.__webglTexture,r.TEXTURE0+b)}function Z(R,b){const H=n.get(R);if(R.version>0&&H.__version!==R.version){ue(H,R,b);return}t.bindTexture(r.TEXTURE_CUBE_MAP,H.__webglTexture,r.TEXTURE0+b)}const _e={[ro]:r.REPEAT,[Nn]:r.CLAMP_TO_EDGE,[ao]:r.MIRRORED_REPEAT},be={[Qt]:r.NEAREST,[hd]:r.NEAREST_MIPMAP_NEAREST,[Cr]:r.NEAREST_MIPMAP_LINEAR,[Vt]:r.LINEAR,[Xa]:r.LINEAR_MIPMAP_NEAREST,[ni]:r.LINEAR_MIPMAP_LINEAR},Re={[lp]:r.NEVER,[pp]:r.ALWAYS,[cp]:r.LESS,[Md]:r.LEQUAL,[hp]:r.EQUAL,[fp]:r.GEQUAL,[up]:r.GREATER,[dp]:r.NOTEQUAL};function Ge(R,b){if(b.type===Mn&&e.has("OES_texture_float_linear")===!1&&(b.magFilter===Vt||b.magFilter===Xa||b.magFilter===Cr||b.magFilter===ni||b.minFilter===Vt||b.minFilter===Xa||b.minFilter===Cr||b.minFilter===ni)&&console.warn("THREE.WebGLRenderer: Unable to use linear filtering with floating point textures. OES_texture_float_linear not supported on this device."),r.texParameteri(R,r.TEXTURE_WRAP_S,_e[b.wrapS]),r.texParameteri(R,r.TEXTURE_WRAP_T,_e[b.wrapT]),(R===r.TEXTURE_3D||R===r.TEXTURE_2D_ARRAY)&&r.texParameteri(R,r.TEXTURE_WRAP_R,_e[b.wrapR]),r.texParameteri(R,r.TEXTURE_MAG_FILTER,be[b.magFilter]),r.texParameteri(R,r.TEXTURE_MIN_FILTER,be[b.minFilter]),b.compareFunction&&(r.texParameteri(R,r.TEXTURE_COMPARE_MODE,r.COMPARE_REF_TO_TEXTURE),r.texParameteri(R,r.TEXTURE_COMPARE_FUNC,Re[b.compareFunction])),e.has("EXT_texture_filter_anisotropic")===!0){if(b.magFilter===Qt||b.minFilter!==Cr&&b.minFilter!==ni||b.type===Mn&&e.has("OES_texture_float_linear")===!1)return;if(b.anisotropy>1||n.get(b).__currentAnisotropy){const H=e.get("EXT_texture_filter_anisotropic");r.texParameterf(R,H.TEXTURE_MAX_ANISOTROPY_EXT,Math.min(b.anisotropy,i.getMaxAnisotropy())),n.get(b).__currentAnisotropy=b.anisotropy}}}function it(R,b){let H=!1;R.__webglInit===void 0&&(R.__webglInit=!0,b.addEventListener("dispose",T));const k=b.source;let X=f.get(k);X===void 0&&(X={},f.set(k,X));const W=V(b);if(W!==R.__cacheKey){X[W]===void 0&&(X[W]={texture:r.createTexture(),usedTimes:0},a.memory.textures++,H=!0),X[W].usedTimes++;const ce=X[R.__cacheKey];ce!==void 0&&(X[R.__cacheKey].usedTimes--,ce.usedTimes===0&&S(b)),R.__cacheKey=W,R.__webglTexture=X[W].texture}return H}function Q(R,b,H){let k=r.TEXTURE_2D;(b.isDataArrayTexture||b.isCompressedArrayTexture)&&(k=r.TEXTURE_2D_ARRAY),b.isData3DTexture&&(k=r.TEXTURE_3D);const X=it(R,b),W=b.source;t.bindTexture(k,R.__webglTexture,r.TEXTURE0+H);const ce=n.get(W);if(W.version!==ce.__version||X===!0){t.activeTexture(r.TEXTURE0+H);const oe=ht.getPrimaries(ht.workingColorSpace),ge=b.colorSpace===wi?null:ht.getPrimaries(b.colorSpace),Be=b.colorSpace===wi||oe===ge?r.NONE:r.BROWSER_DEFAULT_WEBGL;r.pixelStorei(r.UNPACK_FLIP_Y_WEBGL,b.flipY),r.pixelStorei(r.UNPACK_PREMULTIPLY_ALPHA_WEBGL,b.premultiplyAlpha),r.pixelStorei(r.UNPACK_ALIGNMENT,b.unpackAlignment),r.pixelStorei(r.UNPACK_COLORSPACE_CONVERSION_WEBGL,Be);let $=_(b.image,!1,i.maxTextureSize);$=Ve(b,$);const ve=s.convert(b.format,b.colorSpace),Y=s.convert(b.type);let ie=v(b.internalFormat,ve,Y,b.colorSpace,b.isVideoTexture);Ge(k,b);let ae;const Ne=b.mipmaps,Pe=b.isVideoTexture!==!0,Ke=ce.__version===void 0||X===!0,U=W.dataReady,ye=w(b,$);if(b.isDepthTexture)ie=x(b.format===js,b.type),Ke&&(Pe?t.texStorage2D(r.TEXTURE_2D,1,ie,$.width,$.height):t.texImage2D(r.TEXTURE_2D,0,ie,$.width,$.height,0,ve,Y,null));else if(b.isDataTexture)if(Ne.length>0){Pe&&Ke&&t.texStorage2D(r.TEXTURE_2D,ye,ie,Ne[0].width,Ne[0].height);for(let K=0,ee=Ne.length;K0){const we=yc(ae.width,ae.height,b.format,b.type);for(const Me of b.layerUpdates){const Ye=ae.data.subarray(Me*we/ae.data.BYTES_PER_ELEMENT,(Me+1)*we/ae.data.BYTES_PER_ELEMENT);t.compressedTexSubImage3D(r.TEXTURE_2D_ARRAY,K,0,0,Me,ae.width,ae.height,1,ve,Ye)}b.clearLayerUpdates()}else t.compressedTexSubImage3D(r.TEXTURE_2D_ARRAY,K,0,0,0,ae.width,ae.height,$.depth,ve,ae.data)}else t.compressedTexImage3D(r.TEXTURE_2D_ARRAY,K,ie,ae.width,ae.height,$.depth,0,ae.data,0,0);else console.warn("THREE.WebGLRenderer: Attempt to load unsupported compressed texture format in .uploadTexture()");else Pe?U&&t.texSubImage3D(r.TEXTURE_2D_ARRAY,K,0,0,0,ae.width,ae.height,$.depth,ve,Y,ae.data):t.texImage3D(r.TEXTURE_2D_ARRAY,K,ie,ae.width,ae.height,$.depth,0,ve,Y,ae.data)}else{Pe&&Ke&&t.texStorage2D(r.TEXTURE_2D,ye,ie,Ne[0].width,Ne[0].height);for(let K=0,ee=Ne.length;K0){const K=yc($.width,$.height,b.format,b.type);for(const ee of b.layerUpdates){const we=$.data.subarray(ee*K/$.data.BYTES_PER_ELEMENT,(ee+1)*K/$.data.BYTES_PER_ELEMENT);t.texSubImage3D(r.TEXTURE_2D_ARRAY,0,0,0,ee,$.width,$.height,1,ve,Y,we)}b.clearLayerUpdates()}else t.texSubImage3D(r.TEXTURE_2D_ARRAY,0,0,0,0,$.width,$.height,$.depth,ve,Y,$.data)}else t.texImage3D(r.TEXTURE_2D_ARRAY,0,ie,$.width,$.height,$.depth,0,ve,Y,$.data);else if(b.isData3DTexture)Pe?(Ke&&t.texStorage3D(r.TEXTURE_3D,ye,ie,$.width,$.height,$.depth),U&&t.texSubImage3D(r.TEXTURE_3D,0,0,0,0,$.width,$.height,$.depth,ve,Y,$.data)):t.texImage3D(r.TEXTURE_3D,0,ie,$.width,$.height,$.depth,0,ve,Y,$.data);else if(b.isFramebufferTexture){if(Ke)if(Pe)t.texStorage2D(r.TEXTURE_2D,ye,ie,$.width,$.height);else{let K=$.width,ee=$.height;for(let we=0;we>=1,ee>>=1}}else if(Ne.length>0){if(Pe&&Ke){const K=Se(Ne[0]);t.texStorage2D(r.TEXTURE_2D,ye,ie,K.width,K.height)}for(let K=0,ee=Ne.length;K0&&ye++;const ee=Se(ve[0]);t.texStorage2D(r.TEXTURE_CUBE_MAP,ye,Ne,ee.width,ee.height)}for(let ee=0;ee<6;ee++)if($){Pe?U&&t.texSubImage2D(r.TEXTURE_CUBE_MAP_POSITIVE_X+ee,0,0,0,ve[ee].width,ve[ee].height,ie,ae,ve[ee].data):t.texImage2D(r.TEXTURE_CUBE_MAP_POSITIVE_X+ee,0,Ne,ve[ee].width,ve[ee].height,0,ie,ae,ve[ee].data);for(let we=0;we>W),Y=Math.max(1,b.height>>W);X===r.TEXTURE_3D||X===r.TEXTURE_2D_ARRAY?t.texImage3D(X,W,ge,ve,Y,b.depth,0,ce,oe,null):t.texImage2D(X,W,ge,ve,Y,0,ce,oe,null)}t.bindFramebuffer(r.FRAMEBUFFER,R),Ae(b)?o.framebufferTexture2DMultisampleEXT(r.FRAMEBUFFER,k,X,$.__webglTexture,0,re(b)):(X===r.TEXTURE_2D||X>=r.TEXTURE_CUBE_MAP_POSITIVE_X&&X<=r.TEXTURE_CUBE_MAP_NEGATIVE_Z)&&r.framebufferTexture2D(r.FRAMEBUFFER,k,X,$.__webglTexture,W),t.bindFramebuffer(r.FRAMEBUFFER,null)}function pe(R,b,H){if(r.bindRenderbuffer(r.RENDERBUFFER,R),b.depthBuffer){const k=b.depthTexture,X=k&&k.isDepthTexture?k.type:null,W=x(b.stencilBuffer,X),ce=b.stencilBuffer?r.DEPTH_STENCIL_ATTACHMENT:r.DEPTH_ATTACHMENT,oe=re(b);Ae(b)?o.renderbufferStorageMultisampleEXT(r.RENDERBUFFER,oe,W,b.width,b.height):H?r.renderbufferStorageMultisample(r.RENDERBUFFER,oe,W,b.width,b.height):r.renderbufferStorage(r.RENDERBUFFER,W,b.width,b.height),r.framebufferRenderbuffer(r.FRAMEBUFFER,ce,r.RENDERBUFFER,R)}else{const k=b.textures;for(let X=0;X{delete b.__boundDepthTexture,delete b.__depthDisposeCallback,k.removeEventListener("dispose",X)};k.addEventListener("dispose",X),b.__depthDisposeCallback=X}b.__boundDepthTexture=k}if(R.depthTexture&&!b.__autoAllocateDepthBuffer){if(H)throw new Error("target.depthTexture not supported in Cube render targets");Oe(b.__webglFramebuffer,R)}else if(H){b.__webglDepthbuffer=[];for(let k=0;k<6;k++)if(t.bindFramebuffer(r.FRAMEBUFFER,b.__webglFramebuffer[k]),b.__webglDepthbuffer[k]===void 0)b.__webglDepthbuffer[k]=r.createRenderbuffer(),pe(b.__webglDepthbuffer[k],R,!1);else{const X=R.stencilBuffer?r.DEPTH_STENCIL_ATTACHMENT:r.DEPTH_ATTACHMENT,W=b.__webglDepthbuffer[k];r.bindRenderbuffer(r.RENDERBUFFER,W),r.framebufferRenderbuffer(r.FRAMEBUFFER,X,r.RENDERBUFFER,W)}}else if(t.bindFramebuffer(r.FRAMEBUFFER,b.__webglFramebuffer),b.__webglDepthbuffer===void 0)b.__webglDepthbuffer=r.createRenderbuffer(),pe(b.__webglDepthbuffer,R,!1);else{const k=R.stencilBuffer?r.DEPTH_STENCIL_ATTACHMENT:r.DEPTH_ATTACHMENT,X=b.__webglDepthbuffer;r.bindRenderbuffer(r.RENDERBUFFER,X),r.framebufferRenderbuffer(r.FRAMEBUFFER,k,r.RENDERBUFFER,X)}t.bindFramebuffer(r.FRAMEBUFFER,null)}function ze(R,b,H){const k=n.get(R);b!==void 0&&Ie(k.__webglFramebuffer,R,R.texture,r.COLOR_ATTACHMENT0,r.TEXTURE_2D,0),H!==void 0&&We(R)}function Je(R){const b=R.texture,H=n.get(R),k=n.get(b);R.addEventListener("dispose",C);const X=R.textures,W=R.isWebGLCubeRenderTarget===!0,ce=X.length>1;if(ce||(k.__webglTexture===void 0&&(k.__webglTexture=r.createTexture()),k.__version=b.version,a.memory.textures++),W){H.__webglFramebuffer=[];for(let oe=0;oe<6;oe++)if(b.mipmaps&&b.mipmaps.length>0){H.__webglFramebuffer[oe]=[];for(let ge=0;ge0){H.__webglFramebuffer=[];for(let oe=0;oe0&&Ae(R)===!1){H.__webglMultisampledFramebuffer=r.createFramebuffer(),H.__webglColorRenderbuffer=[],t.bindFramebuffer(r.FRAMEBUFFER,H.__webglMultisampledFramebuffer);for(let oe=0;oe0)for(let ge=0;ge0)for(let ge=0;ge0){if(Ae(R)===!1){const b=R.textures,H=R.width,k=R.height;let X=r.COLOR_BUFFER_BIT;const W=R.stencilBuffer?r.DEPTH_STENCIL_ATTACHMENT:r.DEPTH_ATTACHMENT,ce=n.get(R),oe=b.length>1;if(oe)for(let ge=0;ge0&&e.has("WEBGL_multisampled_render_to_texture")===!0&&b.__useRenderToTexture!==!1}function me(R){const b=a.render.frame;h.get(R)!==b&&(h.set(R,b),R.update())}function Ve(R,b){const H=R.colorSpace,k=R.format,X=R.type;return R.isCompressedTexture===!0||R.isVideoTexture===!0||H!==Qs&&H!==wi&&(ht.getTransfer(H)===vt?(k!==dn||X!==li)&&console.warn("THREE.WebGLTextures: sRGB encoded textures have to use RGBAFormat and UnsignedByteType."):console.error("THREE.WebGLTextures: Unsupported texture color space:",H)),b}function Se(R){return typeof HTMLImageElement<"u"&&R instanceof HTMLImageElement?(c.width=R.naturalWidth||R.width,c.height=R.naturalHeight||R.height):typeof VideoFrame<"u"&&R instanceof VideoFrame?(c.width=R.displayWidth,c.height=R.displayHeight):(c.width=R.width,c.height=R.height),c}this.allocateTextureUnit=F,this.resetTextureUnits=G,this.setTexture2D=J,this.setTexture2DArray=q,this.setTexture3D=se,this.setTextureCube=Z,this.rebindTextures=ze,this.setupRenderTarget=Je,this.updateRenderTargetMipmap=te,this.updateMultisampleRenderTarget=De,this.setupDepthRenderbuffer=We,this.setupFrameBufferTexture=Ie,this.useMultisampledRTT=Ae}function Sy(r,e){function t(n,i=wi){let s;const a=ht.getTransfer(i);if(n===li)return r.UNSIGNED_BYTE;if(n===Dc)return r.UNSIGNED_SHORT_4_4_4_4;if(n===Lc)return r.UNSIGNED_SHORT_5_5_5_1;if(n===fd)return r.UNSIGNED_INT_5_9_9_9_REV;if(n===ud)return r.BYTE;if(n===dd)return r.SHORT;if(n===kr)return r.UNSIGNED_SHORT;if(n===Pc)return r.INT;if(n===Ii)return r.UNSIGNED_INT;if(n===Mn)return r.FLOAT;if(n===si)return r.HALF_FLOAT;if(n===pd)return r.ALPHA;if(n===md)return r.RGB;if(n===dn)return r.RGBA;if(n===gd)return r.LUMINANCE;if(n===_d)return r.LUMINANCE_ALPHA;if(n===Ys)return r.DEPTH_COMPONENT;if(n===js)return r.DEPTH_STENCIL;if(n===Uc)return r.RED;if(n===So)return r.RED_INTEGER;if(n===vd)return r.RG;if(n===Nc)return r.RG_INTEGER;if(n===Fc)return r.RGBA_INTEGER;if(n===qa||n===Ya||n===Za||n===Ja)if(a===vt)if(s=e.get("WEBGL_compressed_texture_s3tc_srgb"),s!==null){if(n===qa)return s.COMPRESSED_SRGB_S3TC_DXT1_EXT;if(n===Ya)return s.COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT;if(n===Za)return s.COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT;if(n===Ja)return s.COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT}else return null;else if(s=e.get("WEBGL_compressed_texture_s3tc"),s!==null){if(n===qa)return s.COMPRESSED_RGB_S3TC_DXT1_EXT;if(n===Ya)return s.COMPRESSED_RGBA_S3TC_DXT1_EXT;if(n===Za)return s.COMPRESSED_RGBA_S3TC_DXT3_EXT;if(n===Ja)return s.COMPRESSED_RGBA_S3TC_DXT5_EXT}else return null;if(n===kl||n===Vl||n===Hl||n===Gl)if(s=e.get("WEBGL_compressed_texture_pvrtc"),s!==null){if(n===kl)return s.COMPRESSED_RGB_PVRTC_4BPPV1_IMG;if(n===Vl)return s.COMPRESSED_RGB_PVRTC_2BPPV1_IMG;if(n===Hl)return s.COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;if(n===Gl)return s.COMPRESSED_RGBA_PVRTC_2BPPV1_IMG}else return null;if(n===Wl||n===Xl||n===ql)if(s=e.get("WEBGL_compressed_texture_etc"),s!==null){if(n===Wl||n===Xl)return a===vt?s.COMPRESSED_SRGB8_ETC2:s.COMPRESSED_RGB8_ETC2;if(n===ql)return a===vt?s.COMPRESSED_SRGB8_ALPHA8_ETC2_EAC:s.COMPRESSED_RGBA8_ETC2_EAC}else return null;if(n===Yl||n===Zl||n===Jl||n===Kl||n===$l||n===jl||n===Ql||n===ec||n===tc||n===nc||n===ic||n===sc||n===rc||n===ac)if(s=e.get("WEBGL_compressed_texture_astc"),s!==null){if(n===Yl)return a===vt?s.COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR:s.COMPRESSED_RGBA_ASTC_4x4_KHR;if(n===Zl)return a===vt?s.COMPRESSED_SRGB8_ALPHA8_ASTC_5x4_KHR:s.COMPRESSED_RGBA_ASTC_5x4_KHR;if(n===Jl)return a===vt?s.COMPRESSED_SRGB8_ALPHA8_ASTC_5x5_KHR:s.COMPRESSED_RGBA_ASTC_5x5_KHR;if(n===Kl)return a===vt?s.COMPRESSED_SRGB8_ALPHA8_ASTC_6x5_KHR:s.COMPRESSED_RGBA_ASTC_6x5_KHR;if(n===$l)return a===vt?s.COMPRESSED_SRGB8_ALPHA8_ASTC_6x6_KHR:s.COMPRESSED_RGBA_ASTC_6x6_KHR;if(n===jl)return a===vt?s.COMPRESSED_SRGB8_ALPHA8_ASTC_8x5_KHR:s.COMPRESSED_RGBA_ASTC_8x5_KHR;if(n===Ql)return a===vt?s.COMPRESSED_SRGB8_ALPHA8_ASTC_8x6_KHR:s.COMPRESSED_RGBA_ASTC_8x6_KHR;if(n===ec)return a===vt?s.COMPRESSED_SRGB8_ALPHA8_ASTC_8x8_KHR:s.COMPRESSED_RGBA_ASTC_8x8_KHR;if(n===tc)return a===vt?s.COMPRESSED_SRGB8_ALPHA8_ASTC_10x5_KHR:s.COMPRESSED_RGBA_ASTC_10x5_KHR;if(n===nc)return a===vt?s.COMPRESSED_SRGB8_ALPHA8_ASTC_10x6_KHR:s.COMPRESSED_RGBA_ASTC_10x6_KHR;if(n===ic)return a===vt?s.COMPRESSED_SRGB8_ALPHA8_ASTC_10x8_KHR:s.COMPRESSED_RGBA_ASTC_10x8_KHR;if(n===sc)return a===vt?s.COMPRESSED_SRGB8_ALPHA8_ASTC_10x10_KHR:s.COMPRESSED_RGBA_ASTC_10x10_KHR;if(n===rc)return a===vt?s.COMPRESSED_SRGB8_ALPHA8_ASTC_12x10_KHR:s.COMPRESSED_RGBA_ASTC_12x10_KHR;if(n===ac)return a===vt?s.COMPRESSED_SRGB8_ALPHA8_ASTC_12x12_KHR:s.COMPRESSED_RGBA_ASTC_12x12_KHR}else return null;if(n===Ka||n===oc||n===lc)if(s=e.get("EXT_texture_compression_bptc"),s!==null){if(n===Ka)return a===vt?s.COMPRESSED_SRGB_ALPHA_BPTC_UNORM_EXT:s.COMPRESSED_RGBA_BPTC_UNORM_EXT;if(n===oc)return s.COMPRESSED_RGB_BPTC_SIGNED_FLOAT_EXT;if(n===lc)return s.COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_EXT}else return null;if(n===xd||n===cc||n===hc||n===uc)if(s=e.get("EXT_texture_compression_rgtc"),s!==null){if(n===Ka)return s.COMPRESSED_RED_RGTC1_EXT;if(n===cc)return s.COMPRESSED_SIGNED_RED_RGTC1_EXT;if(n===hc)return s.COMPRESSED_RED_GREEN_RGTC2_EXT;if(n===uc)return s.COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT}else return null;return n===$s?r.UNSIGNED_INT_24_8:r[n]!==void 0?r[n]:null}return{convert:t}}const wy={type:"move"};class wl{constructor(){this._targetRay=null,this._grip=null,this._hand=null}getHandSpace(){return this._hand===null&&(this._hand=new ns,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 ns,this._targetRay.matrixAutoUpdate=!1,this._targetRay.visible=!1,this._targetRay.hasLinearVelocity=!1,this._targetRay.linearVelocity=new A,this._targetRay.hasAngularVelocity=!1,this._targetRay.angularVelocity=new A),this._targetRay}getGripSpace(){return this._grip===null&&(this._grip=new ns,this._grip.matrixAutoUpdate=!1,this._grip.visible=!1,this._grip.hasLinearVelocity=!1,this._grip.linearVelocity=new A,this._grip.hasAngularVelocity=!1,this._grip.angularVelocity=new A),this._grip}dispatchEvent(e){return this._targetRay!==null&&this._targetRay.dispatchEvent(e),this._grip!==null&&this._grip.dispatchEvent(e),this._hand!==null&&this._hand.dispatchEvent(e),this}connect(e){if(e&&e.hand){const t=this._hand;if(t)for(const n of e.hand.values())this._getHandJoint(t,n)}return this.dispatchEvent({type:"connected",data:e}),this}disconnect(e){return this.dispatchEvent({type:"disconnected",data:e}),this._targetRay!==null&&(this._targetRay.visible=!1),this._grip!==null&&(this._grip.visible=!1),this._hand!==null&&(this._hand.visible=!1),this}update(e,t,n){let i=null,s=null,a=null;const o=this._targetRay,l=this._grip,c=this._hand;if(e&&t.session.visibilityState!=="visible-blurred"){if(c&&e.hand){a=!0;for(const _ of e.hand.values()){const g=t.getJointPose(_,n),m=this._getHandJoint(c,_);g!==null&&(m.matrix.fromArray(g.transform.matrix),m.matrix.decompose(m.position,m.rotation,m.scale),m.matrixWorldNeedsUpdate=!0,m.jointRadius=g.radius),m.visible=g!==null}const h=c.joints["index-finger-tip"],u=c.joints["thumb-tip"],f=h.position.distanceTo(u.position),d=.02,p=.005;c.inputState.pinching&&f>d+p?(c.inputState.pinching=!1,this.dispatchEvent({type:"pinchend",handedness:e.handedness,target:this})):!c.inputState.pinching&&f<=d-p&&(c.inputState.pinching=!0,this.dispatchEvent({type:"pinchstart",handedness:e.handedness,target:this}))}else l!==null&&e.gripSpace&&(s=t.getPose(e.gripSpace,n),s!==null&&(l.matrix.fromArray(s.transform.matrix),l.matrix.decompose(l.position,l.rotation,l.scale),l.matrixWorldNeedsUpdate=!0,s.linearVelocity?(l.hasLinearVelocity=!0,l.linearVelocity.copy(s.linearVelocity)):l.hasLinearVelocity=!1,s.angularVelocity?(l.hasAngularVelocity=!0,l.angularVelocity.copy(s.angularVelocity)):l.hasAngularVelocity=!1));o!==null&&(i=t.getPose(e.targetRaySpace,n),i===null&&s!==null&&(i=s),i!==null&&(o.matrix.fromArray(i.transform.matrix),o.matrix.decompose(o.position,o.rotation,o.scale),o.matrixWorldNeedsUpdate=!0,i.linearVelocity?(o.hasLinearVelocity=!0,o.linearVelocity.copy(i.linearVelocity)):o.hasLinearVelocity=!1,i.angularVelocity?(o.hasAngularVelocity=!0,o.angularVelocity.copy(i.angularVelocity)):o.hasAngularVelocity=!1,this.dispatchEvent(wy)))}return o!==null&&(o.visible=i!==null),l!==null&&(l.visible=s!==null),c!==null&&(c.visible=a!==null),this}_getHandJoint(e,t){if(e.joints[t.jointName]===void 0){const n=new ns;n.matrixAutoUpdate=!1,n.visible=!1,e.joints[t.jointName]=n,e.add(n)}return e.joints[t.jointName]}}const Ey=` -void main() { - - gl_Position = vec4( position, 1.0 ); - -}`,Ty=` -uniform sampler2DArray depthColor; -uniform float depthWidth; -uniform float depthHeight; - -void main() { - - vec2 coord = vec2( gl_FragCoord.x / depthWidth, gl_FragCoord.y / depthHeight ); - - if ( coord.x >= 1.0 ) { - - gl_FragDepth = texture( depthColor, vec3( coord.x - 1.0, coord.y, 1 ) ).r; - - } else { - - gl_FragDepth = texture( depthColor, vec3( coord.x, coord.y, 0 ) ).r; - - } - -}`;class Ay{constructor(){this.texture=null,this.mesh=null,this.depthNear=0,this.depthFar=0}init(e,t,n){if(this.texture===null){const i=new Et,s=e.properties.get(i);s.__webglTexture=t.texture,(t.depthNear!==n.depthNear||t.depthFar!==n.depthFar)&&(this.depthNear=t.depthNear,this.depthFar=t.depthFar),this.texture=i}}getMesh(e){if(this.texture!==null&&this.mesh===null){const t=e.cameras[0].viewport,n=new Xt({vertexShader:Ey,fragmentShader:Ty,uniforms:{depthColor:{value:this.texture},depthWidth:{value:t.z},depthHeight:{value:t.w}}});this.mesh=new yt(new lr(20,20),n)}return this.mesh}reset(){this.texture=null,this.mesh=null}getDepthTexture(){return this.texture}}class Cy extends ci{constructor(e,t){super();const n=this;let i=null,s=1,a=null,o="local-floor",l=1,c=null,h=null,u=null,f=null,d=null,p=null;const _=new Ay,g=t.getContextAttributes();let m=null,y=null;const v=[],x=[],w=new j;let T=null;const C=new kt;C.viewport=new pt;const P=new kt;P.viewport=new pt;const S=[C,P],M=new Ag;let D=null,G=null;this.cameraAutoUpdate=!0,this.enabled=!1,this.isPresenting=!1,this.getController=function(Q){let ue=v[Q];return ue===void 0&&(ue=new wl,v[Q]=ue),ue.getTargetRaySpace()},this.getControllerGrip=function(Q){let ue=v[Q];return ue===void 0&&(ue=new wl,v[Q]=ue),ue.getGripSpace()},this.getHand=function(Q){let ue=v[Q];return ue===void 0&&(ue=new wl,v[Q]=ue),ue.getHandSpace()};function F(Q){const ue=x.indexOf(Q.inputSource);if(ue===-1)return;const Ie=v[ue];Ie!==void 0&&(Ie.update(Q.inputSource,Q.frame,c||a),Ie.dispatchEvent({type:Q.type,data:Q.inputSource}))}function V(){i.removeEventListener("select",F),i.removeEventListener("selectstart",F),i.removeEventListener("selectend",F),i.removeEventListener("squeeze",F),i.removeEventListener("squeezestart",F),i.removeEventListener("squeezeend",F),i.removeEventListener("end",V),i.removeEventListener("inputsourceschange",J);for(let Q=0;Q=0&&(x[pe]=null,v[pe].disconnect(Ie))}for(let ue=0;ue=x.length){x.push(Ie),pe=We;break}else if(x[We]===null){x[We]=Ie,pe=We;break}if(pe===-1)break}const Oe=v[pe];Oe&&Oe.connect(Ie)}}const q=new A,se=new A;function Z(Q,ue,Ie){q.setFromMatrixPosition(ue.matrixWorld),se.setFromMatrixPosition(Ie.matrixWorld);const pe=q.distanceTo(se),Oe=ue.projectionMatrix.elements,We=Ie.projectionMatrix.elements,ze=Oe[14]/(Oe[10]-1),Je=Oe[14]/(Oe[10]+1),te=(Oe[9]+1)/Oe[5],de=(Oe[9]-1)/Oe[5],I=(Oe[8]-1)/Oe[0],De=(We[8]+1)/We[0],re=ze*I,Ae=ze*De,me=pe/(-I+De),Ve=me*-I;if(ue.matrixWorld.decompose(Q.position,Q.quaternion,Q.scale),Q.translateX(Ve),Q.translateZ(me),Q.matrixWorld.compose(Q.position,Q.quaternion,Q.scale),Q.matrixWorldInverse.copy(Q.matrixWorld).invert(),Oe[10]===-1)Q.projectionMatrix.copy(ue.projectionMatrix),Q.projectionMatrixInverse.copy(ue.projectionMatrixInverse);else{const Se=ze+me,R=Je+me,b=re-Ve,H=Ae+(pe-Ve),k=te*Je/R*Se,X=de*Je/R*Se;Q.projectionMatrix.makePerspective(b,H,k,X,Se,R),Q.projectionMatrixInverse.copy(Q.projectionMatrix).invert()}}function _e(Q,ue){ue===null?Q.matrixWorld.copy(Q.matrix):Q.matrixWorld.multiplyMatrices(ue.matrixWorld,Q.matrix),Q.matrixWorldInverse.copy(Q.matrixWorld).invert()}this.updateCamera=function(Q){if(i===null)return;let ue=Q.near,Ie=Q.far;_.texture!==null&&(_.depthNear>0&&(ue=_.depthNear),_.depthFar>0&&(Ie=_.depthFar)),M.near=P.near=C.near=ue,M.far=P.far=C.far=Ie,(D!==M.near||G!==M.far)&&(i.updateRenderState({depthNear:M.near,depthFar:M.far}),D=M.near,G=M.far),C.layers.mask=Q.layers.mask|2,P.layers.mask=Q.layers.mask|4,M.layers.mask=C.layers.mask|P.layers.mask;const pe=Q.parent,Oe=M.cameras;_e(M,pe);for(let We=0;We0&&(g.alphaTest.value=m.alphaTest);const y=e.get(m),v=y.envMap,x=y.envMapRotation;v&&(g.envMap.value=v,Zi.copy(x),Zi.x*=-1,Zi.y*=-1,Zi.z*=-1,v.isCubeTexture&&v.isRenderTargetTexture===!1&&(Zi.y*=-1,Zi.z*=-1),g.envMapRotation.value.setFromMatrix4(Ry.makeRotationFromEuler(Zi)),g.flipEnvMap.value=v.isCubeTexture&&v.isRenderTargetTexture===!1?-1:1,g.reflectivity.value=m.reflectivity,g.ior.value=m.ior,g.refractionRatio.value=m.refractionRatio),m.lightMap&&(g.lightMap.value=m.lightMap,g.lightMapIntensity.value=m.lightMapIntensity,t(m.lightMap,g.lightMapTransform)),m.aoMap&&(g.aoMap.value=m.aoMap,g.aoMapIntensity.value=m.aoMapIntensity,t(m.aoMap,g.aoMapTransform))}function a(g,m){g.diffuse.value.copy(m.color),g.opacity.value=m.opacity,m.map&&(g.map.value=m.map,t(m.map,g.mapTransform))}function o(g,m){g.dashSize.value=m.dashSize,g.totalSize.value=m.dashSize+m.gapSize,g.scale.value=m.scale}function l(g,m,y,v){g.diffuse.value.copy(m.color),g.opacity.value=m.opacity,g.size.value=m.size*y,g.scale.value=v*.5,m.map&&(g.map.value=m.map,t(m.map,g.uvTransform)),m.alphaMap&&(g.alphaMap.value=m.alphaMap,t(m.alphaMap,g.alphaMapTransform)),m.alphaTest>0&&(g.alphaTest.value=m.alphaTest)}function c(g,m){g.diffuse.value.copy(m.color),g.opacity.value=m.opacity,g.rotation.value=m.rotation,m.map&&(g.map.value=m.map,t(m.map,g.mapTransform)),m.alphaMap&&(g.alphaMap.value=m.alphaMap,t(m.alphaMap,g.alphaMapTransform)),m.alphaTest>0&&(g.alphaTest.value=m.alphaTest)}function h(g,m){g.specular.value.copy(m.specular),g.shininess.value=Math.max(m.shininess,1e-4)}function u(g,m){m.gradientMap&&(g.gradientMap.value=m.gradientMap)}function f(g,m){g.metalness.value=m.metalness,m.metalnessMap&&(g.metalnessMap.value=m.metalnessMap,t(m.metalnessMap,g.metalnessMapTransform)),g.roughness.value=m.roughness,m.roughnessMap&&(g.roughnessMap.value=m.roughnessMap,t(m.roughnessMap,g.roughnessMapTransform)),m.envMap&&(g.envMapIntensity.value=m.envMapIntensity)}function d(g,m,y){g.ior.value=m.ior,m.sheen>0&&(g.sheenColor.value.copy(m.sheenColor).multiplyScalar(m.sheen),g.sheenRoughness.value=m.sheenRoughness,m.sheenColorMap&&(g.sheenColorMap.value=m.sheenColorMap,t(m.sheenColorMap,g.sheenColorMapTransform)),m.sheenRoughnessMap&&(g.sheenRoughnessMap.value=m.sheenRoughnessMap,t(m.sheenRoughnessMap,g.sheenRoughnessMapTransform))),m.clearcoat>0&&(g.clearcoat.value=m.clearcoat,g.clearcoatRoughness.value=m.clearcoatRoughness,m.clearcoatMap&&(g.clearcoatMap.value=m.clearcoatMap,t(m.clearcoatMap,g.clearcoatMapTransform)),m.clearcoatRoughnessMap&&(g.clearcoatRoughnessMap.value=m.clearcoatRoughnessMap,t(m.clearcoatRoughnessMap,g.clearcoatRoughnessMapTransform)),m.clearcoatNormalMap&&(g.clearcoatNormalMap.value=m.clearcoatNormalMap,t(m.clearcoatNormalMap,g.clearcoatNormalMapTransform),g.clearcoatNormalScale.value.copy(m.clearcoatNormalScale),m.side===fn&&g.clearcoatNormalScale.value.negate())),m.dispersion>0&&(g.dispersion.value=m.dispersion),m.iridescence>0&&(g.iridescence.value=m.iridescence,g.iridescenceIOR.value=m.iridescenceIOR,g.iridescenceThicknessMinimum.value=m.iridescenceThicknessRange[0],g.iridescenceThicknessMaximum.value=m.iridescenceThicknessRange[1],m.iridescenceMap&&(g.iridescenceMap.value=m.iridescenceMap,t(m.iridescenceMap,g.iridescenceMapTransform)),m.iridescenceThicknessMap&&(g.iridescenceThicknessMap.value=m.iridescenceThicknessMap,t(m.iridescenceThicknessMap,g.iridescenceThicknessMapTransform))),m.transmission>0&&(g.transmission.value=m.transmission,g.transmissionSamplerMap.value=y.texture,g.transmissionSamplerSize.value.set(y.width,y.height),m.transmissionMap&&(g.transmissionMap.value=m.transmissionMap,t(m.transmissionMap,g.transmissionMapTransform)),g.thickness.value=m.thickness,m.thicknessMap&&(g.thicknessMap.value=m.thicknessMap,t(m.thicknessMap,g.thicknessMapTransform)),g.attenuationDistance.value=m.attenuationDistance,g.attenuationColor.value.copy(m.attenuationColor)),m.anisotropy>0&&(g.anisotropyVector.value.set(m.anisotropy*Math.cos(m.anisotropyRotation),m.anisotropy*Math.sin(m.anisotropyRotation)),m.anisotropyMap&&(g.anisotropyMap.value=m.anisotropyMap,t(m.anisotropyMap,g.anisotropyMapTransform))),g.specularIntensity.value=m.specularIntensity,g.specularColor.value.copy(m.specularColor),m.specularColorMap&&(g.specularColorMap.value=m.specularColorMap,t(m.specularColorMap,g.specularColorMapTransform)),m.specularIntensityMap&&(g.specularIntensityMap.value=m.specularIntensityMap,t(m.specularIntensityMap,g.specularIntensityMapTransform))}function p(g,m){m.matcap&&(g.matcap.value=m.matcap)}function _(g,m){const y=e.get(m).light;g.referencePosition.value.setFromMatrixPosition(y.matrixWorld),g.nearDistance.value=y.shadow.camera.near,g.farDistance.value=y.shadow.camera.far}return{refreshFogUniforms:n,refreshMaterialUniforms:i}}function Py(r,e,t,n){let i={},s={},a=[];const o=r.getParameter(r.MAX_UNIFORM_BUFFER_BINDINGS);function l(y,v){const x=v.program;n.uniformBlockBinding(y,x)}function c(y,v){let x=i[y.id];x===void 0&&(p(y),x=h(y),i[y.id]=x,y.addEventListener("dispose",g));const w=v.program;n.updateUBOMapping(y,w);const T=e.render.frame;s[y.id]!==T&&(f(y),s[y.id]=T)}function h(y){const v=u();y.__bindingPointIndex=v;const x=r.createBuffer(),w=y.__size,T=y.usage;return r.bindBuffer(r.UNIFORM_BUFFER,x),r.bufferData(r.UNIFORM_BUFFER,w,T),r.bindBuffer(r.UNIFORM_BUFFER,null),r.bindBufferBase(r.UNIFORM_BUFFER,v,x),x}function u(){for(let y=0;y0&&(x+=w-T),y.__size=x,y.__cache={},this}function _(y){const v={boundary:0,storage:0};return typeof y=="number"||typeof y=="boolean"?(v.boundary=4,v.storage=4):y.isVector2?(v.boundary=8,v.storage=8):y.isVector3||y.isColor?(v.boundary=16,v.storage=12):y.isVector4?(v.boundary=16,v.storage=16):y.isMatrix3?(v.boundary=48,v.storage=48):y.isMatrix4?(v.boundary=64,v.storage=64):y.isTexture?console.warn("THREE.WebGLRenderer: Texture samplers can not be part of an uniforms group."):console.warn("THREE.WebGLRenderer: Unsupported uniform value type.",y),v}function g(y){const v=y.target;v.removeEventListener("dispose",g);const x=a.indexOf(v.__bindingPointIndex);a.splice(x,1),r.deleteBuffer(i[v.id]),delete i[v.id],delete s[v.id]}function m(){for(const y in i)r.deleteBuffer(i[y]);a=[],i={},s={}}return{bind:l,update:c,dispose:m}}class Dy{constructor(e={}){const{canvas:t=Dp(),context:n=null,depth:i=!0,stencil:s=!1,alpha:a=!1,antialias:o=!1,premultipliedAlpha:l=!0,preserveDrawingBuffer:c=!1,powerPreference:h="default",failIfMajorPerformanceCaveat:u=!1,reverseDepthBuffer:f=!1}=e;this.isWebGLRenderer=!0;let d;if(n!==null){if(typeof WebGLRenderingContext<"u"&&n instanceof WebGLRenderingContext)throw new Error("THREE.WebGLRenderer: WebGL 1 is not supported since r163.");d=n.getContextAttributes().alpha}else d=a;const p=new Uint32Array(4),_=new Int32Array(4);let g=null,m=null;const y=[],v=[];this.domElement=t,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=xn,this.toneMapping=Ti,this.toneMappingExposure=1;const x=this;let w=!1,T=0,C=0,P=null,S=-1,M=null;const D=new pt,G=new pt;let F=null;const V=new ne(0);let J=0,q=t.width,se=t.height,Z=1,_e=null,be=null;const Re=new pt(0,0,q,se),Ge=new pt(0,0,q,se);let it=!1;const Q=new To;let ue=!1,Ie=!1;this.transmissionResolutionScale=1;const pe=new Ze,Oe=new Ze,We=new A,ze=new pt,Je={background:null,fog:null,environment:null,overrideMaterial:null,isScene:!0};let te=!1;function de(){return P===null?Z:1}let I=n;function De(E,B){return t.getContext(E,B)}try{const E={alpha:!0,depth:i,stencil:s,antialias:o,premultipliedAlpha:l,preserveDrawingBuffer:c,powerPreference:h,failIfMajorPerformanceCaveat:u};if("setAttribute"in t&&t.setAttribute("data-engine",`three.js r${Rc}`),t.addEventListener("webglcontextlost",ee,!1),t.addEventListener("webglcontextrestored",we,!1),t.addEventListener("webglcontextcreationerror",Me,!1),I===null){const B="webgl2";if(I=De(B,E),I===null)throw De(B)?new Error("Error creating WebGL context with your selected attributes."):new Error("Error creating WebGL context.")}}catch(E){throw console.error("THREE.WebGLRenderer: "+E.message),E}let re,Ae,me,Ve,Se,R,b,H,k,X,W,ce,oe,ge,Be,$,ve,Y,ie,ae,Ne,Pe,Ke,U;function ye(){re=new Vv(I),re.init(),Pe=new Sy(I,re),Ae=new Nv(I,re,e,Pe),me=new My(I,re),Ae.reverseDepthBuffer&&f&&me.buffers.depth.setReversed(!0),Ve=new Wv(I),Se=new ly,R=new by(I,re,me,Se,Ae,Pe,Ve),b=new Ov(x),H=new kv(x),k=new $g(I),Ke=new Lv(I,k),X=new Hv(I,k,Ve,Ke),W=new qv(I,X,k,Ve),ie=new Xv(I,Ae,R),$=new Fv(Se),ce=new oy(x,b,H,re,Ae,Ke,$),oe=new Iy(x,Se),ge=new hy,Be=new gy(re),Y=new Dv(x,b,H,me,W,d,l),ve=new xy(x,W,Ae),U=new Py(I,Ve,Ae,me),ae=new Uv(I,re,Ve),Ne=new Gv(I,re,Ve),Ve.programs=ce.programs,x.capabilities=Ae,x.extensions=re,x.properties=Se,x.renderLists=ge,x.shadowMap=ve,x.state=me,x.info=Ve}ye();const K=new Cy(x,I);this.xr=K,this.getContext=function(){return I},this.getContextAttributes=function(){return I.getContextAttributes()},this.forceContextLoss=function(){const E=re.get("WEBGL_lose_context");E&&E.loseContext()},this.forceContextRestore=function(){const E=re.get("WEBGL_lose_context");E&&E.restoreContext()},this.getPixelRatio=function(){return Z},this.setPixelRatio=function(E){E!==void 0&&(Z=E,this.setSize(q,se,!1))},this.getSize=function(E){return E.set(q,se)},this.setSize=function(E,B,N=!0){if(K.isPresenting){console.warn("THREE.WebGLRenderer: Can't change size while VR device is presenting.");return}q=E,se=B,t.width=Math.floor(E*Z),t.height=Math.floor(B*Z),N===!0&&(t.style.width=E+"px",t.style.height=B+"px"),this.setViewport(0,0,E,B)},this.getDrawingBufferSize=function(E){return E.set(q*Z,se*Z).floor()},this.setDrawingBufferSize=function(E,B,N){q=E,se=B,Z=N,t.width=Math.floor(E*N),t.height=Math.floor(B*N),this.setViewport(0,0,E,B)},this.getCurrentViewport=function(E){return E.copy(D)},this.getViewport=function(E){return E.copy(Re)},this.setViewport=function(E,B,N,z){E.isVector4?Re.set(E.x,E.y,E.z,E.w):Re.set(E,B,N,z),me.viewport(D.copy(Re).multiplyScalar(Z).round())},this.getScissor=function(E){return E.copy(Ge)},this.setScissor=function(E,B,N,z){E.isVector4?Ge.set(E.x,E.y,E.z,E.w):Ge.set(E,B,N,z),me.scissor(G.copy(Ge).multiplyScalar(Z).round())},this.getScissorTest=function(){return it},this.setScissorTest=function(E){me.setScissorTest(it=E)},this.setOpaqueSort=function(E){_e=E},this.setTransparentSort=function(E){be=E},this.getClearColor=function(E){return E.copy(Y.getClearColor())},this.setClearColor=function(){Y.setClearColor.apply(Y,arguments)},this.getClearAlpha=function(){return Y.getClearAlpha()},this.setClearAlpha=function(){Y.setClearAlpha.apply(Y,arguments)},this.clear=function(E=!0,B=!0,N=!0){let z=0;if(E){let O=!1;if(P!==null){const he=P.texture.format;O=he===Fc||he===Nc||he===So}if(O){const he=P.texture.type,Ce=he===li||he===Ii||he===kr||he===$s||he===Dc||he===Lc,Le=Y.getClearColor(),Ue=Y.getClearAlpha(),$e=Le.r,je=Le.g,He=Le.b;Ce?(p[0]=$e,p[1]=je,p[2]=He,p[3]=Ue,I.clearBufferuiv(I.COLOR,0,p)):(_[0]=$e,_[1]=je,_[2]=He,_[3]=Ue,I.clearBufferiv(I.COLOR,0,_))}else z|=I.COLOR_BUFFER_BIT}B&&(z|=I.DEPTH_BUFFER_BIT),N&&(z|=I.STENCIL_BUFFER_BIT,this.state.buffers.stencil.setMask(4294967295)),I.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(){t.removeEventListener("webglcontextlost",ee,!1),t.removeEventListener("webglcontextrestored",we,!1),t.removeEventListener("webglcontextcreationerror",Me,!1),Y.dispose(),ge.dispose(),Be.dispose(),Se.dispose(),b.dispose(),H.dispose(),W.dispose(),Ke.dispose(),U.dispose(),ce.dispose(),K.dispose(),K.removeEventListener("sessionstart",Di),K.removeEventListener("sessionend",ui),En.stop()};function ee(E){E.preventDefault(),console.log("THREE.WebGLRenderer: Context Lost."),w=!0}function we(){console.log("THREE.WebGLRenderer: Context Restored."),w=!1;const E=Ve.autoReset,B=ve.enabled,N=ve.autoUpdate,z=ve.needsUpdate,O=ve.type;ye(),Ve.autoReset=E,ve.enabled=B,ve.autoUpdate=N,ve.needsUpdate=z,ve.type=O}function Me(E){console.error("THREE.WebGLRenderer: A WebGL context could not be created. Reason: ",E.statusMessage)}function Ye(E){const B=E.target;B.removeEventListener("dispose",Ye),Mt(B)}function Mt(E){St(E),Se.remove(E)}function St(E){const B=Se.get(E).programs;B!==void 0&&(B.forEach(function(N){ce.releaseProgram(N)}),E.isShaderMaterial&&ce.releaseShaderCache(E))}this.renderBufferDirect=function(E,B,N,z,O,he){B===null&&(B=Je);const Ce=O.isMesh&&O.matrixWorld.determinant()<0,Le=Nt(E,B,N,z,O);me.setMaterial(z,Ce);let Ue=N.index,$e=1;if(z.wireframe===!0){if(Ue=X.getWireframeAttribute(N),Ue===void 0)return;$e=2}const je=N.drawRange,He=N.attributes.position;let lt=je.start*$e,gt=(je.start+je.count)*$e;he!==null&&(lt=Math.max(lt,he.start*$e),gt=Math.min(gt,(he.start+he.count)*$e)),Ue!==null?(lt=Math.max(lt,0),gt=Math.min(gt,Ue.count)):He!=null&&(lt=Math.max(lt,0),gt=Math.min(gt,He.count));const Dt=gt-lt;if(Dt<0||Dt===1/0)return;Ke.setup(O,z,Le,N,Ue);let Tt,dt=ae;if(Ue!==null&&(Tt=k.get(Ue),dt=Ne,dt.setIndex(Tt)),O.isMesh)z.wireframe===!0?(me.setLineWidth(z.wireframeLinewidth*de()),dt.setMode(I.LINES)):dt.setMode(I.TRIANGLES);else if(O.isLine){let Xe=z.linewidth;Xe===void 0&&(Xe=1),me.setLineWidth(Xe*de()),O.isLineSegments?dt.setMode(I.LINES):O.isLineLoop?dt.setMode(I.LINE_LOOP):dt.setMode(I.LINE_STRIP)}else O.isPoints?dt.setMode(I.POINTS):O.isSprite&&dt.setMode(I.TRIANGLES);if(O.isBatchedMesh)if(O._multiDrawInstances!==null)dt.renderMultiDrawInstances(O._multiDrawStarts,O._multiDrawCounts,O._multiDrawCount,O._multiDrawInstances);else if(re.get("WEBGL_multi_draw"))dt.renderMultiDraw(O._multiDrawStarts,O._multiDrawCounts,O._multiDrawCount);else{const Xe=O._multiDrawStarts,Gt=O._multiDrawCounts,_t=O._multiDrawCount,Pn=Ue?k.get(Ue).bytesPerElement:1,fs=Se.get(z).currentProgram.getUniforms();for(let gn=0;gn<_t;gn++)fs.setValue(I,"_gl_DrawID",gn),dt.render(Xe[gn]/Pn,Gt[gn])}else if(O.isInstancedMesh)dt.renderInstances(lt,Dt,O.count);else if(N.isInstancedBufferGeometry){const Xe=N._maxInstanceCount!==void 0?N._maxInstanceCount:1/0,Gt=Math.min(N.instanceCount,Xe);dt.renderInstances(lt,Dt,Gt)}else dt.render(lt,Dt)};function at(E,B,N){E.transparent===!0&&E.side===Bn&&E.forceSinglePass===!1?(E.side=fn,E.needsUpdate=!0,Fn(E,B,N),E.side=Ci,E.needsUpdate=!0,Fn(E,B,N),E.side=Bn):Fn(E,B,N)}this.compile=function(E,B,N=null){N===null&&(N=E),m=Be.get(N),m.init(B),v.push(m),N.traverseVisible(function(O){O.isLight&&O.layers.test(B.layers)&&(m.pushLight(O),O.castShadow&&m.pushShadow(O))}),E!==N&&E.traverseVisible(function(O){O.isLight&&O.layers.test(B.layers)&&(m.pushLight(O),O.castShadow&&m.pushShadow(O))}),m.setupLights();const z=new Set;return E.traverse(function(O){if(!(O.isMesh||O.isPoints||O.isLine||O.isSprite))return;const he=O.material;if(he)if(Array.isArray(he))for(let Ce=0;Ce{function he(){if(z.forEach(function(Ce){Se.get(Ce).currentProgram.isReady()&&z.delete(Ce)}),z.size===0){O(E);return}setTimeout(he,10)}re.get("KHR_parallel_shader_compile")!==null?he():setTimeout(he,10)})};let Yt=null;function Ht(E){Yt&&Yt(E)}function Di(){En.stop()}function ui(){En.start()}const En=new tf;En.setAnimationLoop(Ht),typeof self<"u"&&En.setContext(self),this.setAnimationLoop=function(E){Yt=E,K.setAnimationLoop(E),E===null?En.stop():En.start()},K.addEventListener("sessionstart",Di),K.addEventListener("sessionend",ui),this.render=function(E,B){if(B!==void 0&&B.isCamera!==!0){console.error("THREE.WebGLRenderer.render: camera is not an instance of THREE.Camera.");return}if(w===!0)return;if(E.matrixWorldAutoUpdate===!0&&E.updateMatrixWorld(),B.parent===null&&B.matrixWorldAutoUpdate===!0&&B.updateMatrixWorld(),K.enabled===!0&&K.isPresenting===!0&&(K.cameraAutoUpdate===!0&&K.updateCamera(B),B=K.getCamera()),E.isScene===!0&&E.onBeforeRender(x,E,B,P),m=Be.get(E,v.length),m.init(B),v.push(m),Oe.multiplyMatrices(B.projectionMatrix,B.matrixWorldInverse),Q.setFromProjectionMatrix(Oe),Ie=this.localClippingEnabled,ue=$.init(this.clippingPlanes,Ie),g=ge.get(E,y.length),g.init(),y.push(g),K.enabled===!0&&K.isPresenting===!0){const he=x.xr.getDepthSensingMesh();he!==null&&di(he,B,-1/0,x.sortObjects)}di(E,B,0,x.sortObjects),g.finish(),x.sortObjects===!0&&g.sort(_e,be),te=K.enabled===!1||K.isPresenting===!1||K.hasDepthSensing()===!1,te&&Y.addToRenderList(g,E),this.info.render.frame++,ue===!0&&$.beginShadows();const N=m.state.shadowsArray;ve.render(N,E,B),ue===!0&&$.endShadows(),this.info.autoReset===!0&&this.info.reset();const z=g.opaque,O=g.transmissive;if(m.setupLights(),B.isArrayCamera){const he=B.cameras;if(O.length>0)for(let Ce=0,Le=he.length;Ce0&&Wn(z,O,E,B),te&&Y.render(E),Li(g,E,B);P!==null&&C===0&&(R.updateMultisampleRenderTarget(P),R.updateRenderTargetMipmap(P)),E.isScene===!0&&E.onAfterRender(x,E,B),Ke.resetDefaultState(),S=-1,M=null,v.pop(),v.length>0?(m=v[v.length-1],ue===!0&&$.setGlobalState(x.clippingPlanes,m.state.camera)):m=null,y.pop(),y.length>0?g=y[y.length-1]:g=null};function di(E,B,N,z){if(E.visible===!1)return;if(E.layers.test(B.layers)){if(E.isGroup)N=E.renderOrder;else if(E.isLOD)E.autoUpdate===!0&&E.update(B);else if(E.isLight)m.pushLight(E),E.castShadow&&m.pushShadow(E);else if(E.isSprite){if(!E.frustumCulled||Q.intersectsSprite(E)){z&&ze.setFromMatrixPosition(E.matrixWorld).applyMatrix4(Oe);const Ce=W.update(E),Le=E.material;Le.visible&&g.push(E,Ce,Le,N,ze.z,null)}}else if((E.isMesh||E.isLine||E.isPoints)&&(!E.frustumCulled||Q.intersectsObject(E))){const Ce=W.update(E),Le=E.material;if(z&&(E.boundingSphere!==void 0?(E.boundingSphere===null&&E.computeBoundingSphere(),ze.copy(E.boundingSphere.center)):(Ce.boundingSphere===null&&Ce.computeBoundingSphere(),ze.copy(Ce.boundingSphere.center)),ze.applyMatrix4(E.matrixWorld).applyMatrix4(Oe)),Array.isArray(Le)){const Ue=Ce.groups;for(let $e=0,je=Ue.length;$e0&&Xn(O,B,N),he.length>0&&Xn(he,B,N),Ce.length>0&&Xn(Ce,B,N),me.buffers.depth.setTest(!0),me.buffers.depth.setMask(!0),me.buffers.color.setMask(!0),me.setPolygonOffset(!1)}function Wn(E,B,N,z){if((N.isScene===!0?N.overrideMaterial:null)!==null)return;m.state.transmissionRenderTarget[z.id]===void 0&&(m.state.transmissionRenderTarget[z.id]=new Sn(1,1,{generateMipmaps:!0,type:re.has("EXT_color_buffer_half_float")||re.has("EXT_color_buffer_float")?si:li,minFilter:ni,samples:4,stencilBuffer:s,resolveDepthBuffer:!1,resolveStencilBuffer:!1,colorSpace:ht.workingColorSpace}));const he=m.state.transmissionRenderTarget[z.id],Ce=z.viewport||D;he.setSize(Ce.z*x.transmissionResolutionScale,Ce.w*x.transmissionResolutionScale);const Le=x.getRenderTarget();x.setRenderTarget(he),x.getClearColor(V),J=x.getClearAlpha(),J<1&&x.setClearColor(16777215,.5),x.clear(),te&&Y.render(N);const Ue=x.toneMapping;x.toneMapping=Ti;const $e=z.viewport;if(z.viewport!==void 0&&(z.viewport=void 0),m.setupLightsView(z),ue===!0&&$.setGlobalState(x.clippingPlanes,z),Xn(E,N,z),R.updateMultisampleRenderTarget(he),R.updateRenderTargetMipmap(he),re.has("WEBGL_multisampled_render_to_texture")===!1){let je=!1;for(let He=0,lt=B.length;He0),He=!!N.morphAttributes.position,lt=!!N.morphAttributes.normal,gt=!!N.morphAttributes.color;let Dt=Ti;z.toneMapped&&(P===null||P.isXRRenderTarget===!0)&&(Dt=x.toneMapping);const Tt=N.morphAttributes.position||N.morphAttributes.normal||N.morphAttributes.color,dt=Tt!==void 0?Tt.length:0,Xe=Se.get(z),Gt=m.state.lights;if(ue===!0&&(Ie===!0||E!==M)){const en=E===M&&z.id===S;$.setState(z,E,en)}let _t=!1;z.version===Xe.__version?(Xe.needsLights&&Xe.lightsStateVersion!==Gt.state.version||Xe.outputColorSpace!==Le||O.isBatchedMesh&&Xe.batching===!1||!O.isBatchedMesh&&Xe.batching===!0||O.isBatchedMesh&&Xe.batchingColor===!0&&O.colorTexture===null||O.isBatchedMesh&&Xe.batchingColor===!1&&O.colorTexture!==null||O.isInstancedMesh&&Xe.instancing===!1||!O.isInstancedMesh&&Xe.instancing===!0||O.isSkinnedMesh&&Xe.skinning===!1||!O.isSkinnedMesh&&Xe.skinning===!0||O.isInstancedMesh&&Xe.instancingColor===!0&&O.instanceColor===null||O.isInstancedMesh&&Xe.instancingColor===!1&&O.instanceColor!==null||O.isInstancedMesh&&Xe.instancingMorph===!0&&O.morphTexture===null||O.isInstancedMesh&&Xe.instancingMorph===!1&&O.morphTexture!==null||Xe.envMap!==Ue||z.fog===!0&&Xe.fog!==he||Xe.numClippingPlanes!==void 0&&(Xe.numClippingPlanes!==$.numPlanes||Xe.numIntersection!==$.numIntersection)||Xe.vertexAlphas!==$e||Xe.vertexTangents!==je||Xe.morphTargets!==He||Xe.morphNormals!==lt||Xe.morphColors!==gt||Xe.toneMapping!==Dt||Xe.morphTargetsCount!==dt)&&(_t=!0):(_t=!0,Xe.__version=z.version);let Pn=Xe.currentProgram;_t===!0&&(Pn=Fn(z,B,O));let fs=!1,gn=!1,pr=!1;const bt=Pn.getUniforms(),An=Xe.uniforms;if(me.useProgram(Pn.program)&&(fs=!0,gn=!0,pr=!0),z.id!==S&&(S=z.id,gn=!0),fs||M!==E){me.buffers.depth.getReversed()?(pe.copy(E.projectionMatrix),Up(pe),Np(pe),bt.setValue(I,"projectionMatrix",pe)):bt.setValue(I,"projectionMatrix",E.projectionMatrix),bt.setValue(I,"viewMatrix",E.matrixWorldInverse);const ln=bt.map.cameraPosition;ln!==void 0&&ln.setValue(I,We.setFromMatrixPosition(E.matrixWorld)),Ae.logarithmicDepthBuffer&&bt.setValue(I,"logDepthBufFC",2/(Math.log(E.far+1)/Math.LN2)),(z.isMeshPhongMaterial||z.isMeshToonMaterial||z.isMeshLambertMaterial||z.isMeshBasicMaterial||z.isMeshStandardMaterial||z.isShaderMaterial)&&bt.setValue(I,"isOrthographic",E.isOrthographicCamera===!0),M!==E&&(M=E,gn=!0,pr=!0)}if(O.isSkinnedMesh){bt.setOptional(I,O,"bindMatrix"),bt.setOptional(I,O,"bindMatrixInverse");const en=O.skeleton;en&&(en.boneTexture===null&&en.computeBoneTexture(),bt.setValue(I,"boneTexture",en.boneTexture,R))}O.isBatchedMesh&&(bt.setOptional(I,O,"batchingTexture"),bt.setValue(I,"batchingTexture",O._matricesTexture,R),bt.setOptional(I,O,"batchingIdTexture"),bt.setValue(I,"batchingIdTexture",O._indirectTexture,R),bt.setOptional(I,O,"batchingColorTexture"),O._colorsTexture!==null&&bt.setValue(I,"batchingColorTexture",O._colorsTexture,R));const Cn=N.morphAttributes;if((Cn.position!==void 0||Cn.normal!==void 0||Cn.color!==void 0)&&ie.update(O,N,Pn),(gn||Xe.receiveShadow!==O.receiveShadow)&&(Xe.receiveShadow=O.receiveShadow,bt.setValue(I,"receiveShadow",O.receiveShadow)),z.isMeshGouraudMaterial&&z.envMap!==null&&(An.envMap.value=Ue,An.flipEnvMap.value=Ue.isCubeTexture&&Ue.isRenderTargetTexture===!1?-1:1),z.isMeshStandardMaterial&&z.envMap===null&&B.environment!==null&&(An.envMapIntensity.value=B.environmentIntensity),gn&&(bt.setValue(I,"toneMappingExposure",x.toneMappingExposure),Xe.needsLights&&mn(An,pr),he&&z.fog===!0&&oe.refreshFogUniforms(An,he),oe.refreshMaterialUniforms(An,z,Z,se,m.state.transmissionRenderTarget[E.id]),$a.upload(I,Ui(Xe),An,R)),z.isShaderMaterial&&z.uniformsNeedUpdate===!0&&($a.upload(I,Ui(Xe),An,R),z.uniformsNeedUpdate=!1),z.isSpriteMaterial&&bt.setValue(I,"center",O.center),bt.setValue(I,"modelViewMatrix",O.modelViewMatrix),bt.setValue(I,"normalMatrix",O.normalMatrix),bt.setValue(I,"modelMatrix",O.matrixWorld),z.isShaderMaterial||z.isRawShaderMaterial){const en=z.uniformsGroups;for(let ln=0,No=en.length;ln0&&R.useMultisampledRTT(E)===!1?O=Se.get(E).__webglMultisampledFramebuffer:Array.isArray(je)?O=je[N]:O=je,D.copy(E.viewport),G.copy(E.scissor),F=E.scissorTest}else D.copy(Re).multiplyScalar(Z).floor(),G.copy(Ge).multiplyScalar(Z).floor(),F=it;if(N!==0&&(O=pi),me.bindFramebuffer(I.FRAMEBUFFER,O)&&z&&me.drawBuffers(E,O),me.viewport(D),me.scissor(G),me.setScissorTest(F),he){const Ue=Se.get(E.texture);I.framebufferTexture2D(I.FRAMEBUFFER,I.COLOR_ATTACHMENT0,I.TEXTURE_CUBE_MAP_POSITIVE_X+B,Ue.__webglTexture,N)}else if(Ce){const Ue=Se.get(E.texture),$e=B;I.framebufferTextureLayer(I.FRAMEBUFFER,I.COLOR_ATTACHMENT0,Ue.__webglTexture,N,$e)}else if(E!==null&&N!==0){const Ue=Se.get(E.texture);I.framebufferTexture2D(I.FRAMEBUFFER,I.COLOR_ATTACHMENT0,I.TEXTURE_2D,Ue.__webglTexture,N)}S=-1},this.readRenderTargetPixels=function(E,B,N,z,O,he,Ce){if(!(E&&E.isWebGLRenderTarget)){console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not THREE.WebGLRenderTarget.");return}let Le=Se.get(E).__webglFramebuffer;if(E.isWebGLCubeRenderTarget&&Ce!==void 0&&(Le=Le[Ce]),Le){me.bindFramebuffer(I.FRAMEBUFFER,Le);try{const Ue=E.texture,$e=Ue.format,je=Ue.type;if(!Ae.textureFormatReadable($e)){console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not in RGBA or implementation defined format.");return}if(!Ae.textureTypeReadable(je)){console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not in UnsignedByteType or implementation defined type.");return}B>=0&&B<=E.width-z&&N>=0&&N<=E.height-O&&I.readPixels(B,N,z,O,Pe.convert($e),Pe.convert(je),he)}finally{const Ue=P!==null?Se.get(P).__webglFramebuffer:null;me.bindFramebuffer(I.FRAMEBUFFER,Ue)}}},this.readRenderTargetPixelsAsync=async function(E,B,N,z,O,he,Ce){if(!(E&&E.isWebGLRenderTarget))throw new Error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not THREE.WebGLRenderTarget.");let Le=Se.get(E).__webglFramebuffer;if(E.isWebGLCubeRenderTarget&&Ce!==void 0&&(Le=Le[Ce]),Le){const Ue=E.texture,$e=Ue.format,je=Ue.type;if(!Ae.textureFormatReadable($e))throw new Error("THREE.WebGLRenderer.readRenderTargetPixelsAsync: renderTarget is not in RGBA or implementation defined format.");if(!Ae.textureTypeReadable(je))throw new Error("THREE.WebGLRenderer.readRenderTargetPixelsAsync: renderTarget is not in UnsignedByteType or implementation defined type.");if(B>=0&&B<=E.width-z&&N>=0&&N<=E.height-O){me.bindFramebuffer(I.FRAMEBUFFER,Le);const He=I.createBuffer();I.bindBuffer(I.PIXEL_PACK_BUFFER,He),I.bufferData(I.PIXEL_PACK_BUFFER,he.byteLength,I.STREAM_READ),I.readPixels(B,N,z,O,Pe.convert($e),Pe.convert(je),0);const lt=P!==null?Se.get(P).__webglFramebuffer:null;me.bindFramebuffer(I.FRAMEBUFFER,lt);const gt=I.fenceSync(I.SYNC_GPU_COMMANDS_COMPLETE,0);return I.flush(),await Lp(I,gt,4),I.bindBuffer(I.PIXEL_PACK_BUFFER,He),I.getBufferSubData(I.PIXEL_PACK_BUFFER,0,he),I.deleteBuffer(He),I.deleteSync(gt),he}else throw new Error("THREE.WebGLRenderer.readRenderTargetPixelsAsync: requested read bounds are out of range.")}},this.copyFramebufferToTexture=function(E,B=null,N=0){E.isTexture!==!0&&(Fs("WebGLRenderer: copyFramebufferToTexture function signature has changed."),B=arguments[0]||null,E=arguments[1]);const z=Math.pow(2,-N),O=Math.floor(E.image.width*z),he=Math.floor(E.image.height*z),Ce=B!==null?B.x:0,Le=B!==null?B.y:0;R.setTexture2D(E,0),I.copyTexSubImage2D(I.TEXTURE_2D,N,0,0,Ce,Le,O,he),me.unbindTexture()};const fr=I.createFramebuffer(),ds=I.createFramebuffer();this.copyTextureToTexture=function(E,B,N=null,z=null,O=0,he=null){E.isTexture!==!0&&(Fs("WebGLRenderer: copyTextureToTexture function signature has changed."),z=arguments[0]||null,E=arguments[1],B=arguments[2],he=arguments[3]||0,N=null),he===null&&(O!==0?(Fs("WebGLRenderer: copyTextureToTexture function signature has changed to support src and dst mipmap levels."),he=O,O=0):he=0);let Ce,Le,Ue,$e,je,He,lt,gt,Dt;const Tt=E.isCompressedTexture?E.mipmaps[he]:E.image;if(N!==null)Ce=N.max.x-N.min.x,Le=N.max.y-N.min.y,Ue=N.isBox3?N.max.z-N.min.z:1,$e=N.min.x,je=N.min.y,He=N.isBox3?N.min.z:0;else{const Cn=Math.pow(2,-O);Ce=Math.floor(Tt.width*Cn),Le=Math.floor(Tt.height*Cn),E.isDataArrayTexture?Ue=Tt.depth:E.isData3DTexture?Ue=Math.floor(Tt.depth*Cn):Ue=1,$e=0,je=0,He=0}z!==null?(lt=z.x,gt=z.y,Dt=z.z):(lt=0,gt=0,Dt=0);const dt=Pe.convert(B.format),Xe=Pe.convert(B.type);let Gt;B.isData3DTexture?(R.setTexture3D(B,0),Gt=I.TEXTURE_3D):B.isDataArrayTexture||B.isCompressedArrayTexture?(R.setTexture2DArray(B,0),Gt=I.TEXTURE_2D_ARRAY):(R.setTexture2D(B,0),Gt=I.TEXTURE_2D),I.pixelStorei(I.UNPACK_FLIP_Y_WEBGL,B.flipY),I.pixelStorei(I.UNPACK_PREMULTIPLY_ALPHA_WEBGL,B.premultiplyAlpha),I.pixelStorei(I.UNPACK_ALIGNMENT,B.unpackAlignment);const _t=I.getParameter(I.UNPACK_ROW_LENGTH),Pn=I.getParameter(I.UNPACK_IMAGE_HEIGHT),fs=I.getParameter(I.UNPACK_SKIP_PIXELS),gn=I.getParameter(I.UNPACK_SKIP_ROWS),pr=I.getParameter(I.UNPACK_SKIP_IMAGES);I.pixelStorei(I.UNPACK_ROW_LENGTH,Tt.width),I.pixelStorei(I.UNPACK_IMAGE_HEIGHT,Tt.height),I.pixelStorei(I.UNPACK_SKIP_PIXELS,$e),I.pixelStorei(I.UNPACK_SKIP_ROWS,je),I.pixelStorei(I.UNPACK_SKIP_IMAGES,He);const bt=E.isDataArrayTexture||E.isData3DTexture,An=B.isDataArrayTexture||B.isData3DTexture;if(E.isDepthTexture){const Cn=Se.get(E),en=Se.get(B),ln=Se.get(Cn.__renderTarget),No=Se.get(en.__renderTarget);me.bindFramebuffer(I.READ_FRAMEBUFFER,ln.__webglFramebuffer),me.bindFramebuffer(I.DRAW_FRAMEBUFFER,No.__webglFramebuffer);for(let Ni=0;NiMath.PI&&(n-=hn),i<-Math.PI?i+=hn:i>Math.PI&&(i-=hn),n<=i?this._spherical.theta=Math.max(n,Math.min(i,this._spherical.theta)):this._spherical.theta=this._spherical.theta>(n+i)/2?Math.max(n,this._spherical.theta):Math.min(i,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 s=!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),s=a!=this._spherical.radius}if(Ft.setFromSpherical(this._spherical),Ft.applyQuaternion(this._quatInverse),t.copy(this.target).add(Ft),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=Ft.length();a=this._clampDistance(o*this._scale);const l=o-a;this.object.position.addScaledVector(this._dollyDirection,l),this.object.updateMatrixWorld(),s=!!l}else if(this.object.isOrthographicCamera){const o=new A(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(),s=l!==this.object.zoom;const c=new A(this._mouse.x,this._mouse.y,0);c.unproject(this.object),this.object.position.sub(c).add(o),this.object.updateMatrixWorld(),a=Ft.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):(Va.origin.copy(this.object.position),Va.direction.set(0,0,-1).transformDirection(this.object.matrix),Math.abs(this.object.up.dot(Va.direction))El||8*(1-this._lastQuaternion.dot(this.object.quaternion))>El||this._lastTargetPosition.distanceToSquared(this.target)>El?(this.dispatchEvent(td),this._lastPosition.copy(this.object.position),this._lastQuaternion.copy(this.object.quaternion),this._lastTargetPosition.copy(this.target),!0):!1}_getAutoRotationAngle(e){return e!==null?hn/60*this.autoRotateSpeed*e:hn/60/60*this.autoRotateSpeed}_getZoomScale(e){const t=Math.abs(e*.01);return Math.pow(.95,this.zoomSpeed*t)}_rotateLeft(e){this._sphericalDelta.theta-=e}_rotateUp(e){this._sphericalDelta.phi-=e}_panLeft(e,t){Ft.setFromMatrixColumn(t,0),Ft.multiplyScalar(-e),this._panOffset.add(Ft)}_panUp(e,t){this.screenSpacePanning===!0?Ft.setFromMatrixColumn(t,1):(Ft.setFromMatrixColumn(t,0),Ft.crossVectors(this.object.up,Ft)),Ft.multiplyScalar(e),this._panOffset.add(Ft)}_pan(e,t){const n=this.domElement;if(this.object.isPerspectiveCamera){const i=this.object.position;Ft.copy(i).sub(this.target);let s=Ft.length();s*=Math.tan(this.object.fov/2*Math.PI/180),this._panLeft(2*e*s/n.clientHeight,this.object.matrix),this._panUp(2*t*s/n.clientHeight,this.object.matrix)}else this.object.isOrthographicCamera?(this._panLeft(e*(this.object.right-this.object.left)/this.object.zoom/n.clientWidth,this.object.matrix),this._panUp(t*(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(e){this.object.isPerspectiveCamera||this.object.isOrthographicCamera?this._scale/=e:(console.warn("WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled."),this.enableZoom=!1)}_dollyIn(e){this.object.isPerspectiveCamera||this.object.isOrthographicCamera?this._scale*=e:(console.warn("WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled."),this.enableZoom=!1)}_updateZoomParameters(e,t){if(!this.zoomToCursor)return;this._performCursorZoom=!0;const n=this.domElement.getBoundingClientRect(),i=e-n.left,s=t-n.top,a=n.width,o=n.height;this._mouse.x=i/a*2-1,this._mouse.y=-(s/o)*2+1,this._dollyDirection.set(this._mouse.x,this._mouse.y,1).unproject(this.object).sub(this.object.position).normalize()}_clampDistance(e){return Math.max(this.minDistance,Math.min(this.maxDistance,e))}_handleMouseDownRotate(e){this._rotateStart.set(e.clientX,e.clientY)}_handleMouseDownDolly(e){this._updateZoomParameters(e.clientX,e.clientX),this._dollyStart.set(e.clientX,e.clientY)}_handleMouseDownPan(e){this._panStart.set(e.clientX,e.clientY)}_handleMouseMoveRotate(e){this._rotateEnd.set(e.clientX,e.clientY),this._rotateDelta.subVectors(this._rotateEnd,this._rotateStart).multiplyScalar(this.rotateSpeed);const t=this.domElement;this._rotateLeft(hn*this._rotateDelta.x/t.clientHeight),this._rotateUp(hn*this._rotateDelta.y/t.clientHeight),this._rotateStart.copy(this._rotateEnd),this.update()}_handleMouseMoveDolly(e){this._dollyEnd.set(e.clientX,e.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(e){this._panEnd.set(e.clientX,e.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(e){this._updateZoomParameters(e.clientX,e.clientY),e.deltaY<0?this._dollyIn(this._getZoomScale(e.deltaY)):e.deltaY>0&&this._dollyOut(this._getZoomScale(e.deltaY)),this.update()}_handleKeyDown(e){let t=!1;switch(e.code){case this.keys.UP:e.ctrlKey||e.metaKey||e.shiftKey?this.enableRotate&&this._rotateUp(hn*this.keyRotateSpeed/this.domElement.clientHeight):this.enablePan&&this._pan(0,this.keyPanSpeed),t=!0;break;case this.keys.BOTTOM:e.ctrlKey||e.metaKey||e.shiftKey?this.enableRotate&&this._rotateUp(-hn*this.keyRotateSpeed/this.domElement.clientHeight):this.enablePan&&this._pan(0,-this.keyPanSpeed),t=!0;break;case this.keys.LEFT:e.ctrlKey||e.metaKey||e.shiftKey?this.enableRotate&&this._rotateLeft(hn*this.keyRotateSpeed/this.domElement.clientHeight):this.enablePan&&this._pan(this.keyPanSpeed,0),t=!0;break;case this.keys.RIGHT:e.ctrlKey||e.metaKey||e.shiftKey?this.enableRotate&&this._rotateLeft(-hn*this.keyRotateSpeed/this.domElement.clientHeight):this.enablePan&&this._pan(-this.keyPanSpeed,0),t=!0;break}t&&(e.preventDefault(),this.update())}_handleTouchStartRotate(e){if(this._pointers.length===1)this._rotateStart.set(e.pageX,e.pageY);else{const t=this._getSecondPointerPosition(e),n=.5*(e.pageX+t.x),i=.5*(e.pageY+t.y);this._rotateStart.set(n,i)}}_handleTouchStartPan(e){if(this._pointers.length===1)this._panStart.set(e.pageX,e.pageY);else{const t=this._getSecondPointerPosition(e),n=.5*(e.pageX+t.x),i=.5*(e.pageY+t.y);this._panStart.set(n,i)}}_handleTouchStartDolly(e){const t=this._getSecondPointerPosition(e),n=e.pageX-t.x,i=e.pageY-t.y,s=Math.sqrt(n*n+i*i);this._dollyStart.set(0,s)}_handleTouchStartDollyPan(e){this.enableZoom&&this._handleTouchStartDolly(e),this.enablePan&&this._handleTouchStartPan(e)}_handleTouchStartDollyRotate(e){this.enableZoom&&this._handleTouchStartDolly(e),this.enableRotate&&this._handleTouchStartRotate(e)}_handleTouchMoveRotate(e){if(this._pointers.length==1)this._rotateEnd.set(e.pageX,e.pageY);else{const n=this._getSecondPointerPosition(e),i=.5*(e.pageX+n.x),s=.5*(e.pageY+n.y);this._rotateEnd.set(i,s)}this._rotateDelta.subVectors(this._rotateEnd,this._rotateStart).multiplyScalar(this.rotateSpeed);const t=this.domElement;this._rotateLeft(hn*this._rotateDelta.x/t.clientHeight),this._rotateUp(hn*this._rotateDelta.y/t.clientHeight),this._rotateStart.copy(this._rotateEnd)}_handleTouchMovePan(e){if(this._pointers.length===1)this._panEnd.set(e.pageX,e.pageY);else{const t=this._getSecondPointerPosition(e),n=.5*(e.pageX+t.x),i=.5*(e.pageY+t.y);this._panEnd.set(n,i)}this._panDelta.subVectors(this._panEnd,this._panStart).multiplyScalar(this.panSpeed),this._pan(this._panDelta.x,this._panDelta.y),this._panStart.copy(this._panEnd)}_handleTouchMoveDolly(e){const t=this._getSecondPointerPosition(e),n=e.pageX-t.x,i=e.pageY-t.y,s=Math.sqrt(n*n+i*i);this._dollyEnd.set(0,s),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=(e.pageX+t.x)*.5,o=(e.pageY+t.y)*.5;this._updateZoomParameters(a,o)}_handleTouchMoveDollyPan(e){this.enableZoom&&this._handleTouchMoveDolly(e),this.enablePan&&this._handleTouchMovePan(e)}_handleTouchMoveDollyRotate(e){this.enableZoom&&this._handleTouchMoveDolly(e),this.enableRotate&&this._handleTouchMoveRotate(e)}_addPointer(e){this._pointers.push(e.pointerId)}_removePointer(e){delete this._pointerPositions[e.pointerId];for(let t=0;t - varying vec2 vUv; - uniform sampler2D colorTexture; - uniform vec2 invSize; - uniform vec2 direction; - uniform float gaussianCoefficients[KERNEL_RADIUS]; - - void main() { - float weightSum = gaussianCoefficients[0]; - vec3 diffuseSum = texture2D( colorTexture, vUv ).rgb * weightSum; - for( int i = 1; i < KERNEL_RADIUS; i ++ ) { - float x = float(i); - float w = gaussianCoefficients[i]; - vec2 uvOffset = direction * invSize * x; - vec3 sample1 = texture2D( colorTexture, vUv + uvOffset ).rgb; - vec3 sample2 = texture2D( colorTexture, vUv - uvOffset ).rgb; - diffuseSum += (sample1 + sample2) * w; - weightSum += 2.0 * w; - } - gl_FragColor = vec4(diffuseSum/weightSum, 1.0); - }`})}getCompositeMaterial(e){return new Xt({defines:{NUM_MIPS:e},uniforms:{blurTexture1:{value:null},blurTexture2:{value:null},blurTexture3:{value:null},blurTexture4:{value:null},blurTexture5:{value:null},bloomStrength:{value:1},bloomFactors:{value:null},bloomTintColors:{value:null},bloomRadius:{value:0}},vertexShader:`varying vec2 vUv; - void main() { - vUv = uv; - gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); - }`,fragmentShader:`varying vec2 vUv; - uniform sampler2D blurTexture1; - uniform sampler2D blurTexture2; - uniform sampler2D blurTexture3; - uniform sampler2D blurTexture4; - uniform sampler2D blurTexture5; - uniform float bloomStrength; - uniform float bloomRadius; - uniform float bloomFactors[NUM_MIPS]; - uniform vec3 bloomTintColors[NUM_MIPS]; - - float lerpBloomFactor(const in float factor) { - float mirrorFactor = 1.2 - factor; - return mix(factor, mirrorFactor, bloomRadius); - } - - void main() { - gl_FragColor = bloomStrength * ( lerpBloomFactor(bloomFactors[0]) * vec4(bloomTintColors[0], 1.0) * texture2D(blurTexture1, vUv) + - lerpBloomFactor(bloomFactors[1]) * vec4(bloomTintColors[1], 1.0) * texture2D(blurTexture2, vUv) + - lerpBloomFactor(bloomFactors[2]) * vec4(bloomTintColors[2], 1.0) * texture2D(blurTexture3, vUv) + - lerpBloomFactor(bloomFactors[3]) * vec4(bloomTintColors[3], 1.0) * texture2D(blurTexture4, vUv) + - lerpBloomFactor(bloomFactors[4]) * vec4(bloomTintColors[4], 1.0) * texture2D(blurTexture5, vUv) ); - }`})}}ir.BlurDirectionX=new j(1,0);ir.BlurDirectionY=new j(0,1);function eM(){const e=new Float32Array(6e3),t=new Float32Array(2e3*3);for(let s=0;s<2e3;s++){const a=Math.random()*Math.PI*2,o=Math.acos(2*Math.random()-1),l=600+Math.random()*400;e[s*3]=l*Math.sin(o)*Math.cos(a),e[s*3+1]=l*Math.sin(o)*Math.sin(a),e[s*3+2]=l*Math.cos(o);const c=Math.random();t[s*3]=.55+c*.25,t[s*3+1]=.55+c*.15,t[s*3+2]=.75+c*.25}const n=new qe;n.setAttribute("position",new rt(e,3)),n.setAttribute("color",new rt(t,3));const i=new Ai({size:1.6,sizeAttenuation:!0,vertexColors:!0,transparent:!0,opacity:.6,depthWrite:!1,blending:Wt});return new as(n,i)}function tM(r){const e=new Cd;e.background=new ne(328975),e.fog=new Yr(657946,.0035);const t=new kt(60,r.clientWidth/r.clientHeight,.1,2e3);t.position.set(0,30,80);const n=new Dy({antialias:!0,alpha:!0,powerPreference:"high-performance"});n.setSize(r.clientWidth,r.clientHeight),n.setPixelRatio(Math.min(window.devicePixelRatio,2)),n.toneMapping=cd,n.toneMappingExposure=1.25,r.appendChild(n.domElement);const i=new Uy(t,n.domElement);i.enableDamping=!0,i.dampingFactor=.05,i.rotateSpeed=.5,i.zoomSpeed=.8,i.minDistance=12,i.maxDistance=180,i.autoRotate=!0,i.autoRotateSpeed=.3;const s=new $y(n);s.addPass(new jy(e,t));const a=new ir(new j(r.clientWidth,r.clientHeight),.55,.6,.2);s.addPass(a);const o=new Jd(2763354,.7);e.add(o);const l=new _c(6514417,1.8,240);l.position.set(50,50,50),e.add(l);const c=new _c(11032055,1.2,240);c.position.set(-50,-30,-50),e.add(c);const h=eM();e.add(h);const u=new Wg;u.params.Points={threshold:2};const f=new j;return{scene:e,camera:t,renderer:n,controls:i,composer:s,bloomPass:a,raycaster:u,mouse:f,lights:{ambient:o,point1:l,point2:c},starfield:h}}function nM(r,e){const t=e.clientWidth,n=e.clientHeight;r.camera.aspect=t/n,r.camera.updateProjectionMatrix(),r.renderer.setSize(t,n),r.composer.setSize(t,n)}function iM(r){r.scene.traverse(e=>{var t;(e instanceof yt||e instanceof Pd)&&((t=e.geometry)==null||t.dispose(),Array.isArray(e.material)?e.material.forEach(n=>n.dispose()):e.material&&e.material.dispose())}),r.renderer.dispose(),r.composer.dispose()}class sM{constructor(e){ke(this,"positions");ke(this,"velocities");ke(this,"running",!0);ke(this,"step",0);ke(this,"repulsionStrength",500);ke(this,"attractionStrength",.01);ke(this,"dampening",.9);ke(this,"baseMaxSteps",300);ke(this,"maxSteps",300);ke(this,"cooldownExtension",0);this.positions=e,this.velocities=new Map;for(const t of e.keys())this.velocities.set(t,new A)}addNode(e,t){this.positions.set(e,t.clone()),this.velocities.set(e,new A),this.cooldownExtension=100,this.maxSteps=Math.max(this.maxSteps,this.step+this.cooldownExtension),this.running=!0}removeNode(e){this.positions.delete(e),this.velocities.delete(e)}tick(e){if(!this.running)return;if(this.step>this.maxSteps){this.cooldownExtension>0&&(this.cooldownExtension=0,this.maxSteps=this.baseMaxSteps);return}this.step++;const t=Math.max(.001,1-this.step/this.maxSteps),n=Array.from(this.positions.keys());for(let i=0;i=.7?"active":r>=.4?"dormant":r>=.1?"silent":"unavailable"}const bc={active:"#10b981",dormant:"#f59e0b",silent:"#8b5cf6",unavailable:"#6b7280"},aM={active:"Easily retrievable (retention ≥ 70%)",dormant:"Retrievable with effort (40–70%)",silent:"Difficult, needs cues (10–40%)",unavailable:"Needs reinforcement (< 10%)"},Qa={aha:"#FFD700",confusion:"#EF4444",failure:"#9CA3AF"},oM={aha:"Aha moments and breakthroughs",confusion:"Confusions and weak spots",failure:"Failures and guardrails"};function sd(r,e){return e==="state"?bc[rM(r.retention)]:e==="ahagraph"?lM(r)??Il[r.type]??"#8B95A5":Il[r.type]||"#8B95A5"}function lM(r){const e=new Set((r.tags??[]).map(t=>t.toLowerCase()));return e.has("aha")?Qa.aha:e.has("confusion")||e.has("weak-spot")?Qa.confusion:e.has("failure")||e.has("guardrail")?Qa.failure:null}let Ar=null;function Sc(){if(Ar)return Ar;const r=128,e=document.createElement("canvas");e.width=r,e.height=r;const t=e.getContext("2d");if(!t)return Ar=new Et,Ar;const n=t.createRadialGradient(r/2,r/2,0,r/2,r/2,r/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)"),t.fillStyle=n,t.fillRect(0,0,r,r);const i=new Dd(e);return i.needsUpdate=!0,Ar=i,i}function rd(r){if(r===0||r===1)return r;const e=.3;return Math.pow(2,-10*r)*Math.sin((r-e/4)*(2*Math.PI)/e)+1}function cM(r){return r*r*((1.70158+1)*r-1.70158)}class hM{constructor(){ke(this,"group");ke(this,"meshMap",new Map);ke(this,"glowMap",new Map);ke(this,"positions",new Map);ke(this,"labelSprites",new Map);ke(this,"hoveredNode",null);ke(this,"selectedNode",null);ke(this,"colorMode","type");ke(this,"materializingNodes",[]);ke(this,"dissolvingNodes",[]);ke(this,"growingNodes",[]);this.group=new ns}setColorMode(e){if(this.colorMode!==e){this.colorMode=e;for(const[t,n]of this.meshMap){const i=n.userData.retention??0,s=n.userData.type??"fact",a=Array.isArray(n.userData.tags)?n.userData.tags:[],l=sd({type:s,retention:i,tags:a},e),c=new ne(l),h=n.material;h.color.copy(c),h.emissive.copy(c);const u=this.glowMap.get(t);u&&u.material.color.copy(c)}}}createNodes(e){const t=(1+Math.sqrt(5))/2,n=e.length;for(let i=0;i0,o=new cr(i,16,16),l=new ah({color:new ne(s),emissive:new ne(s),emissiveIntensity:a?0:.3+e.retention*.5,roughness:.3,metalness:.1,transparent:!0,opacity:a?.2:.3+e.retention*.7}),c=new yt(o,l);c.position.copy(t),c.scale.setScalar(n),c.userData={nodeId:e.id,type:e.type,retention:e.retention,tags:e.tags},this.meshMap.set(e.id,c),this.group.add(c);const h=new rs({map:Sc(),color:new ne(s),transparent:!0,opacity:n>0?a?.1:.3+e.retention*.35:0,blending:Wt,depthWrite:!1}),u=new ts(h);u.scale.set(i*6*n,i*6*n,1),u.position.copy(t),u.userData={isGlow:!0,nodeId:e.id},this.glowMap.set(e.id,u),this.group.add(u);const f=e.label||e.type,d=this.createTextSprite(f,"#94a3b8");return d.position.copy(t),d.position.y+=i*2+1.5,d.userData={isLabel:!0,nodeId:e.id,offset:i*2+1.5},this.group.add(d),this.labelSprites.set(e.id,d),{mesh:c,glow:u,label:d,size:i}}addNode(e,t,n={}){const i=(t==null?void 0:t.clone())??new A((Math.random()-.5)*40,(Math.random()-.5)*40,(Math.random()-.5)*40);this.positions.set(e.id,i);const{mesh:s,glow:a,label:o}=this.createNodeMeshes(e,i,0);return s.scale.setScalar(.001),a.scale.set(.001,.001,1),a.material.opacity=0,o.material.opacity=0,n.isBirthRitual?(s.visible=!1,a.visible=!1,o.visible=!1,s.userData.birthRitualPending={totalFrames:30,targetScale:.5+e.retention*2}):this.materializingNodes.push({id:e.id,frame:0,totalFrames:30,mesh:s,glow:a,label:o,targetScale:.5+e.retention*2}),i}igniteNode(e){const t=this.meshMap.get(e),n=this.glowMap.get(e),i=this.labelSprites.get(e);if(!t||!n||!i)return;const s=t.userData.birthRitualPending;s&&(t.visible=!0,n.visible=!0,i.visible=!0,delete t.userData.birthRitualPending,this.materializingNodes.push({id:e,frame:0,totalFrames:s.totalFrames,mesh:t,glow:n,label:i,targetScale:s.targetScale}))}removeNode(e){const t=this.meshMap.get(e),n=this.glowMap.get(e),i=this.labelSprites.get(e);!t||!n||!i||(this.materializingNodes=this.materializingNodes.filter(s=>s.id!==e),this.dissolvingNodes.push({id:e,frame:0,totalFrames:60,mesh:t,glow:n,label:i,originalScale:t.scale.x}))}growNode(e,t){const n=this.meshMap.get(e);if(!n)return;const i=n.scale.x,s=.5+t*2;n.userData.retention=t,this.growingNodes.push({id:e,frame:0,totalFrames:30,startScale:i,targetScale:s})}createTextSprite(e,t){const n=document.createElement("canvas"),i=n.getContext("2d");if(!i){const m=new Et;return new ts(new rs({map:m,transparent:!0,opacity:0}))}n.width=512,n.height=64;const s=e.length>40?e.slice(0,37)+"...":e;i.clearRect(0,0,n.width,n.height),i.font='600 22px -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif';const o=i.measureText(s).width,c=Math.min(o+14*2,n.width-4),h=40,u=(n.width-c)/2,f=(n.height-h)/2,d=h/2;i.fillStyle="rgba(10, 16, 28, 0.82)",i.beginPath(),i.moveTo(u+d,f),i.lineTo(u+c-d,f),i.quadraticCurveTo(u+c,f,u+c,f+d),i.lineTo(u+c,f+h-d),i.quadraticCurveTo(u+c,f+h,u+c-d,f+h),i.lineTo(u+d,f+h),i.quadraticCurveTo(u,f+h,u,f+h-d),i.lineTo(u,f+d),i.quadraticCurveTo(u,f,u+d,f),i.closePath(),i.fill(),i.strokeStyle="rgba(148, 163, 184, 0.18)",i.lineWidth=1,i.stroke(),i.textAlign="center",i.textBaseline="middle",i.fillStyle=t,i.fillText(s,n.width/2,n.height/2+1);const p=new Dd(n);p.needsUpdate=!0;const _=new rs({map:p,transparent:!0,opacity:0,depthTest:!1,sizeAttenuation:!0}),g=new ts(_);return g.scale.set(9,1.2,1),g}updatePositions(){this.group.children.forEach(e=>{if(e.userData.nodeId){const t=this.positions.get(e.userData.nodeId);if(!t)return;e.userData.isGlow?e.position.copy(t):e.userData.isLabel?(e.position.copy(t),e.position.y+=e.userData.offset):e instanceof yt&&e.position.copy(t)}})}animate(e,t,n,i=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),u=rd(h);if(c.mesh.scale.setScalar(Math.max(.001,u)),c.frame>=5){const f=Math.min((c.frame-5)/5,1),d=c.glow.material;d.opacity=f*.4;const p=c.targetScale*6*u;c.glow.scale.set(p,p,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),u=1-cM(h),f=Math.max(.001,c.originalScale*u);c.mesh.scale.setScalar(f);const d=f*6;c.glow.scale.set(d,d,1);const p=c.mesh.material;p.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),u=c.startScale+(c.targetScale-c.startScale)*rd(h),f=this.meshMap.get(c.id);f&&f.scale.setScalar(u);const d=this.glowMap.get(c.id);if(d){const p=u*6;d.scale.set(p,p,1)}c.frame>=c.totalFrames&&this.growingNodes.splice(l,1)}const s=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(s.has(c))return;const h=t.find(y=>y.id===c);if(!h)return;const u=1+Math.sin(e*1.5+t.indexOf(h)*.5)*.15*h.retention;l.scale.setScalar(u);const f=this.positions.get(c),d=f?n.position.distanceTo(f):0,p=1+Math.min(1.4,Math.max(0,(d-60)/100)),_=l.material;if(c===this.hoveredNode)_.emissiveIntensity=1*i;else if(c===this.selectedNode)_.emissiveIntensity=.8*i;else{const v=.3+h.retention*.5+Math.sin(e*(.8+h.retention*.7))*.1*h.retention;_.emissiveIntensity=v*i*p}const g=.3+h.retention*.7;_.opacity=Math.min(1,g*i*p);const m=this.glowMap.get(c);if(m){const y=m.material,v=.3+h.retention*.35;y.opacity=Math.min(.95,v*i*p)}}),this.labelSprites.forEach((l,c)=>{if(s.has(c))return;const h=this.positions.get(c);if(!h)return;const u=n.position.distanceTo(h),f=l.material,d=c===this.hoveredNode||c===this.selectedNode?1:u<40?.9:u<80?.9*(1-(u-40)/40):0;f.opacity+=(d-f.opacity)*.1})}getMeshes(){return Array.from(this.meshMap.values())}dispose(){this.group.traverse(e=>{var t,n,i,s,a;e instanceof yt?((t=e.geometry)==null||t.dispose(),(n=e.material)==null||n.dispose()):e instanceof ts&&((s=(i=e.material)==null?void 0:i.map)==null||s.dispose(),(a=e.material)==null||a.dispose())}),this.materializingNodes=[],this.dissolvingNodes=[],this.growingNodes=[]}}function uM(r){return 1-Math.pow(1-r,3)}class dM{constructor(){ke(this,"group");ke(this,"growingEdges",[]);ke(this,"dissolvingEdges",[]);this.group=new ns}createEdges(e,t){for(const n of e){const i=t.get(n.source),s=t.get(n.target);if(!i||!s)continue;const a=[i,s],o=new qe().setFromPoints(a),l=new qt({color:9133302,transparent:!0,opacity:Math.min(.25+n.weight*.5,.8),blending:Wt,depthWrite:!1}),c=new Vn(o,l);c.userData={source:n.source,target:n.target},this.group.add(c)}}addEdge(e,t){const n=t.get(e.source),i=t.get(e.target);if(!n||!i)return;const s=[n.clone(),n.clone()],a=new qe().setFromPoints(s),o=new qt({color:9133302,transparent:!0,opacity:0,blending:Wt,depthWrite:!1}),l=new Vn(a,o);l.userData={source:e.source,target:e.target},this.group.add(l),this.growingEdges.push({line:l,source:e.source,target:e.target,frame:0,totalFrames:45})}removeEdgesForNode(e){const t=[];this.group.children.forEach(n=>{const i=n;(i.userData.source===e||i.userData.target===e)&&t.push(i)});for(const n of t)this.growingEdges=this.growingEdges.filter(i=>i.line!==n),this.dissolvingEdges.push({line:n,frame:0,totalFrames:40})}animateEdges(e){for(let t=this.growingEdges.length-1;t>=0;t--){const n=this.growingEdges[t];n.frame++;const i=uM(Math.min(n.frame/n.totalFrames,1)),s=e.get(n.source),a=e.get(n.target);if(!s||!a)continue;const o=s.clone().lerp(a,i),l=n.line.geometry.attributes.position;l.setXYZ(0,s.x,s.y,s.z),l.setXYZ(1,o.x,o.y,o.z),l.needsUpdate=!0;const c=n.line.material;c.opacity=i*.65,n.frame>=n.totalFrames&&(c.opacity=.65,this.growingEdges.splice(t,1))}for(let t=this.dissolvingEdges.length-1;t>=0;t--){const n=this.dissolvingEdges[t];n.frame++;const i=n.frame/n.totalFrames,s=n.line.material;s.opacity=Math.max(0,.65*(1-i)),n.frame>=n.totalFrames&&(this.group.remove(n.line),n.line.geometry.dispose(),n.line.material.dispose(),this.dissolvingEdges.splice(t,1))}}updatePositions(e){this.group.children.forEach(t=>{const n=t;if(this.growingEdges.some(a=>a.line===n)||this.dissolvingEdges.some(a=>a.line===n))return;const i=e.get(n.userData.source),s=e.get(n.userData.target);if(i&&s){const a=n.geometry.attributes.position;a.setXYZ(0,i.x,i.y,i.z),a.setXYZ(1,s.x,s.y,s.z),a.needsUpdate=!0}})}dispose(){this.group.children.forEach(e=>{var n,i;const t=e;(n=t.geometry)==null||n.dispose(),(i=t.material)==null||i.dispose()}),this.growingEdges=[],this.dissolvingEdges=[]}}class fM{constructor(e){ke(this,"starField");ke(this,"neuralParticles");this.starField=this.createStarField(),this.neuralParticles=this.createNeuralParticles(),e.add(this.starField),e.add(this.neuralParticles)}createStarField(){const t=new qe,n=new Float32Array(3e3*3),i=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,i[a]=Math.random()*1.5;t.setAttribute("position",new rt(n,3)),t.setAttribute("size",new rt(i,1));const s=new Ai({color:6514417,size:.5,transparent:!0,opacity:.4,sizeAttenuation:!0,blending:Wt});return new as(t,s)}createNeuralParticles(){const t=new qe,n=new Float32Array(500*3),i=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,i[a*3]=.4+Math.random()*.3,i[a*3+1]=.3+Math.random()*.2,i[a*3+2]=.8+Math.random()*.2;t.setAttribute("position",new rt(n,3)),t.setAttribute("color",new rt(i,3));const s=new Ai({size:.3,vertexColors:!0,transparent:!0,opacity:.4,blending:Wt,sizeAttenuation:!0});return new as(t,s)}animate(e){this.starField.rotation.y+=1e-4,this.starField.rotation.x+=5e-5;const t=this.neuralParticles.geometry.attributes.position;for(let n=0;n=0;i--){const s=this.pulseEffects[i];if(s.intensity-=s.decay,s.intensity<=0){this.pulseEffects.splice(i,1);continue}const a=e.get(s.nodeId);if(a){const o=a.material;o.emissive.lerp(s.color,s.intensity*.3),o.emissiveIntensity=Math.max(o.emissiveIntensity,s.intensity)}}for(let i=this.spawnBursts.length-1;i>=0;i--){const s=this.spawnBursts[i];if(s.age++,s.age>120){this.scene.remove(s.particles),s.particles.geometry.dispose(),s.particles.material.dispose(),this.spawnBursts.splice(i,1);continue}const a=s.particles.geometry.attributes.position,o=s.particles.geometry.attributes.velocity;for(let c=0;c=0;i--){const s=this.rainbowBursts[i];if(s.age++,s.age>s.maxAge){this.scene.remove(s.particles),s.particles.geometry.dispose(),s.particles.material.dispose(),this.rainbowBursts.splice(i,1);continue}const a=s.particles.geometry.attributes.position,o=s.particles.geometry.attributes.velocity;for(let f=0;f=0;i--){const s=this.rippleWaves[i];if(s.age++,s.radius+=s.speed,s.age>s.maxAge){this.rippleWaves.splice(i,1);continue}const a=s.radius,o=3;n.forEach((l,c)=>{if(s.pulsedNodes.has(c))return;const h=l.distanceTo(s.origin);h>=a-o&&h<=a+o&&(s.pulsedNodes.add(c),this.addPulse(c,.8,new ne(65489),.03))})}for(let i=this.implosions.length-1;i>=0;i--){const s=this.implosions[i];if(s.age++,s.age>s.maxAge+20){this.scene.remove(s.particles),s.particles.geometry.dispose(),s.particles.material.dispose(),s.flash&&(this.scene.remove(s.flash),s.flash.geometry.dispose(),s.flash.material.dispose()),this.implosions.splice(i,1);continue}if(s.age<=s.maxAge){const a=s.particles.geometry.attributes.position,o=s.particles.geometry.attributes.velocity,l=1+s.age*.02;for(let h=0;hs.maxAge){const a=(s.age-s.maxAge)/20;s.flash.material.opacity=Math.max(0,1-a),s.flash.scale.setScalar(1+a*3)}}for(let i=this.shockwaves.length-1;i>=0;i--){const s=this.shockwaves[i];if(s.age++,s.age>s.maxAge){this.scene.remove(s.mesh),s.mesh.geometry.dispose(),s.mesh.material.dispose(),this.shockwaves.splice(i,1);continue}const a=s.age/s.maxAge;s.mesh.scale.setScalar(1+a*20),s.mesh.material.opacity=.8*(1-a),s.mesh.lookAt(t.position)}for(let i=this.connectionFlashes.length-1;i>=0;i--){const s=this.connectionFlashes[i];if(s.intensity-=.015,s.intensity<=0){this.scene.remove(s.line),s.line.geometry.dispose(),s.line.material.dispose(),this.connectionFlashes.splice(i,1);continue}s.line.material.opacity=s.intensity}for(let i=this.birthOrbs.length-1;i>=0;i--){const s=this.birthOrbs[i];s.age++;const a=s.gestationFrames+s.flightFrames,o=s.sprite.material,l=s.core.material,c=s.getTargetPos();if(c)s.lastTargetPos.copy(c);else if(s.age>s.gestationFrames&&!s.aborted){s.aborted=!0;const h=s.sprite.position;o.color.setRGB(1,.15,.2),l.color.setRGB(1,.6,.6),this.createImplosion(h,new ne(16721203)),s.arriveFired=!0,s.age=a+1}if(s.age<=s.gestationFrames){const h=s.age/s.gestationFrames,u=1-Math.pow(1-h,3),f=.85+Math.sin(s.age*.35)*.15,d=.5+u*4.5*f,p=.2+u*1.8*f;s.sprite.scale.set(d,d,1),s.core.scale.set(p,p,1),o.opacity=u*.95,l.opacity=u,o.color.copy(s.color).multiplyScalar(.7+u*.3),s.sprite.position.copy(s.startPos),s.core.position.copy(s.startPos)}else if(s.age<=a){const h=(s.age-s.gestationFrames)/s.flightFrames,u=h<.5?2*h*h:1-Math.pow(-2*h+2,2)/2,f=s.startPos,d=s.lastTargetPos,p=d.x-f.x,_=d.y-f.y,g=d.z-f.z,m=Math.sqrt(p*p+_*_+g*g),y=(f.x+d.x)*.5,v=(f.y+d.y)*.5+30+m*.15,x=(f.z+d.z)*.5,w=1-u,T=w*w,C=2*w*u,P=u*u,S=T*f.x+C*y+P*d.x,M=T*f.y+C*v+P*d.y,D=T*f.z+C*x+P*d.z;s.sprite.position.set(S,M,D),s.core.position.set(S,M,D);const G=1-u*.35;s.sprite.scale.setScalar(5*G),s.core.scale.setScalar(2*G),o.opacity=.95,l.opacity=1,o.color.copy(s.color)}else if(s.arriveFired){const h=s.age-a,u=Math.max(0,1-h/8);o.opacity=.95*u,l.opacity=1*u,s.sprite.scale.setScalar(5*(1+(1-u)*2)),u<=0&&(this.scene.remove(s.sprite),this.scene.remove(s.core),o.dispose(),l.dispose(),this.birthOrbs.splice(i,1))}else{s.arriveFired=!0;try{s.onArrive()}catch(h){console.warn("[birth-orb] onArrive threw",h)}}}}dispose(){for(const e of this.spawnBursts)this.scene.remove(e.particles),e.particles.geometry.dispose(),e.particles.material.dispose();for(const e of this.rainbowBursts)this.scene.remove(e.particles),e.particles.geometry.dispose(),e.particles.material.dispose();for(const e of this.implosions)this.scene.remove(e.particles),e.particles.geometry.dispose(),e.particles.material.dispose(),e.flash&&(this.scene.remove(e.flash),e.flash.geometry.dispose(),e.flash.material.dispose());for(const e of this.shockwaves)this.scene.remove(e.mesh),e.mesh.geometry.dispose(),e.mesh.material.dispose();for(const e of this.connectionFlashes)this.scene.remove(e.line),e.line.geometry.dispose(),e.line.material.dispose();for(const e of this.birthOrbs)this.scene.remove(e.sprite),this.scene.remove(e.core),e.sprite.material.dispose(),e.core.material.dispose();this.pulseEffects=[],this.spawnBursts=[],this.rainbowBursts=[],this.rippleWaves=[],this.implosions=[],this.shockwaves=[],this.connectionFlashes=[],this.birthOrbs=[]}}const jn={bloomStrength:.8,rotateSpeed:.3,fogColor:328976,fogDensity:.008,nebulaIntensity:0,chromaticIntensity:.002,vignetteRadius:.9,breatheAmplitude:1},bi={bloomStrength:1.8,rotateSpeed:.08,fogColor:656672,fogDensity:.006,nebulaIntensity:1,chromaticIntensity:.005,vignetteRadius:.7,breatheAmplitude:2};class mM{constructor(){ke(this,"active",!1);ke(this,"transition",0);ke(this,"transitionSpeed",.008);ke(this,"current");ke(this,"auroraHue",0);this.current={...jn}}setActive(e){this.active=e}update(e,t,n,i,s){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(jn.bloomStrength,bi.bloomStrength,o),this.current.rotateSpeed=this.lerp(jn.rotateSpeed,bi.rotateSpeed,o),this.current.fogDensity=this.lerp(jn.fogDensity,bi.fogDensity,o),this.current.nebulaIntensity=this.lerp(jn.nebulaIntensity,bi.nebulaIntensity,o),this.current.chromaticIntensity=this.lerp(jn.chromaticIntensity,bi.chromaticIntensity,o),this.current.vignetteRadius=this.lerp(jn.vignetteRadius,bi.vignetteRadius,o),this.current.breatheAmplitude=this.lerp(jn.breatheAmplitude,bi.breatheAmplitude,o),t.strength=this.current.bloomStrength,n.autoRotateSpeed=this.current.rotateSpeed;const l=new ne(jn.fogColor),c=new ne(bi.fogColor),h=l.clone().lerp(c,o);if(e.fog=new Yr(h,this.current.fogDensity),o>.01){this.auroraHue=s*.1%1;const u=new ne().setHSL(.75+this.auroraHue*.15,.8,.5),f=new ne().setHSL(.55+this.auroraHue*.2,.7,.4);i.point1.color.lerp(u,o*.3),i.point2.color.lerp(f,o*.3)}else i.point1.color.set(6514417),i.point2.color.set(11032055)}lerp(e,t,n){return e+(t-e)*n}}const gM=50,Fr=[];function _M(r,e,t){const n=r.tags??[],i=r.type??"";let s=null,a=0;for(const o of e){let l=0;o.type===i&&(l+=2);for(const c of o.tags)n.includes(c)&&(l+=1);l>a&&(a=l,s=o.id)}if(s&&a>0){const o=t.get(s);if(o)return new A(o.x+(Math.random()-.5)*10,o.y+(Math.random()-.5)*10,o.z+(Math.random()-.5)*10)}return new A((Math.random()-.5)*40,(Math.random()-.5)*40,(Math.random()-.5)*40)}function vM(r,e){if(Fr.length<=gM)return;const t=Fr.shift();r.edgeManager.removeEdgesForNode(t),r.nodeManager.removeNode(t),r.forceSim.removeNode(t),r.onMutation({type:"edgesRemoved",nodeId:t}),r.onMutation({type:"nodeRemoved",nodeId:t});const n=e.findIndex(i=>i.id===t);n!==-1&&e.splice(n,1)}function xM(r,e,t){var u,f;const{effects:n,nodeManager:i,edgeManager:s,forceSim:a,camera:o,onMutation:l}=e,c=i.positions,h=i.meshMap;switch(r.type){case"MemoryCreated":{const d=r.data;if(!d.id)break;const p={id:d.id,label:(d.content??"").slice(0,60),type:d.node_type??"fact",retention:Math.max(0,Math.min(1,d.retention??.9)),tags:d.tags??[],createdAt:new Date().toISOString(),updatedAt:new Date().toISOString(),isCenter:!1},_=_M(p,t,c),g=i.addNode(p,_,{isBirthRitual:!0});a.addNode(d.id,g),Fr.push(d.id),vM(e,t);const m=new ne(Il[p.type]||"#00ffd1"),y=m.clone();y.offsetHSL(.15,0,0),n.createBirthOrb(o,m,()=>i.positions.get(p.id),()=>{i.igniteNode(p.id);const v=i.positions.get(p.id)??_,x=i.meshMap.get(p.id);x&&x.scale.multiplyScalar(1.8),n.createRainbowBurst(v,m),n.createShockwave(v,m,o),n.createShockwave(v,y,o),n.createRippleWave(v)}),l({type:"nodeAdded",node:p});break}case"ConnectionDiscovered":{const d=r.data;if(!d.source_id||!d.target_id)break;const p=c.get(d.source_id),_=c.get(d.target_id),g={source:d.source_id,target:d.target_id,weight:d.weight??.5,type:d.connection_type??"semantic"};s.addEdge(g,c),p&&_&&n.createConnectionFlash(p,_,new ne(54527)),d.source_id&&h.has(d.source_id)&&n.addPulse(d.source_id,1,new ne(54527),.02),d.target_id&&h.has(d.target_id)&&n.addPulse(d.target_id,1,new ne(54527),.02),l({type:"edgeAdded",edge:g});break}case"MemoryDeleted":{const d=r.data;if(!d.id)break;const p=c.get(d.id);if(p){const g=new ne(16729943);n.createImplosion(p,g)}s.removeEdgesForNode(d.id),i.removeNode(d.id),a.removeNode(d.id);const _=Fr.indexOf(d.id);_!==-1&&Fr.splice(_,1),l({type:"edgesRemoved",nodeId:d.id}),l({type:"nodeRemoved",nodeId:d.id});break}case"MemoryPromoted":{const d=r.data,p=d==null?void 0:d.id;if(!p)break;const _=d.new_retention??.95;if(h.has(p)){i.growNode(p,_),n.addPulse(p,1.2,new ne(65416),.01);const g=c.get(p);g&&(n.createShockwave(g,new ne(65416),o),n.createSpawnBurst(g,new ne(65416))),l({type:"nodeUpdated",nodeId:p,retention:_})}break}case"MemoryDemoted":{const d=r.data,p=d==null?void 0:d.id;if(!p)break;const _=d.new_retention??.3;h.has(p)&&(i.growNode(p,_),n.addPulse(p,.8,new ne(16729943),.03),l({type:"nodeUpdated",nodeId:p,retention:_}));break}case"MemoryUpdated":{const d=r.data,p=d==null?void 0:d.id;if(!p||!h.has(p))break;n.addPulse(p,.6,new ne(8490232),.02),d.retention!==void 0&&(i.growNode(p,d.retention),l({type:"nodeUpdated",nodeId:p,retention:d.retention}));break}case"SearchPerformed":{h.forEach((d,p)=>{n.addPulse(p,.6+Math.random()*.4,new ne(8490232),.02)});break}case"DreamStarted":{h.forEach((d,p)=>{n.addPulse(p,1,new ne(11032055),.005)});break}case"DreamProgress":{const d=(u=r.data)==null?void 0:u.memory_id;d&&h.has(d)&&n.addPulse(d,1.5,new ne(12616956),.01);break}case"DreamCompleted":{n.createSpawnBurst(new A(0,0,0),new ne(11032055)),n.createShockwave(new A(0,0,0),new ne(11032055),o);break}case"RetentionDecayed":{const d=(f=r.data)==null?void 0:f.id;d&&h.has(d)&&n.addPulse(d,.8,new ne(16729943),.03);break}case"ConsolidationCompleted":{h.forEach((d,p)=>{n.addPulse(p,.4+Math.random()*.3,new ne(16758784),.015)});break}case"ActivationSpread":{const d=r.data;if(d.source_id&&d.target_ids){const p=c.get(d.source_id);if(p)for(const _ of d.target_ids){const g=c.get(_);g&&n.createConnectionFlash(p,g,new ne(1370310))}}break}case"MemorySuppressed":{const d=r.data;if(!d.id)break;const p=c.get(d.id);if(p){n.createImplosion(p,new ne(11032055));const _=Math.max(1,d.suppression_count??1),g=Math.min(.4+_*.15,1);n.addPulse(d.id,g,new ne(11032055),.04)}break}case"MemoryUnsuppressed":{const d=r.data;if(!d.id)break;const p=c.get(d.id);p&&h.has(d.id)&&(n.createRainbowBurst(p,new ne(65416)),n.addPulse(d.id,1,new ne(65416),.02));break}case"Rac1CascadeSwept":{const p=r.data.neighbors_affected??0;if(p===0)break;const _=Array.from(h.keys()),g=Math.min(p,_.length,12);for(let m=0;m"u")return eo;const r=localStorage.getItem(hf);if(r===null)return eo;const e=Number(r);return Number.isFinite(e)?Math.min(Ec,Math.max(wc,e)):eo}const ji=IM();function IM(){let r=Qe(!1),e=Qe(Al(new Date)),t=Qe(!1),n=Qe(1),i=Qe(!1),s=Qe(Al(RM()));return{get temporalEnabled(){return L(r)},set temporalEnabled(a){le(r,a,!0)},get temporalDate(){return L(e)},set temporalDate(a){le(e,a,!0)},get temporalPlaying(){return L(t)},set temporalPlaying(a){le(t,a,!0)},get temporalSpeed(){return L(n)},set temporalSpeed(a){le(n,a,!0)},get dreamMode(){return L(i)},set dreamMode(a){le(i,a,!0)},get brightness(){return L(s)},set brightness(a){const o=Math.min(Ec,Math.max(wc,a));if(le(s,o,!0),typeof localStorage<"u")try{localStorage.setItem(hf,String(o))}catch{}},brightnessMin:wc,brightnessMax:Ec,brightnessDefault:eo}}var PM=ut('
');function DM(r,e){var G;sr(e,!0);let t=Dr(e,"events",19,()=>[]),n=Dr(e,"isDreaming",3,!1),i=Dr(e,"colorMode",3,"type");Ga(()=>{u==null||u.setColorMode(i())});let s,a,o,l=typeof window<"u"&&((G=window.matchMedia)==null?void 0:G.call(window,"(prefers-reduced-motion: reduce)").matches),c=null;function h(F){l=F.matches,a!=null&&a.controls&&(a.controls.autoRotate=!l)}let u,f,d,p,_,g,m,y,v=null,x=[];od(()=>{var J;a=tM(s),l&&(a.controls.autoRotate=!1),typeof window<"u"&&window.matchMedia&&(c=window.matchMedia("(prefers-reduced-motion: reduce)"),(J=c.addEventListener)==null||J.call(c,"change",h)),m=bM(a.scene).material,y=AM(a.composer),d=new fM(a.scene),u=new hM,u.colorMode=i(),f=new dM,p=new pM(a.scene),g=new mM;const V=u.createNodes(e.nodes);f.createEdges(e.edges,V),_=new sM(V),x=[...e.nodes],a.scene.add(f.group),a.scene.add(u.group),T(),window.addEventListener("resize",P),s.addEventListener("pointermove",S),s.addEventListener("click",M)}),Ac(()=>{var F;cancelAnimationFrame(o),window.removeEventListener("resize",P),(F=c==null?void 0:c.removeEventListener)==null||F.call(c,"change",h),s==null||s.removeEventListener("pointermove",S),s==null||s.removeEventListener("click",M),p==null||p.dispose(),d==null||d.dispose(),u==null||u.dispose(),f==null||f.dispose(),a&&iM(a)});let w=0;function T(){o=requestAnimationFrame(T);const F=performance.now();w===0&&(w=F);const V=F-w;if(V<16)return;w=F-V%16;const J=F*.001;_.tick(e.edges),u.updatePositions(),f.updatePositions(u.positions),f.animateEdges(u.positions),d.animate(J),u.animate(J,x,a.camera,ji.brightness),g.setActive(n()),g.update(a.scene,a.bloomPass,a.controls,a.lights,J),SM(m,J,g.current.nebulaIntensity,s.clientWidth,s.clientHeight),CM(y,J,g.current.nebulaIntensity),C(),p.update(u.meshMap,a.camera,u.positions),a.controls.update(),a.composer.render()}function C(){if(!t()||t().length===0)return;const F=[];for(const J of t()){if(J===v)break;F.push(J)}if(F.length===0)return;if(F.length===t().length&&t().length>=200){console.warn("[vestige] Event horizon overflow: dropping visuals for",F.length,"events"),v=t()[0];return}v=t()[0];const V={effects:p,nodeManager:u,edgeManager:f,forceSim:_,camera:a.camera,onMutation:J=>{var q;J.type==="nodeAdded"?x=[...x,J.node]:J.type==="nodeRemoved"&&(x=x.filter(se=>se.id!==J.nodeId)),(q=e.onGraphMutation)==null||q.call(e,J)}};for(let J=F.length-1;J>=0;J--)xM(F[J],V,x)}function P(){!s||!a||nM(a,s)}function S(F){const V=s.getBoundingClientRect();a.mouse.x=(F.clientX-V.left)/V.width*2-1,a.mouse.y=-((F.clientY-V.top)/V.height)*2+1,a.raycaster.setFromCamera(a.mouse,a.camera);const J=a.raycaster.intersectObjects(u.getMeshes());J.length>0?(u.hoveredNode=J[0].object.userData.nodeId,s.style.cursor="pointer"):(u.hoveredNode=null,s.style.cursor="grab")}function M(){var F;if(u.hoveredNode){u.selectedNode=u.hoveredNode,(F=e.onSelect)==null||F.call(e,u.hoveredNode);const V=u.positions.get(u.hoveredNode);V&&a.controls.target.lerp(V.clone(),.5)}}var D=PM();Rl(D,F=>s=F,()=>s),ot(r,D),rr()}var LM=ut('
'),UM=ut('
');function NM(r,e){sr(e,!0);let t=Dr(e,"width",3,240),n=Dr(e,"height",3,80);function i(g){return e.stability<=0?0:Math.exp(-g/e.stability)}let s=ei(()=>{const g=[],m=Math.max(e.stability*3,30),y=4,v=t()-y*2,x=n()-y*2;for(let w=0;w<=50;w++){const T=w/50*m,C=i(T),P=y+w/50*v,S=y+(1-C)*x;g.push(`${w===0?"M":"L"}${P.toFixed(1)},${S.toFixed(1)}`)}return g.join(" ")}),a=ei(()=>[{label:"Now",days:0,value:e.retention},{label:"1d",days:1,value:i(1)},{label:"7d",days:7,value:i(7)},{label:"30d",days:30,value:i(30)}]);function o(g){return g>.7?"#10b981":g>.4?"#f59e0b":"#ef4444"}var l=UM(),c=xe(l),h=xe(c),u=Ee(h),f=Ee(u),d=Ee(f),p=Ee(d);Or(),fe(c);var _=Ee(c,2);Pr(_,21,()=>L(a),Wa,(g,m)=>{var y=LM(),v=xe(y),x=xe(v);fe(v);var w=Ee(v,2),T=xe(w);fe(w),fe(y),Pt((C,P)=>{ct(x,`${L(m).label??""}:`),zr(w,`color: ${C??""}`),ct(T,`${P??""}%`)},[()=>o(L(m).value),()=>(L(m).value*100).toFixed(0)]),ot(g,y)}),fe(_),fe(l),Pt(g=>{Rt(c,"width",t()),Rt(c,"height",n()),Rt(c,"viewBox",`0 0 ${t()??""} ${n()??""}`),Rt(h,"y1",4+(n()-8)*.5),Rt(h,"x2",t()-4),Rt(h,"y2",4+(n()-8)*.5),Rt(u,"y1",4+(n()-8)*.8),Rt(u,"x2",t()-4),Rt(u,"y2",4+(n()-8)*.8),Rt(f,"d",L(s)),Rt(d,"d",`${L(s)??""} L${t()-4},${n()-4} L4,${n()-4} Z`),Rt(p,"cy",4+(1-e.retention)*(n()-8)),Rt(p,"fill",g)},[()=>o(e.retention)]),ot(r,l),rr()}function ad(r,e,t){const n=t.getTime(),i=new Set,s=new Map,a=r.filter(l=>{const c=new Date(l.createdAt).getTime();if(c<=n){i.add(l.id);const h=n-c,u=1440*60*1e3,f=hi.has(l.source)&&i.has(l.target));return{visibleNodes:a,visibleEdges:o,nodeOpacities:s}}function FM(r){if(r.length===0){const n=new Date;return{oldest:n,newest:n}}let e=1/0,t=-1/0;for(const n of r){const i=new Date(n.createdAt).getTime();it&&(t=i)}return{oldest:new Date(e),newest:new Date(t)}}var OM=ut(`
`),BM=ut('');function zM(r,e){sr(e,!0);let t=Qe(!1),n=Qe(!1),i=Qe(1),s=Qe(100),a,o=0,l=ei(()=>FM(e.nodes)),c=ei(()=>{const v=L(l).oldest.getTime(),w=L(l).newest.getTime()-v||1;return new Date(v+L(s)/100*w)});function h(v){return v.toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"})}function u(){le(t,!L(t)),e.onToggle(L(t)),L(t)&&(le(s,100),e.onDateChange(L(c)))}function f(){le(n,!L(n)),L(n)?(le(s,0),o=performance.now(),d()):cancelAnimationFrame(a)}function d(){L(n)&&(a=requestAnimationFrame(v=>{const x=(v-o)/1e3;o=v;const w=L(l).oldest.getTime(),C=(L(l).newest.getTime()-w)/(1440*60*1e3)||1,P=L(i)/C*100;if(le(s,Math.min(100,L(s)+P*x),!0),e.onDateChange(L(c)),L(s)>=100){le(n,!1);return}d()}))}function p(){e.onDateChange(L(c))}Ac(()=>{le(n,!1),cancelAnimationFrame(a)});var _=mf(),g=no(_);{var m=v=>{var x=OM(),w=xe(x),T=xe(w),C=xe(T),P=xe(C),S=xe(P,!0);fe(P);var M=Ee(P,2),D=xe(M);D.value=D.__value=1;var G=Ee(D);G.value=G.__value=7;var F=Ee(G);F.value=F.__value=30,fe(M),fe(C);var V=Ee(C,2),J=xe(V,!0);fe(V);var q=Ee(V,2);fe(T);var se=Ee(T,2);Br(se);var Z=Ee(se,2),_e=xe(Z),be=xe(_e,!0);fe(_e);var Re=Ee(_e,2),Ge=xe(Re,!0);fe(Re),fe(Z),fe(w),fe(x),Pt((it,Q,ue)=>{ct(S,L(n)?"⏸":"▶"),ct(J,it),ct(be,Q),ct(Ge,ue)},[()=>h(L(c)),()=>h(L(l).oldest),()=>h(L(l).newest)]),Ut("click",P,f),Mf(M,()=>L(i),it=>le(i,it)),Ut("click",q,u),Ut("input",se,p),Cl(se,()=>L(s),it=>le(s,it)),ot(v,x)},y=v=>{var x=BM();Ut("click",x,u),ot(v,x)};It(g,v=>{L(t)?v(m):v(y,!1)})}ot(r,_),rr()}Cc(["click","input"]);var kM=ut('
'),VM=ut('
FSRS accessibility
');function HM(r,e){sr(e,!1);const t=["active","dormant","silent","unavailable"];bf();var n=VM(),i=Ee(xe(n),2);Pr(i,1,()=>t,s=>s,(s,a)=>{var o=kM(),l=xe(o),c=Ee(l,2),h=xe(c,!0);fe(c);var u=Ee(c,2),f=xe(u,!0);fe(u),fe(o),Pt(d=>{zr(l,`background: ${bc[L(a)]??""}; box-shadow: 0 0 6px ${bc[L(a)]??""}55;`),ct(h,L(a)),ct(f,d)},[()=>{var d;return((d=aM[L(a)].match(/\(([^)]+)\)/))==null?void 0:d[1])??""}]),ot(s,o)}),fe(n),ot(r,n),rr()}function uf(r){var t,n;const e={};for(const i of r)(e[t=i.source]??(e[t]=[])).push({edge:i,otherId:i.target}),(e[n=i.target]??(e[n]=[])).push({edge:i,otherId:i.source});for(const i of Object.keys(e))e[i].sort((s,a)=>(a.edge.weight??0)-(s.edge.weight??0));return e}function to(r){const e=(r.type??"").toLowerCase();return e.includes("contradict")||e.includes("conflict")||e.includes("supersede")}function Ws(r){const e=Date.parse(r.updatedAt||r.createdAt||"");return Number.isFinite(e)?e:0}function GM(r,e,t,n=7){var g;const i=new Map(r.map(m=>[m.id,m])),s={beats:[],centerId:t,pivoted:!1,flowEdges:[]};if(r.length===0)return s;const a=uf(e),o=i.has(t);let l=o?t:"";l||(l=((g=r.find(m=>m.isCenter))==null?void 0:g.id)??""),l||(l=r.map(m=>{var y;return{id:m.id,deg:((y=a[m.id])==null?void 0:y.length)??0}}).sort((m,y)=>y.deg-m.deg)[0].id);const c=i.get(l);if(!c)return s;const h=!o,u=new Set([l]),f=[{nodeId:l,node:c,viaEdge:null,kind:"origin",intensity:1}],d=[];let p=l,_=!1;for(;f.length!u.has(x.otherId)&&to(x.edge)),y&&(_=!0)),y||(y=m.find(x=>!u.has(x.otherId))),!y){const x=r.filter(T=>!u.has(T.id)).sort((T,C)=>Ws(C)-Ws(T));if(x.length===0)break;const w=x[0];u.add(w.id),f.push({nodeId:w.id,node:w,viaEdge:null,kind:"bridge",intensity:.6}),p=w.id;continue}const v=i.get(y.otherId);if(!v){u.add(y.otherId);continue}u.add(v.id),d.push(y.edge),f.push({nodeId:v.id,node:v,viaEdge:y.edge,kind:to(y.edge)?"contradiction":"connection",intensity:to(y.edge)?1:Math.min(1,.55+(y.edge.weight??0)*.45)}),p=v.id}if(f.lengthv.id!==m).sort((v,x)=>Ws(x)-Ws(v))[0];y&&!f.some(v=>v.nodeId===y.id)&&f.push({nodeId:y.id,node:y,viaEdge:null,kind:"recent",intensity:.8})}return{beats:f,centerId:l,pivoted:h,flowEdges:d}}const jt=new A,WM=new A(0,1,0),XM=new A(0,0,0);class qM{constructor(e,t,n,i,s={},a={}){ke(this,"camera");ke(this,"target");ke(this,"positions");ke(this,"path");ke(this,"cb");ke(this,"opts");ke(this,"phase","idle");ke(this,"beatIndex",0);ke(this,"phaseElapsed",0);ke(this,"fromPos",new A);ke(this,"toPos",new A);ke(this,"fromTarget",new A);ke(this,"toTarget",new A);this.camera=e,this.target=t,this.positions=n,this.path=i,this.cb=s,this.opts={flightSeconds:a.flightSeconds??2.4,dwellSeconds:a.dwellSeconds??3.2,standoff:a.standoff??26,reducedMotion:a.reducedMotion??!1,shots:a.shots??[],centerOnOrigin:a.centerOnOrigin??!1}}shotAt(e){return this.opts.shots[e]??null}flightSecondsAt(e){var t;return((t=this.shotAt(e))==null?void 0:t.flightSeconds)??this.opts.flightSeconds}dwellSecondsAt(e){var t;return((t=this.shotAt(e))==null?void 0:t.dwellSeconds)??this.opts.dwellSeconds}get totalBeats(){return this.path.beats.length}get isRunning(){return this.phase!=="idle"&&this.phase!=="done"}start(){var e,t;if(this.path.beats.length===0){this.phase="done",(t=(e=this.cb).onComplete)==null||t.call(e);return}this.beatIndex=0,this.beginFlightTo(0)}stop(){this.phase="done"}focalPoint(e){return this.opts.centerOnOrigin?XM:this.positions.get(e.nodeId)??null}framePosition(e,t,n){const i=this.focalPoint(e);if(!i)return n.copy(this.camera.position);const s=this.shotAt(t);jt.copy(this.camera.position).sub(i),jt.lengthSq()<1e-4&&jt.set(0,.4,1),jt.normalize();let a=.35;s&&(s.angle==="low"?a=-.45:s.angle==="high"&&(a=.7)),jt.addScaledVector(WM,a).normalize();let o=(s==null?void 0:s.standoff)??this.opts.standoff;return s&&(s.move==="push_in"?o*=.7:s.move==="pull_back"?o*=1.5:s.move==="crane"&&(o*=1.8)),this.opts.centerOnOrigin&&(o=Math.max(31,Math.min(43,o))),n.copy(i).addScaledVector(jt,o)}beginFlightTo(e){var a,o;const t=this.path.beats[e],n=this.focalPoint(t),i=this.shotAt(e);this.fromPos.copy(this.camera.position),this.fromTarget.copy(this.target),this.framePosition(t,e,this.toPos),this.toTarget.copy(n??this.target),this.phaseElapsed=0,this.opts.reducedMotion||(i==null?void 0:i.cut)==="hard_cut"||(i==null?void 0:i.cut)==="match_cut"?(this.camera.position.copy(this.toPos),this.target.copy(this.toTarget),this.phase="dwelling",(o=(a=this.cb).onBeat)==null||o.call(a,t,e,i)):this.phase="flying"}update(e){var o,l,c,h,u,f,d,p,_;if(this.phase==="idle"||this.phase==="done")return;const t=Math.max(0,Math.min(e,.05));this.phaseElapsed+=t;const n=this.flightSecondsAt(this.beatIndex),i=this.dwellSecondsAt(this.beatIndex);if(this.phase==="flying"){const g=Math.min(1,this.phaseElapsed/n),m=YM(g);this.camera.position.lerpVectors(this.fromPos,this.toPos,m),this.target.lerpVectors(this.fromTarget,this.toTarget,m),this.applyDutch(this.beatIndex,m),g>=1&&(this.phase="dwelling",this.phaseElapsed=0,(l=(o=this.cb).onBeat)==null||l.call(o,this.path.beats[this.beatIndex],this.beatIndex,this.shotAt(this.beatIndex)))}else if(this.phase==="dwelling"){if(!this.opts.reducedMotion){const g=this.focalPoint(this.path.beats[this.beatIndex]);g&&(this.target.lerp(g,.02),((c=this.shotAt(this.beatIndex))==null?void 0:c.move)==="orbit"&&this.orbitAround(g,t*.35))}if(this.phaseElapsed>=i){const g=this.beatIndex+1;if(g>=this.path.beats.length){this.phase="done",(u=(h=this.cb).onProgress)==null||u.call(h,1),(d=(f=this.cb).onComplete)==null||d.call(f);return}this.beatIndex=g,this.beginFlightTo(g)}}const s=this.path.beats.length>0?1/this.path.beats.length:0,a=this.phase==="flying"?Math.min(1,this.phaseElapsed/n)*.5:.5+Math.min(1,this.phaseElapsed/i)*.5;(_=(p=this.cb).onProgress)==null||_.call(p,Math.min(1,this.beatIndex*s+a*s))}orbitAround(e,t){jt.copy(this.camera.position).sub(e);const n=Math.cos(t),i=Math.sin(t),s=jt.x*n-jt.z*i,a=jt.x*i+jt.z*n;jt.x=s,jt.z=a,this.camera.position.copy(e).add(jt)}applyDutch(e,t){var s;const i=(((s=this.shotAt(e))==null?void 0:s.dutch)??0)*t;jt.set(0,0,-1).applyQuaternion(this.camera.quaternion),this.camera.up.set(0,1,0).applyAxisAngle(jt,i)}}function YM(r){return r<.5?4*r*r*r:1-Math.pow(-2*r+2,3)/2}const Tc={origin:"Origin",connection:"Connection",contradiction:"Tension",recent:"Now",bridge:"Jump",surprise:"Surprise"};function ZM(r,e=90){const t=(r??"").replace(/\s+/g," ").trim();return t.length<=e?t:t.slice(0,e-1).trimEnd()+"…"}function Tl(r){const e=(r??"memory").toLowerCase();return e.charAt(0).toUpperCase()+e.slice(1)}function df(r){return{source:"local-captions",beats:r.beats.map((t,n)=>{var o,l;const i=t.node,s=ZM(i.label||`(${Tl(i.type)} memory)`);let a;switch(t.kind){case"origin":a=`We begin at a ${Tl(i.type).toLowerCase()} the graph is centered on — "${s}".`;break;case"contradiction":{a=`This is held in tension with the last memory through ${(o=t.viaEdge)!=null&&o.type?t.viaEdge.type.replace(/_/g," "):"a conflict"}: "${s}".`;break}case"recent":a=`And where the mind is now — a recent memory: "${s}".`;break;case"bridge":a=`Crossing to a separate cluster — "${s}".`;break;default:{const c=((l=t.viaEdge)==null?void 0:l.weight)??0;a=`${c>.66?"strongly":c>.33?"closely":"loosely"} connected from there: a ${Tl(i.type).toLowerCase()} — "${s}".`}}return i.tags&&i.tags.length>0&&n>0&&(a+=` [${i.tags.slice(0,3).join(", ")}]`),{nodeId:t.nodeId,text:a,chip:Tc[t.kind]}})}}async function JM(r,e){const t=df(r);if(!e)return t;let n;try{const i=await Promise.race([e(),new Promise(l=>{n=setTimeout(()=>l(null),6e3)})]),s=Array.isArray(i)?i.filter(l=>!!l&&typeof l.nodeId=="string"&&typeof l.text=="string"&&l.text.trim().length>0):[];if(s.length===0)return t;const a=new Map(s.map(l=>[l.nodeId,l]));return{source:"backend-llm",beats:r.beats.map((l,c)=>{const h=a.get(l.nodeId);if(h){const u=typeof h.chip=="string"&&h.chip.trim()?h.chip:Tc[l.kind];return{nodeId:l.nodeId,text:h.text,chip:u}}return t.beats[c]??{nodeId:l.nodeId,text:l.node.label||"(unlabeled memory)",chip:Tc[l.kind]}})}}catch{return t}finally{n&&clearTimeout(n)}}function KM(r){const e=(r.type??"").toLowerCase();return e.includes("merge")||e.includes("supersede")||e.includes("duplicate")}function $M(r,e){const t=new Map;for(const n of r)t.set(n,0);for(const n of r){const i=[],s=new Map,a=new Map,o=new Map;for(const u of r)s.set(u,[]),a.set(u,0),o.set(u,-1);a.set(n,1),o.set(n,0);const l=[n];let c=0;for(;c0;){const u=i.pop();for(const f of s.get(u)??[]){const d=(a.get(f)??0)/(a.get(u)||1)*(1+(h.get(u)??0));h.set(f,(h.get(f)??0)+d)}u!==n&&t.set(u,(t.get(u)??0)+(h.get(u)??0))}}return t}function jM(r,e){const t=new Map;for(const l of r)t.set(l,l);const n=l=>{let c=l;for(;t.get(c)!==c;)c=t.get(c);let h=l;for(;t.get(h)!==c;){const u=t.get(h);t.set(h,c),h=u}return c},i=(l,c)=>{const h=n(l),u=n(c);h!==u&&t.set(h,u)};for(const l of e)t.has(l.source)&&t.has(l.target)&&i(l.source,l.target);const s=new Map,a=new Map;let o=0;for(const l of r){const c=n(l);s.has(c)||s.set(c,o++),a.set(l,s.get(c))}return{clusterOf:a,count:o}}function QM(r,e){var y;const t=r.map(v=>v.id),n=uf(e),i=[...r].sort((v,x)=>Ws(v)-Ws(x)),s=new Map;i.forEach((v,x)=>s.set(v.id,r.length>1?x/(r.length-1):1));const a=600;let o=t;t.length>a&&(o=[...t].sort((v,x)=>{var w,T;return(((w=n[x])==null?void 0:w.length)??0)-(((T=n[v])==null?void 0:T.length)??0)}).slice(0,a));const l=$M(o,n);let c=0;for(const v of l.values())c=Math.max(c,v);const{clusterOf:h,count:u}=jM(t,e),f=Math.max(1,...r.map(v=>v.suppression_count??0)),d=new Map;let p=t[0]??"",_=-1;for(const v of r){const x=c>0?(l.get(v.id)??0)/c:0;x>_&&(_=x,p=v.id),d.set(v.id,{nodeId:v.id,degree:((y=n[v.id])==null?void 0:y.length)??0,betweenness:x,clusterId:h.get(v.id)??0,recencyRank:s.get(v.id)??0,retention:Ha(v.retention??0),suppression:Ha((v.suppression_count??0)/f)})}const g=new Map;for(const v of t)g.set(v,new Set((n[v]??[]).map(x=>x.otherId)));const m=e.map(v=>{const x=g.get(v.source),w=g.get(v.target);let T=0;if(x&&w){const[M,D]=x.size{const h=t>1?c/(t-1):0,u=cb(h),f=e.nodes.get(l.nodeId),d=l.nodeId===e.peakBetweennessId,p=c===t-1,_=c===0;let g={nodeId:l.nodeId,move:"push_in",angle:"eye",cut:"fly",stormMode:"connection",tone:"curious",scoreCue:"motif",act:u,intensity:.6,tension:.3,why:"a connected memory"};return _&&(g={...g,move:"push_in",tone:"curious",tension:.25,stormMode:"anchor",why:"opening on the focal memory"}),(d||f&&f.betweenness>.6)&&(g={...g,move:"orbit",angle:"low",stormMode:"anchor",intensity:.75,tension:.45,tone:"awe",why:"low-angle orbit — the most load-bearing memory in the graph"}),l.kind==="contradiction"&&(g={...g,move:"push_in",angle:"eye",dutch:.28,cut:"hard_cut",stormMode:"contradiction",intensity:1,tension:.95,tone:"tense",scoreCue:"minor_drop",viaEdgeKey:l.viaEdge?`${l.viaEdge.source}->${l.viaEdge.target}`:void 0,why:"two memories in tension — a Dutch two-shot collision"}),l.kind==="surprise"&&(g={...g,move:"orbit",stormMode:"surprise",intensity:.85,tension:.6,tone:"awe",scoreCue:"motif",why:"a surprising, distant-but-plausible connection"}),f&&(f.retention<.35||f.suppression>.5)&&(g={...g,angle:"high",move:"pull_back",tone:"neutral",intensity:.4,why:"a fading memory — high-angle drift"}),l.kind==="recent"&&(g={...g,move:"push_in",tone:"resolved",tension:.4,why:"where the memory is now"}),p&&(g={...g,move:"crane",cut:"fly",stormMode:"anchor",tone:"awe",tension:.5,scoreCue:"major_resolve",why:"crane pull-back over the whole cluster — resolution"}),g}),i=r.beats.some(l=>l.kind==="contradiction")?"man_in_hole":"rags_to_riches";return{source:"deterministic",logline:`A short film about ${((o=r.beats[0])==null?void 0:o.node.label)??"a memory"} — ${t} shots through the graph${i==="man_in_hole"?", through a contradiction and out the other side":""}.`,arc:i,shots:n}}var ub=ut(''),db=ut(' '),fb=ut(' '),pb=ut('WebGPU'),mb=ut(' '),gb=ut(`
Director's plan

`),_b=ut('
'),vb=ut('

'),xb=ut('∞ dreaming'),yb=ut(''),Mb=ut('

',1),bb=ut(''),Sb=ut(' ',1);function wb(r,e){sr(e,!0);let t=Qe(!1),n=Qe("idle"),i=Qe(""),s=Qe(""),a=Qe(0),o=Qe(0),l=Qe(0),c=Qe(null),h=Qe(!1),u=Qe(!1),f=Qe(!1),d=Qe(!0),p=Qe(""),_=Qe(""),g=Qe("I"),m=Qe(0),y=Qe(""),v=Qe(null),x=Qe(void 0),w=null,T=null,C=null,P=null,S=0,M=0,D=null,G=0,F=Qe(null);const V=typeof window<"u"&&window.matchMedia("(prefers-reduced-motion: reduce)").matches,J=(k,X,W,ce)=>k+(X-k)*(1-Math.exp(-W*ce)),q={x:0,y:0},se={yaw:0,pitch:0};let Z=0,_e=0,be=0;const Re=2500,Ge=.35,it=.22,Q=()=>{be=performance.now()},ue=14;function Ie(k){const X=new Map,W=k.beats.length;for(let ce=0;ce1?ce/(W-1):.5)*2,Be=Math.sqrt(Math.max(0,1-ge*ge)),$=ce*2.399963;X.set(k.beats[ce].nodeId,new A(Math.cos($)*Be*ue,ge*ue*.5,Math.sin($)*Be*ue))}return X}function pe(k){if(!(!L(u)||typeof speechSynthesis>"u"))try{speechSynthesis.cancel();const X=new SpeechSynthesisUtterance(k);X.rate=.98,X.pitch=1,speechSynthesis.speak(X)}catch{}}function Oe(k){if(D&&clearInterval(D),le(i,""),V){le(i,k,!0);return}let X=0;D=setInterval(()=>{le(i,k.slice(0,++X),!0),X>=k.length&&D&&(clearInterval(D),D=null)},18)}function We(k){return k==="surprise"?"connection":k}function ze(k,X,W){var oe,ge;le(o,X+1);const ce=((oe=P==null?void 0:P.beats[X])==null?void 0:oe.text)??k.node.label??"";if(le(s,((ge=P==null?void 0:P.beats[X])==null?void 0:ge.chip)??"",!0),Oe(ce),pe(ce),W&&(le(_,W.why,!0),le(g,W.act,!0),le(m,W.tension,!0)),w&&L(h)){const Be=Je==null?void 0:Je.get(k.nodeId);if(Be){const ve=(W==null?void 0:W.stormMode)??"connection";w.transitionTo(We(ve),Be,(W==null?void 0:W.act)??"I",X)}const $=(W==null?void 0:W.tension)??0;w.setFlythrough(V?0:$*.8)}}let Je=null;async function te(){var ge,Be;if(cancelAnimationFrame(S),De(),D&&clearInterval(D),T==null||T.stop(),w==null||w.dispose(),w=null,T=null,P=null,G=0,le(_,""),le(y,""),le(v,null),le(g,"I"),le(m,0),le(t,!0),le(n,"planning"),le(p,"Planning a path through your memory…"),le(i,""),le(s,""),le(a,0),le(o,0),C=GM(e.nodes,e.edges,e.centerId,7),le(l,C.beats.length,!0),L(l)===0){le(p,"Not enough memory to compose a tour yet."),le(n,"done");return}Je=Ie(C);const k=QM(e.nodes,e.edges);le(v,hb(C,k),!0),le(y,L(v).logline,!0);const X=lb(L(v),C);if(le(g,((ge=X[0])==null?void 0:ge.act)??"I",!0),le(m,((Be=X[0])==null?void 0:Be.tension)??0,!0),P=await JM(C,L(f)?Ve():e.fetchBackendNarration),le(c,P.source,!0),le(h,!1),L(x))try{const{CinemaSandbox:$,isWebGPUSupported:ve}=await Sf(async()=>{const{CinemaSandbox:Y,isWebGPUSupported:ie}=await import("./Ma4NfFrG.js");return{CinemaSandbox:Y,isWebGPUSupported:ie}},__vite__mapDeps([0,1]),import.meta.url);ve()&&(w=new $(L(x)),await w.boot(),le(h,!0))}catch($){console.warn("[cinema] WebGPU sandbox unavailable, camera-only mode:",$),w=null,le(h,!1)}const W=L(x)&&L(x).clientHeight>0?L(x).clientWidth/L(x).clientHeight:16/9,ce=(w==null?void 0:w.cameraRef)??new kt(60,W,.1,2e3),oe=(w==null?void 0:w.target)??new A;T=new qM(ce,oe,Je,C,{onBeat:ze,onProgress:$=>le(a,$,!0),onComplete:()=>{le(n,"done"),le(p,V||!L(h)?"End of tour.":"∞ Dreaming — endless generative figures",!0),I()}},{reducedMotion:V,shots:X,centerOnOrigin:L(h)}),le(n,"playing"),le(p,L(h)?"Rendering 150k-particle semantic storm on WebGPU…":"Cinematic flythrough (captions mode).",!0),M=performance.now(),T.start(),de()}async function de(){const k=performance.now(),X=Math.max(0,Math.min(.05,(k-M)/1e3));M=k;try{T==null||T.update(X)}catch(ce){console.warn("[cinema] director error:",ce)}if(!V&&w&&L(h)){const ce=w.cameraRef,oe=performance.now()-be>Re,ge=oe?0:q.x*Ge,Be=oe?0:q.y*it,$=oe?1.5:9;se.yaw=J(se.yaw,ge,$,X),se.pitch=J(se.pitch,Be,$,X),oe&&(Z=J(Z,0,1.2,X)),_e=J(_e,Z,8,X);const ve=ce.position.clone(),Y=new xc().setFromVector3(ve);Y.theta+=se.yaw,Y.phi=bd.clamp(Y.phi+se.pitch,.2,Math.PI-.2),Y.radius*=1-_e*.35,ve.setFromSpherical(Y),ce.position.copy(ve)}const W=w;if(W&&L(h))try{await W.render(X),G=0}catch(ce){++G>=3&&w===W&&(console.warn("[cinema] WebGPU render failing, dropping to camera-only:",ce),le(h,!1),W.dispose(),w=null)}S=requestAnimationFrame(de)}function I(){V||!w||!L(h)||(De(),V||w==null||w.setFlythrough(.6),w==null||w.dreamBeat(),le(i,""),le(s,"Dreaming"),le(F,setInterval(()=>{if(!w||!L(h)){De();return}w.dreamBeat()},5500),!0))}function De(){L(F)&&(clearInterval(L(F)),le(F,null))}function re(){var k;cancelAnimationFrame(S),De(),D&&clearInterval(D),typeof speechSynthesis<"u"&&speechSynthesis.cancel(),w&&((k=w.setFlythrough)==null||k.call(w,0)),se.yaw=se.pitch=0,Z=_e=0,T==null||T.stop(),w==null||w.dispose(),w=null,T=null,le(t,!1),le(n,"idle"),le(h,!1)}let Ae=Qe(void 0);function me(k){k.key==="Escape"?(k.preventDefault(),re()):(k.key==="h"||k.key==="H")&&(k.preventDefault(),le(d,!L(d)))}Ga(()=>{L(t)&&L(Ae)&&L(Ae).focus()}),Ga(()=>{if(!(typeof document>"u"))return document.body.classList.toggle("cinema-open",L(t)),()=>document.body.classList.remove("cinema-open")}),Ga(()=>{if(!L(t)||V||!L(x))return;const k=L(x),X=($,ve,Y)=>Math.min(Y,Math.max(ve,$));k.style.touchAction="none";const W=$=>{q.x=$.clientX/window.innerWidth*2-1,q.y=-($.clientY/window.innerHeight)*2+1,Q()},ce=$=>{$.preventDefault(),Z=X(Z+$.deltaY*8e-4,-1,1),Q()};let oe=null;const ge=$=>{if($.touches.length===2){const ve=$.touches[0].clientX-$.touches[1].clientX,Y=$.touches[0].clientY-$.touches[1].clientY,ie=Math.hypot(ve,Y);oe!==null&&(Z=X(Z+(ie-oe)*.002,-1,1)),oe=ie,Q()}},Be=()=>{oe=null};return k.addEventListener("pointermove",W,{passive:!0}),k.addEventListener("wheel",ce,{passive:!1}),k.addEventListener("touchmove",ge,{passive:!0}),k.addEventListener("touchend",Be),()=>{k.removeEventListener("pointermove",W),k.removeEventListener("wheel",ce),k.removeEventListener("touchmove",ge),k.removeEventListener("touchend",Be)}});function Ve(){return async()=>{var k,X;if(!C)return null;try{le(p,"Loading on-device model (first run downloads weights)…");const ce=await import("@huggingface/transformers").catch(()=>null);if(!(ce!=null&&ce.pipeline))return null;const oe=await ce.pipeline("text-generation","onnx-community/Qwen2.5-0.5B-Instruct",{device:"webgpu",dtype:"q4"}),ge=df(C);le(p,"Narrating with the on-device model…");const Be=[];for(const $ of ge.beats){const ve=`You are narrating a cinematic tour of an AI's memory graph. In one vivid sentence, narrate this beat: "${$.text}"`,Y=await oe(ve,{max_new_tokens:48,temperature:.7,do_sample:!0}),ie=(X=(k=Y==null?void 0:Y[0])==null?void 0:k.generated_text)==null?void 0:X.replace(ve,"").trim();Be.push({nodeId:$.nodeId,chip:$.chip,text:ie&&ie.length>4?ie:$.text})}return Be}catch(W){return console.warn("[cinema] on-device narration failed, using local captions:",W),null}}}Ac(re);var Se=Sb(),R=no(Se),b=Ee(R,2);{var H=k=>{var X=bb(),W=xe(X);Rl(W,$=>le(x,$),()=>L(x));var ce=Ee(W,2);{var oe=$=>{var ve=ub();Ut("click",ve,()=>le(d,!0)),ot($,ve)};It(ce,$=>{L(d)||$(oe)})}var ge=Ee(ce,2);{var Be=$=>{var ve=Mb(),Y=no(ve),ie=xe(Y),ae=xe(ie);let Ne;var Pe=Ee(ae,2),Ke=xe(Pe,!0);fe(Pe);var U=Ee(Pe,2);{var ye=N=>{var z=db(),O=xe(z,!0);fe(z),Pt(()=>ct(O,L(v).source==="deterministic"?"Auteur (local)":"Auteur (AI)")),ot(N,z)};It(U,N=>{L(v)&&N(ye)})}var K=Ee(U,2);{var ee=N=>{var z=fb(),O=xe(z,!0);fe(z),Pt(()=>ct(O,L(c)==="backend-llm"?"AI narration":"Live captions")),ot(N,z)};It(K,N=>{L(c)&&N(ee)})}var we=Ee(K,2);{var Me=N=>{var z=pb();ot(N,z)};It(we,N=>{L(h)&&N(Me)})}var Ye=Ee(we,2);{var Mt=N=>{var z=mb(),O=xe(z);fe(z),Pt(()=>ct(O,`Act ${L(g)??""}`)),ot(N,z)};It(Ye,N=>{L(n)==="playing"&&N(Mt)})}fe(ie);var St=Ee(ie,2),at=xe(St),Yt=xe(at);Br(Yt),Or(),fe(at);var Ht=Ee(at,2),Di=xe(Ht);Br(Di),Or(),fe(Ht);var ui=Ee(Ht,2);Rl(ui,N=>le(Ae,N),()=>L(Ae)),fe(St),fe(Y);var En=Ee(Y,2);{var di=N=>{var z=gb(),O=Ee(xe(z),2),he=xe(O,!0);fe(O),fe(z),Pt(()=>ct(he,L(y))),ot(N,z)};It(En,N=>{L(n)==="planning"&&L(y)&&N(di)})}var Li=Ee(En,2),Wn=xe(Li);{var Xn=N=>{var z=_b(),O=xe(z,!0);fe(z),Pt(()=>ct(O,L(s))),ot(N,z)};It(Wn,N=>{L(s)&&N(Xn)})}var fi=Ee(Wn,2),Fn=xe(fi,!0);fe(fi);var Ui=Ee(fi,2);{var Zt=N=>{var z=vb(),O=xe(z);fe(z),Pt(()=>ct(O,`▸ ${L(_)??""}`)),ot(N,z)};It(Ui,N=>{L(_)&&L(n)==="playing"&&N(Zt)})}var Nt=Ee(Ui,2),mn=xe(Nt);fe(Nt);var Tn=Ee(Nt,2),pi=xe(Tn);{var fr=N=>{var z=xb();ot(N,z)},ds=N=>{var z=gf();Pt(()=>ct(z,`Beat ${L(o)??""} / ${L(l)??""}`)),ot(N,z)};It(pi,N=>{L(n)==="done"&&L(F)?N(fr):L(l)>0&&N(ds,1)})}var E=Ee(pi,2);{var B=N=>{var z=yb();Ut("click",z,te),ot(N,z)};It(E,N=>{L(n)==="done"&&N(B)})}fe(Tn),fe(Li),Pt(()=>{Ne=Ns(ae,1,"cinema-dot svelte-1uwqs3k",null,Ne,{active:L(n)==="playing"}),ct(Ke,L(p)),ct(Fn,L(i)),zr(mn,`width:${L(a)*100}%; --tension:${L(m)??""}`)}),ph(Yt,()=>L(u),N=>le(u,N)),ph(Di,()=>L(f),N=>le(f,N)),Ut("click",ui,re),ot($,ve)};It(ge,$=>{L(d)&&$(Be)})}fe(X),Ut("keydown",X,me),ot(k,X)};It(b,k=>{L(t)&&k(H)})}Ut("click",R,te),ot(r,Se),rr()}Cc(["click","keydown"]);var Eb=ut('

Weaving your memory graph…

'),Tb=ut(`

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
`),Ab=ut('

Your Mind Awaits

No memories yet — the moment Vestige starts remembering, your constellation will bloom here.

'),Cb=ut('

Your Mind Awaits

'),Rb=ut(' · · ',1),Ib=ut('
'),Pb=ut('
'),Db=ut('
AhaGraph
'),Lb=ut(' '),Ub=ut('
'),Nb=ut("
"),Fb=ut(`

Memory Detail

Retention Forecast
Explore Connections
`),Ob=ut(`
`);function Fw(r,e){sr(e,!0);const t=()=>yf(Ef,"$eventFeed",n),[n,i]=xf();let s=Qe(null),a=Qe(null),o=Qe(!0),l=Qe(""),c=Qe(!1),h=Qe(""),u=Qe(150);const f=[{value:"50",label:"50 nodes"},{value:"100",label:"100 nodes"},{value:"150",label:"150 nodes"},{value:"200",label:"200 nodes"}];let d=Qe("150");function p(Y){le(u,parseInt(Y,10),!0),S()}let _=Qe(!1),g=Qe(Al(new Date)),m=Qe("type");const y=Object.entries(Qa);let v=Qe(0),x=Qe(0),w=ei(()=>L(s)?L(_)?ad(L(s).nodes,L(s).edges,L(g)).visibleNodes:L(s).nodes:[]),T=ei(()=>L(s)?L(_)?ad(L(s).nodes,L(s).edges,L(g)).visibleEdges:L(s).edges:[]);function C(Y){if(L(s))switch(Y.type){case"nodeAdded":L(s).nodes=[...L(s).nodes,Y.node],L(s).nodeCount=L(s).nodes.length,le(v,L(s).nodeCount,!0);break;case"nodeRemoved":L(s).nodes=L(s).nodes.filter(ie=>ie.id!==Y.nodeId),L(s).nodeCount=L(s).nodes.length,le(v,L(s).nodeCount,!0);break;case"edgeAdded":L(s).edges=[...L(s).edges,Y.edge],L(s).edgeCount=L(s).edges.length,le(x,L(s).edgeCount,!0);break;case"edgesRemoved":L(s).edges=L(s).edges.filter(ie=>ie.source!==Y.nodeId&&ie.target!==Y.nodeId),L(s).edgeCount=L(s).edges.length,le(x,L(s).edgeCount,!0);break;case"nodeUpdated":{const ie=L(s).nodes.find(ae=>ae.id===Y.nodeId);ie&&(ie.retention=Y.retention);break}}}od(()=>{const Y=new URLSearchParams(window.location.search),ie=Y.get("colorMode");P(ie)&&le(m,ie,!0);const ae=Y.get("center");S(void 0,ae||void 0)});function P(Y){return Y==="type"||Y==="state"||Y==="ahagraph"}async function S(Y,ie){var ae;le(o,!0),le(l,"");try{const Ne=!Y&&!ie;if(le(s,await ms.graph({max_nodes:L(u),depth:3,query:Y||void 0,center_id:ie||void 0,sort:Ne?"recent":void 0}),!0),Ne&&L(s)&&L(s).nodeCount<=1&&L(s).edgeCount===0){const Pe=await ms.graph({max_nodes:L(u),depth:3,sort:"connected"});Pe&&Pe.nodeCount>L(s).nodeCount&&le(s,Pe,!0)}L(s)&&(le(v,L(s).nodeCount,!0),le(x,L(s).edgeCount,!0))}catch(Ne){const Pe=Ne instanceof Error?Ne.message:String(Ne),Ke=Pe.replace(/\/[\w./-]+\.(sqlite|rs|db|toml|lock)\b/g,"[path]").slice(0,200),U=Ne instanceof TypeError||/failed to fetch|NetworkError|load failed/i.test(Pe)||/^API 500:?\s*(Internal Server Error)?\s*$/i.test(Pe.trim()),ye=(((ae=L(s))==null?void 0:ae.nodeCount)??0)===0&&/not found|404|empty|no memor/i.test(Pe);U?le(l,"OFFLINE"):ye?le(l,"EMPTY"):le(l,`Failed to load graph: ${Ke}`)}finally{le(o,!1)}}async function M(){le(c,!0);try{await ms.dream(),await S()}catch{}finally{le(c,!1)}}async function D(Y){try{le(a,await ms.memories.get(Y),!0)}catch{le(a,null)}}function G(){L(h).trim()&&S(L(h))}var F=Ob(),V=xe(F);{var J=Y=>{var ie=Eb();ot(Y,ie)},q=Y=>{var ie=Tb(),ae=xe(ie),Ne=xe(ae),Pe=xe(Ne);ps(Pe,{name:"activation",size:52,strokeWidth:1.2}),fe(Ne);var Ke=Ee(Ne,8),U=xe(Ke),ye=Ee(U,2);fe(Ke),fe(ae),fe(ie),Pt(()=>Rt(ye,"href",`${mh??""}/settings`)),Ut("click",U,()=>S()),ot(Y,ie)},se=Y=>{var ie=Ab(),ae=xe(ie),Ne=xe(ae),Pe=xe(Ne);ps(Pe,{name:"graph",size:52,strokeWidth:1.2}),fe(Ne),Or(4),fe(ae),fe(ie),ot(Y,ie)},Z=Y=>{var ie=Cb(),ae=xe(ie),Ne=xe(ae),Pe=xe(Ne);ps(Pe,{name:"graph",size:52,strokeWidth:1.2}),fe(Ne);var Ke=Ee(Ne,4),U=xe(Ke,!0);fe(Ke),fe(ae),fe(ie),Pt(()=>ct(U,L(l))),ot(Y,ie)},_e=Y=>{DM(Y,{get nodes(){return L(w)},get edges(){return L(T)},get centerId(){return L(s).center_id},get events(){return t()},get isDreaming(){return L(c)},get colorMode(){return L(m)},onSelect:D,onGraphMutation:C})};It(V,Y=>{L(o)?Y(J):L(l)==="OFFLINE"?Y(q,1):L(l)==="EMPTY"?Y(se,2):L(l)?Y(Z,3):L(s)&&Y(_e,4)})}var be=Ee(V,2),Re=xe(be),Ge=xe(Re);Br(Ge);var it=Ee(Ge,2);fe(Re);var Q=Ee(Re,2),ue=xe(Q),Ie=xe(ue),pe=Ee(Ie,2),Oe=Ee(pe,2);fe(ue);var We=Ee(ue,2);wf(We,{get options(){return f},icon:"graph",class:"shrink-0",onChange:p,get value(){return L(d)},set value(Y){le(d,Y,!0)}});var ze=Ee(We,2),Je=Ee(xe(ze),2);Br(Je);var te=Ee(Je,2),de=xe(te);fe(te),fe(ze);var I=Ee(ze,2),De=xe(I),re=xe(De);ps(re,{name:"dreams",size:16}),fe(De);var Ae=Ee(De);fe(I);var me=Ee(I,2);{var Ve=Y=>{{let ie=ei(()=>{var ae;return((ae=L(s))==null?void 0:ae.center_id)??""});wb(Y,{get nodes(){return L(w)},get edges(){return L(T)},get centerId(){return L(ie)}})}};It(me,Y=>{L(w).length>0&&Y(Ve)})}var Se=Ee(me,2),R=xe(Se);ps(R,{name:"pulse",size:16}),fe(Se),fe(Q),fe(be);var b=Ee(be,2),H=xe(b);{var k=Y=>{var ie=Rb(),ae=no(ie),Ne=xe(ae);fe(ae);var Pe=Ee(ae,4),Ke=xe(Pe);fe(Pe);var U=Ee(Pe,4),ye=xe(U);fe(U),Pt(()=>{ct(Ne,`${L(v)??""} nodes`),ct(Ke,`${L(x)??""} edges`),ct(ye,`depth ${L(s).depth??""}`)}),ot(Y,ie)};It(H,Y=>{L(s)&&Y(k)})}fe(b);var X=Ee(b,2);{var W=Y=>{var ie=Ib(),ae=xe(ie);HM(ae,{}),fe(ie),ot(Y,ie)};It(X,Y=>{L(m)==="state"&&Y(W)})}var ce=Ee(X,2);{var oe=Y=>{var ie=Db(),ae=Ee(xe(ie),2);Pr(ae,21,()=>y,Wa,(Ne,Pe)=>{var Ke=ei(()=>_f(L(Pe),2));let U=()=>L(Ke)[0],ye=()=>L(Ke)[1];var K=Pb(),ee=xe(K),we=Ee(ee,2),Me=xe(we,!0);fe(we),fe(K),Pt(()=>{zr(ee,`background: ${ye()??""}`),ct(Me,oM[U()])}),ot(Ne,K)}),fe(ae),fe(ie),ot(Y,ie)};It(ce,Y=>{L(m)==="ahagraph"&&Y(oe)})}var ge=Ee(ce,2);{var Be=Y=>{zM(Y,{get nodes(){return L(s).nodes},onDateChange:ie=>{le(g,ie,!0)},onToggle:ie=>{le(_,ie,!0)}})};It(ge,Y=>{L(s)&&Y(Be)})}var $=Ee(ge,2);{var ve=Y=>{var ie=Fb(),ae=xe(ie),Ne=Ee(xe(ae),2);fe(ae);var Pe=Ee(ae,2),Ke=xe(Pe),U=xe(Ke),ye=xe(U,!0);fe(U);var K=Ee(U,2);Pr(K,17,()=>L(a).tags,Wa,(Zt,Nt)=>{var mn=Lb(),Tn=xe(mn,!0);fe(mn),Pt(()=>ct(Tn,L(Nt))),ot(Zt,mn)}),fe(Ke);var ee=Ee(Ke,2),we=xe(ee,!0);fe(ee);var Me=Ee(ee,2);Pr(Me,21,()=>[{label:"Retention",value:L(a).retentionStrength},{label:"Storage",value:L(a).storageStrength},{label:"Retrieval",value:L(a).retrievalStrength}],Wa,(Zt,Nt)=>{var mn=Ub(),Tn=xe(mn),pi=xe(Tn),fr=xe(pi,!0);fe(pi);var ds=Ee(pi,2),E=xe(ds);fe(ds),fe(Tn);var B=Ee(Tn,2),N=xe(B);fe(B),fe(mn),Pt(z=>{ct(fr,L(Nt).label),ct(E,`${z??""}%`),zr(N,`width: ${L(Nt).value*100}%; background: ${L(Nt).value>.7?"#10b981":L(Nt).value>.4?"#f59e0b":"#ef4444"}`)},[()=>(L(Nt).value*100).toFixed(1)]),ot(Zt,mn)}),fe(Me);var Ye=Ee(Me,2),Mt=Ee(xe(Ye),2);{let Zt=ei(()=>L(a).storageStrength*30);NM(Mt,{get retention(){return L(a).retentionStrength},get stability(){return L(Zt)}})}fe(Ye);var St=Ee(Ye,2),at=xe(St),Yt=xe(at);fe(at);var Ht=Ee(at,2),Di=xe(Ht);fe(Ht);var ui=Ee(Ht,2);{var En=Zt=>{var Nt=Nb(),mn=xe(Nt);fe(Nt),Pt(Tn=>ct(mn,`Accessed: ${Tn??""}`),[()=>new Date(L(a).lastAccessedAt).toLocaleString()]),ot(Zt,Nt)};It(ui,Zt=>{L(a).lastAccessedAt&&Zt(En)})}var di=Ee(ui,2),Li=xe(di);fe(di),fe(St);var Wn=Ee(St,2),Xn=xe(Wn),fi=Ee(Xn,2);fe(Wn);var Fn=Ee(Wn,2),Ui=xe(Fn);ps(Ui,{name:"explore",size:14}),Or(),fe(Fn),fe(Pe),fe(ie),Pt((Zt,Nt)=>{ct(ye,L(a).nodeType),ct(we,L(a).content),ct(Yt,`Created: ${Zt??""}`),ct(Di,`Updated: ${Nt??""}`),ct(Li,`Reviews: ${L(a).reviewCount??0??""}`),Rt(Fn,"href",`${mh??""}/explore`)},[()=>new Date(L(a).createdAt).toLocaleString(),()=>new Date(L(a).updatedAt).toLocaleString()]),Ut("click",Ne,()=>le(a,null)),Ut("click",Xn,()=>{L(a)&&ms.memories.promote(L(a).id)}),Ut("click",fi,()=>{L(a)&&ms.memories.demote(L(a).id)}),ot(Y,ie)};It($,Y=>{L(a)&&Y(ve)})}fe(F),Pt((Y,ie)=>{Rt(Ie,"aria-checked",L(m)==="type"),Ns(Ie,1,`min-h-9 px-3 py-1.5 rounded-lg transition ${L(m)==="type"?"bg-synapse/25 text-synapse-glow":"text-dim hover:text-text"}`),Rt(pe,"aria-checked",L(m)==="state"),Ns(pe,1,`min-h-9 px-3 py-1.5 rounded-lg transition ${L(m)==="state"?"bg-synapse/25 text-synapse-glow":"text-dim hover:text-text"}`),Rt(Oe,"aria-checked",L(m)==="ahagraph"),Ns(Oe,1,`min-h-9 px-3 py-1.5 rounded-lg transition ${L(m)==="ahagraph"?"bg-synapse/25 text-synapse-glow":"text-dim hover:text-text"}`),Rt(ze,"title",`Adjust graph brightness (${Y??""}x). Combines with auto distance compensation.`),Rt(Je,"min",ji.brightnessMin),Rt(Je,"max",ji.brightnessMax),ct(de,`${ie??""}x`),I.disabled=L(c),Ns(I,1,`shrink-0 inline-flex items-center gap-2 min-h-10 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 - ${L(c)?"glow-dream animate-pulse-glow":""}`),Ns(De,1,vf(L(c)?"breathe":"")),ct(Ae,` ${L(c)?"Dreaming…":"Dream"}`)},[()=>ji.brightness.toFixed(1),()=>ji.brightness.toFixed(1)]),Ut("keydown",Ge,Y=>Y.key==="Enter"&&G()),Cl(Ge,()=>L(h),Y=>le(h,Y)),Ut("click",it,G),Ut("click",Ie,()=>le(m,"type")),Ut("click",pe,()=>le(m,"state")),Ut("click",Oe,()=>le(m,"ahagraph")),Cl(Je,()=>ji.brightness,Y=>ji.brightness=Y),Ut("click",I,M),Ut("click",Se,()=>S()),ot(r,F),rr(),i()}Cc(["click","keydown"]);export{cs as $,Wt as A,fn as B,ne as C,IS as D,ci as E,pt as F,ns as G,si as H,Pd as I,Pc as J,YS as K,rg as L,bd as M,ii as N,Rc as O,lr as P,wn as Q,zc as R,ho as S,Pi as T,Ii as U,A as V,uo as W,on as X,qs as Y,ch as Z,sw as _,tt as a,Nn as a$,op as a0,yt as a1,Lo as a2,kt as a3,or as a4,Et as a5,Ld as a6,Ti as a7,Qs as a8,Ze as a9,ld as aA,To as aB,Ci as aC,Bn as aD,Fs as aE,_c as aF,yg as aG,Mg as aH,Jd as aI,mg as aJ,Sg as aK,Jf as aL,Kf as aM,$f as aN,cd as aO,Qf as aP,ep as aQ,rS as aR,io as aS,so as aT,cr as aU,js as aV,Ys as aW,$s as aX,li as aY,Dp as aZ,ao as a_,wi as aa,Ic as ab,In as ac,cp as ad,Qn as ae,vd as af,$m as ag,rs as ah,rt as ai,JS as aj,Zf as ak,Yf as al,bo as am,wo as an,Mn as ao,tm as ap,Cd as aq,em as ar,qr as as,qe as at,dn as au,Fe as av,mt as aw,cS as ax,Si as ay,xn as az,j as b,Yl as b$,ro as b0,hd as b1,Qt as b2,dp as b3,up as b4,fp as b5,hp as b6,Md as b7,pp as b8,lp as b9,Ul as bA,Ll as bB,Dc as bC,Lc as bD,fd as bE,ud as bF,dd as bG,kr as bH,pd as bI,md as bJ,gd as bK,_d as bL,Uc as bM,So as bN,Nc as bO,Fc as bP,qa as bQ,Ya as bR,Za as bS,Ja as bT,kl as bU,Vl as bV,Hl as bW,Gl as bX,Wl as bY,Xl as bZ,ql as b_,Pf as ba,If as bb,Qi as bc,zf as bd,Vf as be,Dl as bf,Of as bg,Bf as bh,kf as bi,Hf as bj,Pl as bk,Ff as bl,Nf as bm,Uf as bn,Tf as bo,gh as bp,Af as bq,Rf as br,vh as bs,_h as bt,zl as bu,Bl as bv,Ol as bw,Fl as bx,Ks as by,Nl as bz,Wc as c,pn as c$,Zl as c0,Jl as c1,Kl as c2,$l as c3,jl as c4,Ql as c5,ec as c6,tc as c7,nc as c8,ic as c9,gs as cA,Lf as cB,Df as cC,Eo as cD,hS as cE,yd as cF,Vg as cG,yo as cH,QS as cI,dw as cJ,uw as cK,jS as cL,_m as cM,Ag as cN,Dw as cO,xh as cP,Rg as cQ,hw as cR,Kd as cS,lw as cT,aw as cU,Lw as cV,rp as cW,mm as cX,Id as cY,hr as cZ,xw as c_,sc as ca,rc as cb,ac as cc,Ka as cd,xd as ce,cc as cf,hc as cg,uc as ch,ai as ci,Td as cj,Ed as ck,CS as cl,AS as cm,RS as cn,ES as co,TS as cp,wS as cq,yh as cr,SS as cs,MS as ct,yS as cu,xS as cv,vS as cw,bS as cx,_S as cy,gS as cz,nr as d,Gn as d$,Iw as d0,Rw as d1,Eg as d2,Ei as d3,Hc as d4,Cw as d5,Dd as d6,Jc as d7,vm as d8,Kc as d9,Aw as dA,cg as dB,jc as dC,FS as dD,LS as dE,Rm as dF,Yc as dG,Qc as dH,Gc as dI,Yr as dJ,_w as dK,BS as dL,Mh as dM,Ew as dN,ww as dO,eh as dP,rw as dQ,Mo as dR,Op as dS,wg as dT,XS as dU,qS as dV,HS as dW,Po as dX,oo as dY,dc as dZ,Fo as d_,$d as da,Yd as db,KS as dc,$S as dd,qc as de,ew as df,$c as dg,Xf as dh,Gf as di,qg as dj,tw as dk,Ud as dl,Tm as dm,lg as dn,sS as dp,Hn as dq,Cm as dr,jf as ds,Zr as dt,vw as du,kc as dv,nw as dw,VS as dx,fg as dy,tp as dz,ht as e,Vs as e$,nm as e0,Ao as e1,Vc as e2,us as e3,Vn as e4,yw as e5,Nd as e6,Am as e7,gm as e8,hi as e9,cw as eA,ft as eB,Pg as eC,Fd as eD,Od as eE,rn as eF,Do as eG,hg as eH,ap as eI,pS as eJ,oc as eK,lc as eL,mS as eM,jm as eN,ar as eO,Wg as eP,fw as eQ,pw as eR,Io as eS,Xt as eT,Js as eU,th as eV,Uw as eW,oi as eX,Xc as eY,bw as eZ,sm as e_,qd as ea,lS as eb,co as ec,fu as ed,Zd as ee,np as ef,sp as eg,ip as eh,Xs as ei,Qd as ej,Hd as ek,Gd as el,oS as em,aS as en,Oc as eo,vo as ep,Ro as eq,qf as er,Wf as es,Cf as et,_o as eu,Pw as ev,Sw as ew,as as ex,Tw as ey,hs as ez,vt as f,an as f0,xc as f1,bg as f2,Bd as f3,Mw as f4,ts as f5,NS as f6,DS as f7,ow as f8,OS as f9,zs as fA,Fw as fB,PS as fa,US as fb,ur as fc,Os as fd,nh as fe,iw as ff,Nw as fg,ih as fh,sh as fi,yn as fj,fS as fk,dS as fl,uS as fm,rh as fn,GS as fo,WS as fp,jd as fq,mw as fr,xo as fs,ZS as ft,kS as fu,zS as fv,Sn as fw,Km as fx,lo as fy,Bs as fz,Ri as g,os as h,zn as i,_g as j,gw as k,Hr as l,Ai as m,qt as n,kn as o,ig as p,sg as q,ng as r,eg as s,Qm as t,ah as u,tg as v,Vt as w,Xa as x,Cr as y,ni as z}; diff --git a/apps/dashboard/build/_app/immutable/chunks/C-SOZ1Oi.js.br b/apps/dashboard/build/_app/immutable/chunks/C-SOZ1Oi.js.br deleted file mode 100644 index 85b0c51..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/C-SOZ1Oi.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/C-SOZ1Oi.js.gz b/apps/dashboard/build/_app/immutable/chunks/C-SOZ1Oi.js.gz deleted file mode 100644 index fcd5c95..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/C-SOZ1Oi.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/C8kRUgax.js b/apps/dashboard/build/_app/immutable/chunks/C8kRUgax.js deleted file mode 100644 index c7c6805..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/C8kRUgax.js +++ /dev/null @@ -1 +0,0 @@ -var du=Object.defineProperty;var hu=(E,i,r)=>i in E?du(E,i,{enumerable:!0,configurable:!0,writable:!0,value:r}):E[i]=r;var o=(E,i,r)=>hu(E,typeof i!="symbol"?i+"":i,r);import{StorageBufferAttribute as It,SpriteNodeMaterial as ru}from"./CfobEeQC.js";import{uniform as h,storage as lt,Fn as R,instanceIndex as W,fract as T,float as t,mx_noise_vec3 as X,vec3 as u,sin as m,cos as b,sqrt as Rt,floor as nt,pow as H,abs as et,clamp as C,vec2 as y,smoothstep as mt,oneMinus as at,mix as M,select as n,cross as is,length as $,min as vu,atan as cs,positionView as bu}from"./BZQzXWp7.js";import{V as ds,A as pu,P as gu,I as fu}from"./C-SOZ1Oi.js";const xu={anchor:0,connection:1,contradiction:2};class Bu{constructor(i,r,g={}){o(this,"count");o(this,"scene");o(this,"renderer");o(this,"bufferPos");o(this,"bufferVel");o(this,"bufferPhase");o(this,"computeNode");o(this,"mesh",null);o(this,"material",null);o(this,"computeInFlight",null);o(this,"uTarget",h(new ds(0,0,0)));o(this,"uTime",h(0));o(this,"uIgnition",h(.2));o(this,"uMode",h(0));o(this,"uContainRadius",h(48));o(this,"uHueShift",h(0));o(this,"uModeTintAmt",h(.25));o(this,"uBurst",h(0));o(this,"uActDim",h(.12));o(this,"uWorld",h(0));o(this,"uPrevWorld",h(0));o(this,"uBlend",h(0));o(this,"worldCount",7);o(this,"uBlast",h(0));o(this,"uBlastTime",h(0));o(this,"uMorphSeed",h(0));o(this,"uChaos",h(0));o(this,"uClash",h(0));o(this,"uFadeNear",h(2));o(this,"uFadeBand",h(7));o(this,"uFogDensity",h(.012));o(this,"uFocus",h(28));o(this,"uFocusRange",h(20));o(this,"uDofDim",h(.3));o(this,"uZoomPeriod",h(9));o(this,"uLambda",h(1.923));o(this,"uZoomOn",h(0));o(this,"uCamVelView",h(new ds(0,0,0)));o(this,"uStreak",h(0));o(this,"uMaxStretch",h(7));o(this,"dreamCount",0);this.renderer=i,this.scene=r,this.count=g.count??15e4;const f=g.spawnRadius??34,q=new Float32Array(this.count*3),D=new Float32Array(this.count*3),c=new Float32Array(this.count);for(let S=0;S{const c=f.element(W),p=q.element(W),B=D.element(W),P=B.mul(12.9898).sin().mul(43758.5453),S=B.mul(78.233).sin().mul(12543.531),L=B.mul(39.346).sin().mul(24634.633),O=T(P),V=T(S),a=T(L),l=O.mul(6.28318),d=V.mul(3.14159),e=this.uContainRadius,w=T(B.mul(3.7)),z=t(.62).add(w.mul(w).mul(.38)),A=t(W),v=R(([s])=>{const x=t(.6),es=X(s.add(u(x,0,0))).sub(X(s.sub(u(x,0,0)))),ms=X(s.add(u(0,x,0))).sub(X(s.sub(u(0,x,0)))),as=X(s.add(u(0,0,x))).sub(X(s.sub(u(0,0,x))));return u(ms.z.sub(as.y),as.x.sub(es.z),es.y.sub(ms.x)).normalize()}),F=u(m(d).mul(b(l)),b(d),m(d).mul(m(l))),G=F.mul(e.mul(z)),tt=F.mul(e.mul(t(.5).add(w.mul(.3)))),N=t(.19),Z=u(m(c.y).sub(c.x.mul(N)),m(c.z).sub(c.y.mul(N)),m(c.x).sub(c.z.mul(N))),_=c.add(Z.mul(e.mul(.12))),j=G,st=u(O.sub(.5).mul(2).mul(e.mul(.8)),V.sub(.5).mul(2).mul(e.mul(.8)),a.sub(.5).mul(2).mul(e.mul(.8))),K=O.mul(6.28318).mul(3).add(a.mul(.6)),it=e.mul(.2).add(e.mul(.8).mul(a)),hs=u(it.mul(b(K)),e.mul(.06).mul(m(B.mul(20))),it.mul(m(K))),rs=t(2.39996323),Wt=A.mul(rs),Dt=Rt(A).mul(e.mul(.0042)),vs=u(Wt.cos().mul(Dt),e.mul(.04).mul(m(B.mul(9))),Wt.sin().mul(Dt)),Y=this.uMorphSeed,xt=this.uChaos,bs=T(Y.mul(.731).add(.13)),ps=T(Y.mul(1.323).add(.51)),gs=T(Y.mul(2.117).add(.27)),yt=t(387),J=T(A.div(yt)),k=nt(A.div(yt)).div(yt),Vt=s=>s.exp().sub(s.mul(-1).exp()).mul(.5),Zt=s=>s.exp().add(s.mul(-1).exp()).mul(.5),I=(s,x)=>y(s.x.mul(x.x).sub(s.y.mul(x.y)),s.x.mul(x.y).add(s.y.mul(x.x))),wt=s=>{const x=s.x.exp();return y(x.mul(b(s.y)),x.mul(m(s.y)))},fs=s=>y(s.x.mul(s.x).add(s.y.mul(s.y)).max(1e-12).log().mul(.5),cs(s.y,s.x)),Ot=(s,x)=>wt(I(y(x,t(0)),fs(s))),xs=s=>y(Zt(s.x).mul(b(s.y)),Vt(s.x).mul(m(s.y))),ys=s=>y(Vt(s.x).mul(b(s.y)),Zt(s.x).mul(m(s.y))),Mt=s=>{const x=s.x.mul(s.x).add(s.y.mul(s.y)).max(1e-6);return y(s.x.div(x),s.y.mul(-1).div(x))},ct=t(2).add(nt(bs.mul(14))),Nt=l,ws=H(et(b(ct.mul(Nt).div(4))),t(2).add(ps.mul(8))).add(H(et(m(ct.mul(Nt).div(4))),t(2).add(gs.mul(8)))).add(1e-4).pow(t(-.5)),Ms=H(et(b(ct.mul(d).div(4))),t(3)).add(H(et(m(ct.mul(d).div(4))),t(3))).add(1e-4).pow(t(-.5)),Tt=e.mul(.85).mul(C(ws.mul(Ms).mul(.5),.1,1.4)),Ts=u(m(d).mul(b(l)).mul(Tt),b(d).mul(Tt),m(d).mul(m(l)).mul(Tt)),kt=t(5),Ht=nt(T(Y.mul(.013).add(A.mul(.00667))).mul(25)),Et=nt(Ht.div(5)),qs=Ht.sub(Et.mul(5)),Cs=J.mul(2).sub(1),Bs=k.mul(1.5708),Lt=y(Cs,Bs),As=wt(y(t(0),Et.mul(6.28318).div(kt))),zs=wt(y(t(0),qs.mul(6.28318).div(kt))),Gt=I(As,Ot(xs(Lt),t(.4))),_t=I(zs,Ot(ys(Lt),t(.4))),jt=this.uTime.mul(.25).add(Y).add(xt.mul(1.5)),Ss=u(Gt.x,_t.x,b(jt).mul(Gt.y).add(m(jt).mul(_t.y))).mul(e.mul(.55)),Kt=Rt(J),Yt=k.mul(6.28318),qt=y(Kt.mul(b(Yt)),Kt.mul(m(Yt))),Q=I(qt,qt),ut=I(Q,qt),dt=Mt(Q),ht=Mt(ut),Ps=y(ut.x.sub(ht.x).add(2.2360679),ut.y.sub(ht.y)),Ct=Mt(Ps),Fs=I(y(t(0),t(1)),y(Q.x.sub(dt.x),Q.y.sub(dt.y))),Is=y(Q.x.add(dt.x),Q.y.add(dt.y)),Rs=I(y(t(0),t(.6667)),y(ut.x.add(ht.x),ut.y.add(ht.y))),Bt=I(Ct,Fs).x,At=I(Ct,Is).x,zt=I(Ct,Rs).x.add(.5),St=Bt.mul(Bt).add(At.mul(At)).add(zt.mul(zt)).max(1e-4),Ws=u(Bt.div(St),At.div(St),zt.div(St).sub(.86)).mul(e.mul(.5)),rt=k.mul(2).sub(1),Jt=Rt(t(1).sub(rt.mul(rt)).max(0)).mul(.9).add(.25),Qt=J.mul(6.28318).add(rt.mul(6).mul(xt.add(.4))),Ds=u(Jt.mul(b(Qt)),rt.mul(1.4),Jt.mul(m(Qt))).mul(e.mul(.5)),Ut=t(2.2).add(xt.mul(2)),vt=J.mul(6.28318).mul(Ut),bt=k.mul(6.28318).mul(Ut),pt=this.uTime.mul(.3).add(Y.mul(6.28318)),Vs=m(vt).mul(b(bt)).add(m(bt).mul(b(pt))).add(m(pt).mul(b(vt))),Zs=b(vt).mul(b(bt)).mul(b(pt)).sub(m(vt).mul(m(bt)).mul(m(pt))),Xt=this.uTime.mul(.15),Os=b(Xt).mul(Vs).add(m(Xt).mul(Zs)),Ns=u(m(k.mul(3.14159)).mul(b(J.mul(6.28318))),b(k.mul(3.14159)),m(k.mul(3.14159)).mul(m(J.mul(6.28318)))).mul(e.mul(.5).add(Os.mul(e.mul(.12)))),Pt=s=>n(s.equal(0),G,n(s.equal(1),tt,n(s.equal(2),_,n(s.equal(3),j,n(s.equal(4),st,n(s.equal(5),hs,n(s.equal(6),vs,n(s.equal(7),Ts,n(s.equal(8),Ss,n(s.equal(9),Ws,n(s.equal(10),Ds,Ns))))))))))),ks=Pt(t(this.uWorld)),Hs=Pt(t(this.uPrevWorld)),Es=mt(t(0),t(1),at(this.uBlend)),Ls=M(Hs,ks,Es),Gs=T(A.mul(.001).add(.5)).greaterThan(.66),Ft=t(this.uWorld).add(5),_s=n(Ft.greaterThan(11),Ft.sub(12),Ft),ot=Pt(_s),$t=this.uTime.mul(.4),ts=b($t),ss=m($t),js=u(ot.x.mul(ts).sub(ot.z.mul(ss)),ot.y,ot.x.mul(ss).add(ot.z.mul(ts))).mul(.52),Ks=this.uTime.div(this.uZoomPeriod).fract(),Ys=this.uTime.div(this.uZoomPeriod).add(.5).fract(),Js=M(t(1),this.uLambda.pow(Ks),this.uZoomOn),Qs=M(t(1),this.uLambda.pow(Ys),this.uZoomOn),Us=Ls.mul(Js),Xs=js.mul(Qs.mul(this.uLambda)),us=M(Us,Xs,Gs.select(t(1),t(0))),$s=c.normalize(),tu=at(T(B.mul(7.3)).mul(.4));p.addAssign($s.mul(this.uBurst.mul(.95).mul(tu))),p.addAssign(us.sub(c).mul(.045));const su=v(c.mul(.045).add(u(0,this.uTime.mul(.2),0))),uu=n(this.uWorld.equal(0),t(.05),n(this.uWorld.equal(5),t(.06),t(0)));p.addAssign(su.mul(uu)),p.addAssign(is(u(0,1,0),c).mul(9e-4).mul(n(this.uWorld.equal(1),t(1),t(0)))),p.addAssign(Z.mul(.012).mul(n(this.uWorld.equal(2),t(1),t(0))));const gt=c.x.div(e.mul(.5)),ft=c.y.div(e.mul(.5)),U=c.z.div(e.mul(.5)),ou=u(U.sub(.7).mul(gt).sub(ft.mul(3.5)),gt.mul(3.5).add(U.sub(.7).mul(ft)),t(.6).add(U.mul(.95)).sub(U.mul(U).mul(U).div(3)).sub(gt.mul(gt).add(ft.mul(ft))));p.addAssign(ou.mul(.008).mul(n(this.uWorld.equal(10),t(1),t(0)))),p.addAssign(is(u(0,1,0),c).mul(.0016).mul(n(this.uWorld.equal(5),t(1),t(0))));const lu=us.normalize().mul(m(this.uTime.mul(1.3).add(B.mul(6.1))).mul(.015));p.addAssign(lu);const os=$(p),nu=t(1.3);p.assign(p.mul(vu(nu,os).div(os.max(1e-4)))),c.addAssign(p),p.mulAssign(.9);const eu=n(this.uWorld.equal(4),t(1.6),t(1)),ls=M(t(.55),t(6),C(this.uBurst,0,1)),mu=nt(c.div(ls)).add(.5).mul(ls),au=C(at(this.uBurst.mul(1.4)),0,.9).mul(eu).min(.9);c.assign(M(c,mu,au));const iu=$(c),ns=this.uContainRadius,cu=c.normalize().mul(ns);c.assign(M(c,cu,iu.greaterThan(ns).select(t(1),t(0))))})().compute(this.count)}buildRender(i,r){const g=new ru({transparent:!0,blending:pu,depthWrite:!1}),f=lt(r,"float",this.count),q=lt(i,"vec3",this.count).element(W);g.positionNode=q;const D=g;{const a=u(this.uCamVelView),l=y(a.x,a.y),d=$(l).max(1e-4),e=T(f.element(W).mul(13.17)).mul(.5).add(.75);D.rotationNode=cs(l.y,l.x);const w=C(d.mul(this.uStreak).mul(e).mul(.6).add(1),t(1),this.uMaxStretch),z=t(1);D.scaleNode=y(z.mul(w),z)}const c=R(([a,l,d,e,w])=>l.add(d.mul(b(e.mul(a).add(w).mul(6.28318))))),p=R(([a])=>{const l=a.div(100),d=H(l.sub(60).max(1e-4),t(-.1332047592)).mul(329.698727446),e=l.lessThanEqual(66).select(t(255),d),w=l.max(1e-4).log().mul(99.4708025861).sub(161.1195681661),z=H(l.sub(60).max(1e-4),t(-.0755148492)).mul(288.1221695283),A=l.lessThanEqual(66).select(w,z),v=l.sub(10).max(1e-4).log().mul(138.5177312231).sub(305.0447927307),F=l.greaterThanEqual(66).select(t(255),l.lessThanEqual(19).select(t(0),v));return C(u(e,A,F).div(255),0,1)}),B=R(()=>{const a=q,l=f.element(W),d=$(a.sub(u(this.uTarget))),e=t(W),w=T(e.mul(.001).add(.5)).greaterThan(.66),z=a.x.mul(.03).add(a.y.mul(.021)).add(a.z.mul(.027)),A=T(l.mul(.41).add(d.mul(.06)).add(z).add(this.uTime.mul(.1)).add(this.uHueShift)),v=this.uClash,F=n(v.equal(0),u(0,.85,1),n(v.equal(1),u(.55,1,0),n(v.equal(2),u(1,.82,0),n(v.equal(3),u(0,1,.6),u(.1,.5,1))))),G=n(v.equal(0),u(.3,.2,1),n(v.equal(1),u(0,.7,.5),n(v.equal(2),u(1,.4,0),n(v.equal(3),u(0,.6,1),u(.5,0,1))))),tt=n(v.equal(0),u(1,.25,0),n(v.equal(1),u(1,0,.55),n(v.equal(2),u(.6,0,1),n(v.equal(3),u(1,.1,.3),u(1,.7,0))))),N=n(v.equal(0),u(1,0,.3),n(v.equal(1),u(1,.45,0),n(v.equal(2),u(1,0,.7),n(v.equal(3),u(1,.5,0),u(1,.2,.4))))),Z=mt(t(0),t(1),A),_=M(F,G,Z),j=M(tt,N,Z),st=M(_,j,w.select(t(1),t(0))),K=n(this.uMode.equal(2),u(1,.08,.32),n(this.uMode.equal(3),u(1,.78,.1),u(.1,.9,1)));return M(st,K,this.uModeTintAmt.mul(.4))}),P=R(()=>{const a=q,l=C($(a).div(this.uContainRadius.max(1e-4)),0,1),d=l.mul(l),e=a.normalize(),w=H(at(et(e.z)),t(4)),z=t(.12).add(d.mul(.6)).add(w.mul(.5)),A=t(W),v=T(A.mul(.001).add(.5)).greaterThan(.66),F=t(.07).add(w.mul(.3)),G=v.select(F,z),tt=this.uTime.div(this.uZoomPeriod).fract(),N=this.uTime.div(this.uZoomPeriod).add(.5).fract(),Z=m(tt.mul(3.14159)),_=m(N.mul(3.14159)),j=Z.add(_).max(1e-4),st=M(t(1),Z.div(j).mul(2).min(1),this.uZoomOn),K=M(t(1),_.div(j).mul(2).min(1),this.uZoomOn),it=v.select(K,st);return G.mul(it)}),S=R(()=>{const a=bu.z.negate(),l=mt(this.uFadeNear,this.uFadeNear.add(this.uFadeBand),a),d=C(this.uFogDensity.mul(a).negate().exp(),.45,1),e=C(a.sub(this.uFocus).abs().div(this.uFocusRange),0,1),w=at(e.mul(this.uDofDim));return l.mul(d).mul(w)}),L=R(()=>{const a=q,l=C(this.uBlast,0,1),d=this.uBlastTime,e=C($(a).div(this.uContainRadius.max(1e-4)),0,1),w=M(t(1600),t(5200),l),z=C(l.mul(1.1).add(.4),0,1.3),A=p(w).mul(z),v=T(e.mul(1.6).sub(d.mul(1.5))),F=c(v,u(.55),u(.55),u(3),u(0,.33,.67));return M(A,F,t(.78))});g.colorNode=R(()=>{const a=C(this.uIgnition.mul(.05).add(.72),0,1.25),l=B().mul(a).mul(P()).mul(this.uActDim),d=mt(t(0),t(.85),C(this.uBlast,0,1)).mul(.6);return M(l,L().mul(P()).mul(this.uActDim),d).mul(S())})(),g.emissiveNode=R(()=>{const a=C(this.uIgnition.mul(.04).add(.85),0,1.35),l=B().mul(a).mul(P()).mul(this.uActDim),d=mt(t(0),t(.85),C(this.uBlast,0,1)).mul(.6);return M(l,L().mul(.85).mul(P()).mul(this.uActDim),d).mul(S())})();const O=new gu(.1,.1),V=new fu(O,g,this.count);V.frustumCulled=!1,this.material=g,this.mesh=V,this.scene.add(this.mesh)}async update(i){const r=Math.max(0,Math.min(i,.05));this.uTime.value+=r,this.uHueShift.value=(this.uHueShift.value+r*.06)%1,this.uBlend.value=Math.max(0,this.uBlend.value-r*1),this.uIgnition.value=Math.max(0,this.uIgnition.value-r*2),this.uBurst.value=Math.max(0,this.uBurst.value-r*.85),this.uBlast.value=Math.max(0,this.uBlast.value-r*.35),this.uBlastTime.value+=r;const g=this.uTime.value/this.uZoomPeriod.value%1,f=this.uZoomOn.value>.5?40-g*16:26+Math.sin(this.uTime.value*.18)*9;this.uFocus.value+=(f-this.uFocus.value)*Math.min(1,r*3),this.computeInFlight&&await this.computeInFlight,this.computeInFlight=this.renderer.computeAsync(this.computeNode).finally(()=>{this.computeInFlight=null}),await this.computeInFlight}transitionTo(i,r,g="II",f=99){this.uTarget.value.copy(r);const q=xu[i]??1;this.uMode.value=q,this.uPrevWorld.value=this.uWorld.value,this.uWorld.value=f%this.worldCount,this.uBlend.value=1,this.uClash.value=f%5,this.uZoomOn.value=f>=2?1:0;const D=f===0?.12:f===1?.2:null,c=g==="I"?.26:1;this.uActDim.value=D??c,this.uIgnition.value=f<=1?.4:g==="I"?1.6:4.5;const p=this.uWorld.value===3;this.uBurst.value=q===2||p?1:.8,this.uBlast.value=f<=1?.25:1,this.uBlastTime.value=0,this.uModeTintAmt.value=q>=2?.7:.22}dreamBeat(){this.dreamCount+=1;const i=7+Math.floor(Math.random()*5);this.uPrevWorld.value=this.uWorld.value,this.uWorld.value=i,this.uBlend.value=1,this.uMorphSeed.value=Math.random()*1e3,this.uClash.value=Math.floor(Math.random()*5),this.uChaos.value=Math.min(1,.25+this.dreamCount*.1),this.uZoomOn.value=1,this.uActDim.value=.85,this.uIgnition.value=3,this.uBurst.value=1,this.uBlast.value=1,this.uBlastTime.value=0;const r=[1,2,3];this.uMode.value=r[Math.floor(Math.random()*r.length)],this.uModeTintAmt.value=.3+Math.random()*.5}setContainRadius(i){this.uContainRadius.value=Math.max(8,i)}setZoom(i){this.uZoomOn.value=i?1:0}setCameraVel(i){this.uCamVelView.value.copy(i)}setStreak(i){this.uStreak.value=i}dispose(){var i,r,g,f;this.mesh&&(this.scene.remove(this.mesh),(i=this.mesh.geometry)==null||i.dispose(),(g=(r=this.mesh).dispose)==null||g.call(r),this.mesh=null),(f=this.material)==null||f.dispose(),this.material=null,this.bufferPos=null,this.bufferVel=null,this.bufferPhase=null}}export{Bu as SemanticComputeStorm}; diff --git a/apps/dashboard/build/_app/immutable/chunks/C8kRUgax.js.br b/apps/dashboard/build/_app/immutable/chunks/C8kRUgax.js.br deleted file mode 100644 index cb81051..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/C8kRUgax.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/C8kRUgax.js.gz b/apps/dashboard/build/_app/immutable/chunks/C8kRUgax.js.gz deleted file mode 100644 index cc5255b..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/C8kRUgax.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CLrXVRi2.js b/apps/dashboard/build/_app/immutable/chunks/CLrXVRi2.js deleted file mode 100644 index ba54390..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/CLrXVRi2.js +++ /dev/null @@ -1 +0,0 @@ -import{TempNode as O,NodeUpdateType as D,RendererUtils as F,QuadMesh as L,NodeMaterial as f}from"./CfobEeQC.js";import{nodeObject as P,uniform as g,texture as c,passTexture as W,Fn as B,luminance as j,smoothstep as q,mix as H,vec4 as m,uniformArray as M,float as b,uv as E,Loop as Q,int as U,add as X}from"./BZQzXWp7.js";import{R as N,H as w,b as x,V as _}from"./C-SOZ1Oi.js";const p=new L,Y=new x,G=new x(1,0),I=new x(0,1);let z;class J extends O{static get type(){return"BloomNode"}constructor(r,t=1,i=0,s=0){super("vec4"),this.inputNode=r,this.strength=g(t),this.radius=g(i),this.threshold=g(s),this.smoothWidth=g(.01),this._renderTargetsHorizontal=[],this._renderTargetsVertical=[],this._nMips=5,this._renderTargetBright=new N(1,1,{depthBuffer:!1,type:w}),this._renderTargetBright.texture.name="UnrealBloomPass.bright",this._renderTargetBright.texture.generateMipmaps=!1;for(let e=0;e{const a=this.inputNode,l=j(a.rgb),d=q(this.threshold,this.threshold.add(this.smoothWidth),l);return H(m(0),a,d)});this._highPassFilterMaterial=this._highPassFilterMaterial||new f,this._highPassFilterMaterial.fragmentNode=t().context(r.getSharedContext()),this._highPassFilterMaterial.name="Bloom_highPass",this._highPassFilterMaterial.needsUpdate=!0;const i=[3,5,7,9,11];for(let a=0;a{const d=b(1.2).sub(a);return H(a,d,l)}).setLayout({name:"lerpBloomFactor",type:"float",inputs:[{name:"factor",type:"float"},{name:"radius",type:"float"}]}),u=B(()=>{const a=o(s.element(0),this.radius).mul(m(e.element(0),1)).mul(this._textureNodeBlur0),l=o(s.element(1),this.radius).mul(m(e.element(1),1)).mul(this._textureNodeBlur1),d=o(s.element(2),this.radius).mul(m(e.element(2),1)).mul(this._textureNodeBlur2),h=o(s.element(3),this.radius).mul(m(e.element(3),1)).mul(this._textureNodeBlur3),n=o(s.element(4),this.radius).mul(m(e.element(4),1)).mul(this._textureNodeBlur4);return a.add(l).add(d).add(h).add(n).mul(this.strength)});return this._compositeMaterial=this._compositeMaterial||new f,this._compositeMaterial.fragmentNode=u().context(r.getSharedContext()),this._compositeMaterial.name="Bloom_comp",this._compositeMaterial.needsUpdate=!0,this._textureOutput}dispose(){for(let r=0;rs.sample(n),d=B(()=>{const n=e.element(0).toVar(),T=l(a).rgb.mul(n).toVar();return Q({start:U(1),end:U(t),type:"int",condition:"<"},({i:V})=>{const R=b(V),v=e.element(V),y=u.mul(o).mul(R),A=l(a.add(y)).rgb,C=l(a.sub(y)).rgb;T.addAssign(X(A,C).mul(v)),n.addAssign(b(2).mul(v))}),m(T.div(n),1)}),h=new f;return h.fragmentNode=d().context(r.getSharedContext()),h.name="Bloom_separable",h.needsUpdate=!0,h.colorTexture=s,h.direction=u,h.invSize=o,h}}const k=(S,r,t,i)=>P(new J(P(S),r,t,i));export{k as bloom,J as default}; diff --git a/apps/dashboard/build/_app/immutable/chunks/CLrXVRi2.js.br b/apps/dashboard/build/_app/immutable/chunks/CLrXVRi2.js.br deleted file mode 100644 index cba39ff..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/CLrXVRi2.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CLrXVRi2.js.gz b/apps/dashboard/build/_app/immutable/chunks/CLrXVRi2.js.gz deleted file mode 100644 index 16ed5a1..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/CLrXVRi2.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CNfQDikv.js b/apps/dashboard/build/_app/immutable/chunks/CNfQDikv.js new file mode 100644 index 0000000..81b3695 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/CNfQDikv.js @@ -0,0 +1 @@ +import{Y as u,Z as v,_ as h,m as i,$ as g,a0 as f,B as A,a1 as S}from"./CvjSAYrz.js";const p=Symbol("is custom element"),N=Symbol("is html"),T=f?"link":"LINK",E=f?"progress":"PROGRESS";function k(r){if(i){var s=!1,a=()=>{if(!s){if(s=!0,r.hasAttribute("value")){var e=r.value;_(r,"value",null),r.value=e}if(r.hasAttribute("checked")){var o=r.checked;_(r,"checked",null),r.checked=o}}};r.__on_r=a,A(a),S()}}function l(r,s){var a=d(r);a.value===(a.value=s??void 0)||r.value===s&&(s!==0||r.nodeName!==E)||(r.value=s??"")}function _(r,s,a,e){var o=d(r);i&&(o[s]=r.getAttribute(s),s==="src"||s==="srcset"||s==="href"&&r.nodeName===T)||o[s]!==(o[s]=a)&&(s==="loading"&&(r[u]=a),a==null?r.removeAttribute(s):typeof a!="string"&&L(r).includes(s)?r[s]=a:r.setAttribute(s,a))}function d(r){return r.__attributes??(r.__attributes={[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..470cb5e 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..4beeba4 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/CnZzd20v.js b/apps/dashboard/build/_app/immutable/chunks/CVpUe0w3.js similarity index 87% rename from apps/dashboard/build/_app/immutable/chunks/CnZzd20v.js rename to apps/dashboard/build/_app/immutable/chunks/CVpUe0w3.js index cbdf8cb..4a0c459 100644 --- a/apps/dashboard/build/_app/immutable/chunks/CnZzd20v.js +++ b/apps/dashboard/build/_app/immutable/chunks/CVpUe0w3.js @@ -1 +1 @@ -import{v as k,w as f,O as m,P as t,Q as _,G as b,C as i}from"./wpu9U-D0.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}; +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..4faa38e 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/CZfHMhLI.js b/apps/dashboard/build/_app/immutable/chunks/CZfHMhLI.js deleted file mode 100644 index bf586da..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/CZfHMhLI.js +++ /dev/null @@ -1 +0,0 @@ -const i="/api";async function t(e,o){const r=await fetch(`${i}${e}`,{headers:{"Content-Type":"application/json"},...o});if(!r.ok)throw new Error(`API ${r.status}: ${r.statusText}`);return r.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(([,r])=>r!==void 0).map(([r,s])=>[r,String(s)])).toString():"";return t(`/graph${o}`)},dream:()=>t("/dream",{method:"POST"}),explore:(e,o="associations",r,s=10)=>t("/explore",{method:"POST",body:JSON.stringify({from_id:e,action:o,to_id:r,limit:s})}),predict:()=>t("/predict",{method:"POST"}),importance:e=>t("/importance",{method:"POST",body:JSON.stringify({content:e})}),consolidate:()=>t("/consolidate",{method:"POST"}),retentionDistribution:()=>t("/retention-distribution"),intentions:(e="active")=>t(`/intentions?status=${e}`),deepReference:(e,o=20)=>t("/deep_reference",{method:"POST",body:JSON.stringify({query:e,depth:o})}),sanhedrin:{latest:()=>t("/sanhedrin/latest"),telemetry:(e=7)=>t(`/sanhedrin/telemetry?days=${e}`),appeal:(e,o,r,s)=>t("/sanhedrin/appeal",{method:"POST",body:JSON.stringify({reason:e,note:o,claimId:r,receiptId:s})})},traces:{list:(e=50)=>t(`/traces?limit=${e}`),get:e=>t(`/traces/${encodeURIComponent(e)}`),exportUrl:e=>`${i}/traces/${encodeURIComponent(e)}/export`},receipts:{list:(e=50)=>t(`/receipts?limit=${e}`),listForRun:(e,o=50)=>t(`/receipts?run=${encodeURIComponent(e)}&limit=${o}`),get:e=>t(`/receipts/${encodeURIComponent(e)}`)},memoryPrs:{list:(e,o=100)=>{const r=new URLSearchParams;return e&&r.set("status",e),r.set("limit",String(o)),t(`/memory-prs?${r.toString()}`)},get:e=>t(`/memory-prs/${encodeURIComponent(e)}`),act:(e,o)=>t(`/memory-prs/${encodeURIComponent(e)}/${o}`,{method:"POST"}),getMode:()=>t("/memory-prs/mode"),setMode:e=>t("/memory-prs/mode",{method:"POST",body:JSON.stringify({mode:e})})}};export{n as a}; diff --git a/apps/dashboard/build/_app/immutable/chunks/CZfHMhLI.js.br b/apps/dashboard/build/_app/immutable/chunks/CZfHMhLI.js.br deleted file mode 100644 index c18a573..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/CZfHMhLI.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CZfHMhLI.js.gz b/apps/dashboard/build/_app/immutable/chunks/CZfHMhLI.js.gz deleted file mode 100644 index cc66162..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/CZfHMhLI.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..6192b6e 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/CcUbQ_Wl.js.br b/apps/dashboard/build/_app/immutable/chunks/CcUbQ_Wl.js.br deleted file mode 100644 index e5e0cfe..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/CcUbQ_Wl.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CcUbQ_Wl.js.gz b/apps/dashboard/build/_app/immutable/chunks/CcUbQ_Wl.js.gz deleted file mode 100644 index 3d7c9cc..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/CcUbQ_Wl.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CfobEeQC.js b/apps/dashboard/build/_app/immutable/chunks/CfobEeQC.js deleted file mode 100644 index c2d39e6..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/CfobEeQC.js +++ /dev/null @@ -1,376 +0,0 @@ -import{C as mt,S as vg,c as Ag,d as Rg,G as ll,e as _t,f as Ku,a as Yn,g as Zs,h as Js,W as ln,i as Pn,B as Ct,j as dl,k as Cg,D as zs,l as oa,m as hl,V as j,L as pl,N as en,n as Eg,o as fl,p as wg,q as Mg,r as Bg,s as Fg,t as Ug,u as Pg,v as Dg,E as gl,M as ml,R as Ss,w as ft,x as _o,y as Xs,z as ms,b as Ye,F as Me,J as ze,U as Le,K as yl,O as xl,Q as Lg,T as Ig,X as Xu,Y as Ys,Z as Vg,_ as Gg,$ as Yu,a0 as Og,a1 as tn,a2 as Tl,a3 as kg,a4 as _l,a5 as aa,a6 as vs,H as pt,a7 as ss,a8 as kt,a9 as Ie,aa as Dn,ab as zg,ac as Wg,ad as ua,ae as fr,af as Ln,ag as $g,ah as Hg,ai as Ur,aj as bl,ak as qg,al as Kg,am as Xg,an as Yg,ao as ot,ap as jg,aq as Nl,ar as Qg,as as bo,at as Sl,au as sn,av as ju,aw as Zg,ax as Jg,ay as vl,az as q,aA as em,aB as tm,aC as No,aD as js,aE as sm,aF as nm,aG as rm,aH as im,aI as om,aJ as am,aK as um,aL as cm,aM as lm,aN as dm,aO as hm,aP as pm,aQ as fm,aR as gm,aS as jn,aT as Qn,aU as mm,aV as oi,aW as ai,aX as ui,aY as ys,aZ as ym,a_ as ci,a$ as ca,b0 as li,b1 as So,b2 as bt,b3 as Al,b4 as Rl,b5 as Cl,b6 as El,b7 as wl,b8 as Ml,b9 as Bl,ba as Fl,bb as Ul,bc as Gs,bd as Pl,be as Dl,bf as Ll,bg as Il,bh as Vl,bi as Gl,bj as Ol,bk as kl,bl as zl,bm as Wl,bn as $l,bo as xm,bp as Tm,bq as _m,br as Hl,bs as $r,bt as Hr,A as qr,bu as ql,bv as Kl,bw as Xl,bx as Yl,by as jl,bz as Ql,bA as Zl,bB as Jl,bC as bm,bD as Nm,bE as ed,bF as An,bG as Rn,bH as Ws,bI as Sm,bJ as la,bK as vm,bL as Am,bM as da,bN as ha,bO as pa,bP as fa,bQ as Li,bR as Pr,bS as Dr,bT as Lr,bU as Qu,bV as Zu,bW as Ju,bX as ec,bY as tc,bZ as vo,b_ as Ao,b$ as Ro,c0 as Co,c1 as Eo,c2 as wo,c3 as Mo,c4 as Bo,c5 as Fo,c6 as Uo,c7 as Po,c8 as Do,c9 as Lo,ca as Io,cb as Vo,cc as Go,cd as Ii,ce as Rm,cf as sc,cg as nc,ch as rc,ci as Cm,cj as Em,ck as wm,cl as Mm,cm as Bm,cn as Fm,co as Um,cp as Pm,cq as Dm,cr as Lm,cs as Im,ct as Vm,cu as Gm,cv as Om,cw as km,cx as zm,cy as Wm,cz as $m,cA as Hm,cB as qm,cC as Km,cD as td,cE as Xm}from"./C-SOZ1Oi.js";import{cF as RR,cG as CR,cH as ER,cI as wR,cJ as MR,cK as BR,cL as FR,cM as UR,cN as PR,cO as DR,cP as LR,cQ as IR,cR as VR,cS as GR,cT as OR,cU as kR,cV as zR,cW as WR,cX as $R,cY as HR,cZ as qR,c_ as KR,c$ as XR,d0 as YR,d1 as jR,d2 as QR,d3 as ZR,d4 as JR,d5 as e0,d6 as t0,d7 as s0,d8 as n0,d9 as r0,da as i0,db as o0,dc as a0,dd as u0,de as c0,df as l0,dg as d0,dh as h0,di as p0,dj as f0,dk as g0,dl as m0,dm as y0,dn as x0,dp as T0,dq as _0,dr as b0,ds as N0,dt as S0,du as v0,dv as A0,dw as R0,dx as C0,dy as E0,dz as w0,dA as M0,dB as B0,dC as F0,dD as U0,dE as P0,dF as D0,dG as L0,dH as I0,dI as V0,dJ as G0,dK as O0,dL as k0,dM as z0,dN as W0,dO as $0,dP as H0,dQ as q0,dR as K0,dS as X0,dT as Y0,I as j0,dU as Q0,dV as Z0,dW as J0,dX as eC,dY as tC,dZ as sC,d_ as nC,d$ as rC,e0 as iC,e1 as oC,e2 as aC,e3 as uC,e4 as cC,e5 as lC,e6 as dC,e7 as hC,e8 as pC,e9 as fC,ea as gC,eb as mC,ec as yC,ed as xC,ee as TC,ef as _C,eg as bC,eh as NC,ei as SC,ej as vC,ek as AC,el as RC,em as CC,en as EC,eo as wC,ep as MC,eq as BC,er as FC,es as UC,et as PC,eu as DC,P as LC,ev as IC,ew as VC,ex as GC,ey as OC,ez as kC,eA as zC,eB as WC,eC as $C,eD as HC,eE as qC,eF as KC,eG as XC,eH as YC,eI as jC,eJ as QC,eK as ZC,eL as JC,eM as eE,eN as tE,eO as sE,eP as nE,eQ as rE,eR as iE,eS as oE,eT as aE,eU as uE,eV as cE,eW as lE,eX as dE,eY as hE,eZ as pE,e_ as fE,e$ as gE,f0 as mE,f1 as yE,f2 as xE,f3 as TE,f4 as _E,f5 as bE,f6 as NE,f7 as SE,f8 as vE,f9 as AE,fa as RE,fb as CE,fc as EE,fd as wE,fe as ME,ff as BE,fg as FE,fh as UE,fi as PE,fj as DE,fk as LE,fl as IE,fm as VE,fn as GE,fo as OE,fp as kE,fq as zE,fr as WE,fs as $E,ft as HE,fu as qE,fv as KE,fw as XE,fx as YE,fy as jE,fz as QE,fA as ZE}from"./C-SOZ1Oi.js";/** - * @license - * Copyright 2010-2024 Three.js Authors - * SPDX-License-Identifier: MIT - */const Ym=["alphaMap","alphaTest","anisotropy","anisotropyMap","anisotropyRotation","aoMap","attenuationColor","attenuationDistance","bumpMap","clearcoat","clearcoatMap","clearcoatNormalMap","clearcoatNormalScale","clearcoatRoughness","color","dispersion","displacementMap","emissive","emissiveMap","envMap","gradientMap","ior","iridescence","iridescenceIOR","iridescenceMap","iridescenceThicknessMap","lightMap","map","matcap","metalness","metalnessMap","normalMap","normalScale","opacity","roughness","roughnessMap","sheen","sheenColor","sheenColorMap","sheenRoughnessMap","shininess","specular","specularColor","specularColorMap","specularIntensity","specularIntensityMap","specularMap","thickness","transmission","transmissionMap"];class jm{constructor(e){this.renderObjects=new WeakMap,this.hasNode=this.containsNode(e),this.hasAnimation=e.object.isSkinnedMesh===!0,this.refreshUniforms=Ym,this.renderId=0}firstInitialization(e){return this.renderObjects.has(e)===!1?(this.getRenderObjectData(e),!0):!1}getRenderObjectData(e){let t=this.renderObjects.get(e);if(t===void 0){const{geometry:s,material:n,object:r}=e;if(t={material:this.getMaterialData(n),geometry:{attributes:this.getAttributesData(s.attributes),indexVersion:s.index?s.index.version:null,drawRange:{start:s.drawRange.start,count:s.drawRange.count}},worldMatrix:r.matrixWorld.clone()},r.center&&(t.center=r.center.clone()),r.morphTargetInfluences&&(t.morphTargetInfluences=r.morphTargetInfluences.slice()),e.bundle!==null&&(t.version=e.bundle.version),t.material.transmission>0){const{width:i,height:a}=e.context;t.bufferWidth=i,t.bufferHeight=a}this.renderObjects.set(e,t)}return t}getAttributesData(e){const t={};for(const s in e){const n=e[s];t[s]={version:n.version}}return t}containsNode(e){const t=e.material;for(const s in t)if(t[s]&&t[s].isNode)return!0;return e.renderer.nodes.modelViewMatrix!==null||e.renderer.nodes.modelNormalViewMatrix!==null}getMaterialData(e){const t={};for(const s of this.refreshUniforms){const n=e[s];n!=null&&(typeof n=="object"&&n.clone!==void 0?n.isTexture===!0?t[s]={id:n.id,version:n.version}:t[s]=n.clone():t[s]=n)}return t}equals(e){const{object:t,material:s,geometry:n}=e,r=this.getRenderObjectData(e);if(r.worldMatrix.equals(t.matrixWorld)!==!0)return r.worldMatrix.copy(t.matrixWorld),!1;const i=r.material;for(const m in i){const x=i[m],N=s[m];if(x.equals!==void 0){if(x.equals(N)===!1)return x.copy(N),!1}else if(N.isTexture===!0){if(x.id!==N.id||x.version!==N.version)return x.id=N.id,x.version=N.version,!1}else if(x!==N)return i[m]=N,!1}if(i.transmission>0){const{width:m,height:x}=e.context;if(r.bufferWidth!==m||r.bufferHeight!==x)return r.bufferWidth=m,r.bufferHeight=x,!1}const a=r.geometry,u=n.attributes,c=a.attributes,l=Object.keys(c),d=Object.keys(u);if(l.length!==d.length)return r.geometry.attributes=this.getAttributesData(u),!1;for(const m of l){const x=c[m],N=u[m];if(N===void 0)return delete c[m],!1;if(x.version!==N.version)return x.version=N.version,!1}const h=n.index,p=a.indexVersion,f=h?h.version:null;if(p!==f)return a.indexVersion=f,!1;if(a.drawRange.start!==n.drawRange.start||a.drawRange.count!==n.drawRange.count)return a.drawRange.start=n.drawRange.start,a.drawRange.count=n.drawRange.count,!1;if(r.morphTargetInfluences){let m=!1;for(let x=0;x>>16,2246822507),t^=Math.imul(s^s>>>13,3266489909),s=Math.imul(s^s>>>16,2246822507),s^=Math.imul(t^t>>>13,3266489909),4294967296*(2097151&s)+(t>>>0)}const ga=o=>In(o),Zn=o=>In(o),di=(...o)=>In(o);function ma(o,e=!1){const t=[];o.isNode===!0&&(t.push(o.id),o=o.getSelf());for(const{property:s,childNode:n}of Vn(o))t.push(t,In(s.slice(0,-4)),n.getCacheKey(e));return In(t)}function*Vn(o,e=!1){for(const t in o){if(t.startsWith("_")===!0)continue;const s=o[t];if(Array.isArray(s)===!0)for(let n=0;ne.charCodeAt(0)).buffer}var cR=Object.freeze({__proto__:null,arrayBufferToBase64:Na,base64ToArrayBuffer:Sa,getCacheKey:ma,getDataFromObject:ba,getLengthFromType:Ta,getNodeChildren:Vn,getTypeFromLength:ya,getTypedArrayFromType:xa,getValueFromType:_a,getValueType:Ot,hash:di,hashArray:Zn,hashString:ga});const Oo={VERTEX:"vertex",FRAGMENT:"fragment"},W={NONE:"none",FRAME:"frame",RENDER:"render",OBJECT:"object"},Zm={BOOLEAN:"bool",INTEGER:"int",FLOAT:"float",VECTOR2:"vec2",VECTOR3:"vec3",VECTOR4:"vec4",MATRIX2:"mat2",MATRIX3:"mat3",MATRIX4:"mat4"},Ve={READ_ONLY:"readOnly",WRITE_ONLY:"writeOnly",READ_WRITE:"readWrite"},sd=["fragment","vertex"],ko=["setup","analyze","generate"],zo=[...sd,"compute"],As=["x","y","z","w"];let Jm=0;class O extends gl{static get type(){return"Node"}constructor(e=null){super(),this.nodeType=e,this.updateType=W.NONE,this.updateBeforeType=W.NONE,this.updateAfterType=W.NONE,this.uuid=ml.generateUUID(),this.version=0,this.global=!1,this.isNode=!0,this._cacheKey=null,this._cacheKeyVersion=0,Object.defineProperty(this,"id",{value:Jm++})}set needsUpdate(e){e===!0&&this.version++}get type(){return this.constructor.type}onUpdate(e,t){return this.updateType=t,this.update=e.bind(this.getSelf()),this}onFrameUpdate(e){return this.onUpdate(e,W.FRAME)}onRenderUpdate(e){return this.onUpdate(e,W.RENDER)}onObjectUpdate(e){return this.onUpdate(e,W.OBJECT)}onReference(e){return this.updateReference=e.bind(this.getSelf()),this}getSelf(){return this.self||this}updateReference(){return this}isGlobal(){return this.global}*getChildren(){for(const{childNode:e}of Vn(this))yield e}dispose(){this.dispatchEvent({type:"dispose"})}traverse(e){e(this);for(const t of this.getChildren())t.traverse(e)}getCacheKey(e=!1){return e=e||this.version!==this._cacheKeyVersion,(e===!0||this._cacheKey===null)&&(this._cacheKey=di(ma(this,e),this.customCacheKey()),this._cacheKeyVersion=this.version),this._cacheKey}customCacheKey(){return 0}getScope(){return this}getHash(){return this.uuid}getUpdateType(){return this.updateType}getUpdateBeforeType(){return this.updateBeforeType}getUpdateAfterType(){return this.updateAfterType}getElementType(e){const t=this.getNodeType(e);return e.getElementType(t)}getNodeType(e){const t=e.getNodeProperties(this);return t.outputNode?t.outputNode.getNodeType(e):this.nodeType}getShared(e){const t=this.getHash(e);return e.getNodeFromHash(t)||this}setup(e){const t=e.getNodeProperties(this);let s=0;for(const n of this.getChildren())t["node"+s++]=n;return t.outputNode||null}analyze(e){if(e.increaseUsage(this)===1){const s=e.getNodeProperties(this);for(const n of Object.values(s))n&&n.isNode===!0&&n.build(e)}}generate(e,t){const{outputNode:s}=e.getNodeProperties(this);if(s&&s.isNode===!0)return s.build(e,t)}updateBefore(){console.warn("Abstract function.")}updateAfter(){console.warn("Abstract function.")}update(){console.warn("Abstract function.")}build(e,t=null){const s=this.getShared(e);if(this!==s)return s.build(e,t);e.addNode(this),e.addChain(this);let n=null;const r=e.getBuildStage();if(r==="setup"){this.updateReference(e);const i=e.getNodeProperties(this);if(i.initialized!==!0){i.initialized=!0;const a=this.setup(e),u=a&&a.isNode===!0;for(const c of Object.values(i))c&&c.isNode===!0&&c.build(e);u&&a.build(e),i.outputNode=a}}else if(r==="analyze")this.analyze(e);else if(r==="generate")if(this.generate.length===1){const a=this.getNodeType(e),u=e.getDataFromNode(this);n=u.snippet,n===void 0?(n=this.generate(e)||"",u.snippet=n):u.flowCodes!==void 0&&e.context.nodeBlock!==void 0&&e.addFlowCodeHierarchy(this,e.context.nodeBlock),n=e.format(n,a,t)}else n=this.generate(e,t)||"";return e.removeChain(this),e.addSequentialNode(this),n}getSerializeChildren(){return Vn(this)}serialize(e){const t=this.getSerializeChildren(),s={};for(const{property:n,index:r,childNode:i}of t)r!==void 0?(s[n]===void 0&&(s[n]=Number.isInteger(r)?[]:{}),s[n][r]=i.toJSON(e.meta).uuid):s[n]=i.toJSON(e.meta).uuid;Object.keys(s).length>0&&(e.inputNodes=s)}deserialize(e){if(e.inputNodes!==void 0){const t=e.meta.nodes;for(const s in e.inputNodes)if(Array.isArray(e.inputNodes[s])){const n=[];for(const r of e.inputNodes[s])n.push(t[r]);this[s]=n}else if(typeof e.inputNodes[s]=="object"){const n={};for(const r in e.inputNodes[s]){const i=e.inputNodes[s][r];n[r]=t[i]}this[s]=n}else{const n=e.inputNodes[s];this[s]=t[n]}}}toJSON(e){const{uuid:t,type:s}=this,n=e===void 0||typeof e=="string";n&&(e={textures:{},images:{},nodes:{}});let r=e.nodes[t];r===void 0&&(r={uuid:t,type:s,meta:e,metadata:{version:4.6,type:"Node",generator:"Node.toJSON"}},n!==!0&&(e.nodes[r.uuid]=r),this.serialize(r),delete r.meta);function i(a){const u=[];for(const c in a){const l=a[c];delete l.metadata,u.push(l)}return u}if(n){const a=i(e.textures),u=i(e.images),c=i(e.nodes);a.length>0&&(r.textures=a),u.length>0&&(r.images=u),c.length>0&&(r.nodes=c)}return r}}class Rs extends O{static get type(){return"ArrayElementNode"}constructor(e,t){super(),this.node=e,this.indexNode=t,this.isArrayElementNode=!0}getNodeType(e){return this.node.getElementType(e)}generate(e){const t=this.node.build(e),s=this.indexNode.build(e,"uint");return`${t}[ ${s} ]`}}class nd extends O{static get type(){return"ConvertNode"}constructor(e,t){super(),this.node=e,this.convertTo=t}getNodeType(e){const t=this.node.getNodeType(e);let s=null;for(const n of this.convertTo.split("|"))(s===null||e.getTypeLength(t)===e.getTypeLength(n))&&(s=n);return s}serialize(e){super.serialize(e),e.convertTo=this.convertTo}deserialize(e){super.deserialize(e),this.convertTo=e.convertTo}generate(e,t){const s=this.node,n=this.getNodeType(e),r=s.build(e,n);return e.format(r,n,t)}}class be extends O{static get type(){return"TempNode"}constructor(e=null){super(e),this.isTempNode=!0}hasDependencies(e){return e.getDataFromNode(this).usageCount>1}build(e,t){if(e.getBuildStage()==="generate"){const n=e.getVectorType(this.getNodeType(e,t)),r=e.getDataFromNode(this);if(r.propertyName!==void 0)return e.format(r.propertyName,n,t);if(n!=="void"&&t!=="void"&&this.hasDependencies(e)){const i=super.build(e,n),a=e.getVarFromNode(this,null,n),u=e.getPropertyName(a);return e.addLineFlowCode(`${u} = ${i}`,this),r.snippet=i,r.propertyName=u,e.format(r.propertyName,n,t)}}return super.build(e,t)}}class ey extends be{static get type(){return"JoinNode"}constructor(e=[],t=null){super(t),this.nodes=e}getNodeType(e){return this.nodeType!==null?e.getVectorType(this.nodeType):e.getTypeFromLength(this.nodes.reduce((t,s)=>t+e.getTypeLength(s.getNodeType(e)),0))}generate(e,t){const s=this.getNodeType(e),n=this.nodes,r=e.getComponentType(s),i=[];for(const u of n){let c=u.build(e);const l=e.getComponentType(u.getNodeType(e));l!==r&&(c=e.format(c,l,r)),i.push(c)}const a=`${e.getType(s)}( ${i.join(", ")} )`;return e.format(a,s,t)}}const ty=As.join("");class Wo extends O{static get type(){return"SplitNode"}constructor(e,t="x"){super(),this.node=e,this.components=t,this.isSplitNode=!0}getVectorLength(){let e=this.components.length;for(const t of this.components)e=Math.max(As.indexOf(t)+1,e);return e}getComponentType(e){return e.getComponentType(this.node.getNodeType(e))}getNodeType(e){return e.getTypeFromLength(this.components.length,this.getComponentType(e))}generate(e,t){const s=this.node,n=e.getTypeLength(s.getNodeType(e));let r=null;if(n>1){let i=null;this.getVectorLength()>=n&&(i=e.getTypeFromLength(this.getVectorLength(),this.getComponentType(e)));const u=s.build(e,i);this.components.length===n&&this.components===ty.slice(0,this.components.length)?r=e.format(u,i,t):r=e.format(`${u}.${this.components}`,this.getNodeType(e),t)}else r=s.build(e,t);return r}serialize(e){super.serialize(e),e.components=this.components}deserialize(e){super.deserialize(e),this.components=e.components}}class sy extends be{static get type(){return"SetNode"}constructor(e,t,s){super(),this.sourceNode=e,this.components=t,this.targetNode=s}getNodeType(e){return this.sourceNode.getNodeType(e)}generate(e){const{sourceNode:t,components:s,targetNode:n}=this,r=this.getNodeType(e),i=e.getComponentType(n.getNodeType(e)),a=e.getTypeFromLength(s.length,i),u=n.build(e,a),c=t.build(e,r),l=e.getTypeLength(r),d=[];for(let h=0;ho.replace(/r|s/g,"x").replace(/g|t/g,"y").replace(/b|p/g,"z").replace(/a|q/g,"w"),oc=o=>rd(o).split("").sort().join(""),id={setup(o,e){const t=e.shift();return o(Jn(t),...e)},get(o,e,t){if(typeof e=="string"&&o[e]===void 0){if(o.isStackNode!==!0&&e==="assign")return(...s)=>(nn.assign(t,...s),t);if($s.has(e)){const s=$s.get(e);return o.isStackNode?(...n)=>t.add(s(...n)):(...n)=>s(t,...n)}else{if(e==="self")return o;if(e.endsWith("Assign")&&$s.has(e.slice(0,e.length-6))){const s=$s.get(e.slice(0,e.length-6));return o.isStackNode?(...n)=>t.assign(n[0],s(...n)):(...n)=>t.assign(s(t,...n))}else{if(/^[xyzwrgbastpq]{1,4}$/.test(e)===!0)return e=rd(e),E(new Wo(t,e));if(/^set[XYZWRGBASTPQ]{1,4}$/.test(e)===!0)return e=oc(e.slice(3).toLowerCase()),s=>E(new sy(o,e,s));if(/^flip[XYZWRGBASTPQ]{1,4}$/.test(e)===!0)return e=oc(e.slice(4).toLowerCase()),()=>E(new ny(E(o),e));if(e==="width"||e==="height"||e==="depth")return e==="width"?e="x":e==="height"?e="y":e==="depth"&&(e="z"),E(new Wo(o,e));if(/^\d+$/.test(e)===!0)return E(new Rs(t,new yt(Number(e),"uint")))}}}return Reflect.get(o,e,t)},set(o,e,t,s){return typeof e=="string"&&o[e]===void 0&&(/^[xyzwrgbastpq]{1,4}$/.test(e)===!0||e==="width"||e==="height"||e==="depth"||/^\d+$/.test(e)===!0)?(s[e].assign(t),!0):Reflect.set(o,e,t,s)}},Vi=new WeakMap,ac=new WeakMap,ry=function(o,e=null){const t=Ot(o);if(t==="node"){let s=Vi.get(o);return s===void 0&&(s=new Proxy(o,id),Vi.set(o,s),Vi.set(s,s)),s}else{if(e===null&&(t==="float"||t==="boolean")||t&&t!=="shader"&&t!=="string")return E($o(o,e));if(t==="shader")return b(o)}return o},iy=function(o,e=null){for(const t in o)o[t]=E(o[t],e);return o},oy=function(o,e=null){const t=o.length;for(let s=0;sE(s!==null?Object.assign(r,s):r);return e===null?(...r)=>n(new o(...xs(r))):t!==null?(t=E(t),(...r)=>n(new o(e,...xs(r),t))):(...r)=>n(new o(e,...xs(r)))},uy=function(o,...e){return E(new o(...xs(e)))};class cy extends O{constructor(e,t){super(),this.shaderNode=e,this.inputNodes=t}getNodeType(e){return this.shaderNode.nodeType||this.getOutputNode(e).getNodeType(e)}call(e){const{shaderNode:t,inputNodes:s}=this,n=e.getNodeProperties(t);if(n.onceOutput)return n.onceOutput;let r=null;if(t.layout){let i=ac.get(e.constructor);i===void 0&&(i=new WeakMap,ac.set(e.constructor,i));let a=i.get(t);a===void 0&&(a=E(e.buildFunctionNode(t)),i.set(t,a)),e.currentFunctionNode!==null&&e.currentFunctionNode.includes.push(a),r=E(a.call(s))}else{const i=t.jsFunc,a=s!==null?i(s,e):i(e);r=E(a)}return t.once&&(n.onceOutput=r),r}getOutputNode(e){const t=e.getNodeProperties(this);return t.outputNode===null&&(t.outputNode=this.setupOutput(e)),t.outputNode}setup(e){return this.getOutputNode(e)}setupOutput(e){return e.addStack(),e.stack.outputNode=this.call(e),e.removeStack()}generate(e,t){return this.getOutputNode(e).build(e,t)}}class ly extends O{constructor(e,t){super(t),this.jsFunc=e,this.layout=null,this.global=!0,this.once=!1}setLayout(e){return this.layout=e,this}call(e=null){return Jn(e),E(new cy(this,e))}setup(){return this.call()}}const dy=[!1,!0],hy=[0,1,2,3],py=[-1,-2],od=[.5,1.5,1/3,1e-6,1e6,Math.PI,Math.PI*2,1/Math.PI,2/Math.PI,1/(Math.PI*2),Math.PI/2],Aa=new Map;for(const o of dy)Aa.set(o,new yt(o));const Ra=new Map;for(const o of hy)Ra.set(o,new yt(o,"uint"));const Ca=new Map([...Ra].map(o=>new yt(o.value,"int")));for(const o of py)Ca.set(o,new yt(o,"int"));const hi=new Map([...Ca].map(o=>new yt(o.value)));for(const o of od)hi.set(o,new yt(o));for(const o of od)hi.set(-o,new yt(-o));const pi={bool:Aa,uint:Ra,ints:Ca,float:hi},uc=new Map([...Aa,...hi]),$o=(o,e)=>uc.has(o)?uc.get(o):o.isNode===!0?o:new yt(o,e),fy=o=>{try{return o.getNodeType()}catch{return}},ve=function(o,e=null){return(...t)=>{if((t.length===0||!["bool","float","int","uint"].includes(o)&&t.every(n=>typeof n!="object"))&&(t=[_a(o,...t)]),t.length===1&&e!==null&&e.has(t[0]))return E(e.get(t[0]));if(t.length===1){const n=$o(t[0],o);return fy(n)===o?E(n):E(new nd(n,o))}const s=t.map(n=>$o(n));return E(new ey(s,o))}},Gn=o=>typeof o=="object"&&o!==null?o.value:o,ad=o=>o!=null?o.nodeType||o.convertTo||(typeof o=="string"?o:null):null;function Cn(o,e){return new Proxy(new ly(o,e),id)}const E=(o,e=null)=>ry(o,e),Jn=(o,e=null)=>new iy(o,e),xs=(o,e=null)=>new oy(o,e),C=(...o)=>new ay(...o),U=(...o)=>new uy(...o),b=(o,e)=>{const t=new Cn(o,e),s=(...n)=>{let r;return Jn(n),n[0]&&n[0].isNode?r=[...n]:r=n[0],t.call(r)};return s.shaderNode=t,s.setLayout=n=>(t.setLayout(n),s),s.once=()=>(t.once=!0,s),s},gy=(...o)=>(console.warn("TSL.ShaderNode: tslFn() has been renamed to Fn()."),b(...o));R("toGlobal",o=>(o.global=!0,o));const On=o=>{nn=o},Ea=()=>nn,z=(...o)=>nn.If(...o);function ud(o){return nn&&nn.add(o),o}R("append",ud);const cd=new ve("color"),g=new ve("float",pi.float),y=new ve("int",pi.ints),D=new ve("uint",pi.uint),Ht=new ve("bool",pi.bool),M=new ve("vec2"),Ae=new ve("ivec2"),ld=new ve("uvec2"),dd=new ve("bvec2"),T=new ve("vec3"),hd=new ve("ivec3"),dn=new ve("uvec3"),wa=new ve("bvec3"),P=new ve("vec4"),pd=new ve("ivec4"),fd=new ve("uvec4"),gd=new ve("bvec4"),fi=new ve("mat2"),Oe=new ve("mat3"),Ts=new ve("mat4"),my=(o="")=>E(new yt(o,"string")),yy=o=>E(new yt(o,"ArrayBuffer"));R("toColor",cd);R("toFloat",g);R("toInt",y);R("toUint",D);R("toBool",Ht);R("toVec2",M);R("toIVec2",Ae);R("toUVec2",ld);R("toBVec2",dd);R("toVec3",T);R("toIVec3",hd);R("toUVec3",dn);R("toBVec3",wa);R("toVec4",P);R("toIVec4",pd);R("toUVec4",fd);R("toBVec4",gd);R("toMat2",fi);R("toMat3",Oe);R("toMat4",Ts);const md=C(Rs),yd=(o,e)=>E(new nd(E(o),e)),xy=(o,e)=>E(new Wo(E(o),e));R("element",md);R("convert",yd);class xd extends O{static get type(){return"UniformGroupNode"}constructor(e,t=!1,s=1){super("string"),this.name=e,this.shared=t,this.order=s,this.isUniformGroup=!0}serialize(e){super.serialize(e),e.name=this.name,e.version=this.version,e.shared=this.shared}deserialize(e){super.deserialize(e),this.name=e.name,this.version=e.version,this.shared=e.shared}}const Td=o=>new xd(o),Ma=(o,e=0)=>new xd(o,!0,e),_d=Ma("frame"),k=Ma("render"),Ba=Td("object");class er extends va{static get type(){return"UniformNode"}constructor(e,t=null){super(e,t),this.isUniformNode=!0,this.name="",this.groupNode=Ba}label(e){return this.name=e,this}setGroup(e){return this.groupNode=e,this}getGroup(){return this.groupNode}getUniformHash(e){return this.getHash(e)}onUpdate(e,t){const s=this.getSelf();return e=e.bind(s),super.onUpdate(n=>{const r=e(n,s);r!==void 0&&(this.value=r)},t)}generate(e,t){const s=this.getNodeType(e),n=this.getUniformHash(e);let r=e.getNodeFromHash(n);r===void 0&&(e.setHashNode(this,n),r=this);const i=r.getInputType(e),a=e.getUniformFromNode(r,i,e.shaderStage,this.name||e.context.label),u=e.getPropertyName(a);return e.context.label!==void 0&&delete e.context.label,e.format(u,s,t)}}const V=(o,e)=>{const t=ad(e||o),s=o&&o.isNode===!0?o.node&&o.node.value||o.value:o;return E(new er(s,t))};class se extends O{static get type(){return"PropertyNode"}constructor(e,t=null,s=!1){super(e),this.name=t,this.varying=s,this.isPropertyNode=!0}getHash(e){return this.name||super.getHash(e)}isGlobal(){return!0}generate(e){let t;return this.varying===!0?(t=e.getVaryingFromNode(this,this.name),t.needsInterpolation=!0):t=e.getVarFromNode(this,this.name),e.getPropertyName(t)}}const Fa=(o,e)=>E(new se(o,e)),et=(o,e)=>E(new se(o,e,!0)),ee=U(se,"vec4","DiffuseColor"),Ho=U(se,"vec3","EmissiveColor"),Nt=U(se,"float","Roughness"),kn=U(se,"float","Metalness"),Kr=U(se,"float","Clearcoat"),zn=U(se,"float","ClearcoatRoughness"),fs=U(se,"vec3","Sheen"),gi=U(se,"float","SheenRoughness"),mi=U(se,"float","Iridescence"),Ua=U(se,"float","IridescenceIOR"),Pa=U(se,"float","IridescenceThickness"),Xr=U(se,"float","AlphaT"),Zt=U(se,"float","Anisotropy"),En=U(se,"vec3","AnisotropyT"),_s=U(se,"vec3","AnisotropyB"),We=U(se,"color","SpecularColor"),Wn=U(se,"float","SpecularF90"),Yr=U(se,"float","Shininess"),$n=U(se,"vec4","Output"),bs=U(se,"float","dashSize"),Hn=U(se,"float","gapSize"),Ty=U(se,"float","pointWidth"),wn=U(se,"float","IOR"),jr=U(se,"float","Transmission"),Da=U(se,"float","Thickness"),La=U(se,"float","AttenuationDistance"),Ia=U(se,"color","AttenuationColor"),Va=U(se,"float","Dispersion");class _y extends be{static get type(){return"AssignNode"}constructor(e,t){super(),this.targetNode=e,this.sourceNode=t}hasDependencies(){return!1}getNodeType(e,t){return t!=="void"?this.targetNode.getNodeType(e):"void"}needsSplitAssign(e){const{targetNode:t}=this;if(e.isAvailable("swizzleAssign")===!1&&t.isSplitNode&&t.components.length>1){const s=e.getTypeLength(t.node.getNodeType(e));return As.join("").slice(0,s)!==t.components}return!1}generate(e,t){const{targetNode:s,sourceNode:n}=this,r=this.needsSplitAssign(e),i=s.getNodeType(e),a=s.context({assign:!0}).build(e),u=n.build(e,i),c=n.getNodeType(e),l=e.getDataFromNode(this);let d;if(l.initialized===!0)t!=="void"&&(d=a);else if(r){const h=e.getVarFromNode(this,null,i),p=e.getPropertyName(h);e.addLineFlowCode(`${p} = ${u}`,this);const f=s.node.context({assign:!0}).build(e);for(let m=0;m{const l=c.type,d=l==="pointer";let h;return d?h="&"+u.build(e):h=u.build(e,l),h};if(Array.isArray(r))for(let u=0;u(e=e.length>1||e[0]&&e[0].isNode===!0?xs(e):Jn(e[0]),E(new by(E(o),e)));R("call",Nd);class pe extends be{static get type(){return"OperatorNode"}constructor(e,t,s,...n){if(super(),n.length>0){let r=new pe(e,t,s);for(let i=0;i>"||s==="<<")return e.getIntegerType(i);if(s==="!"||s==="=="||s==="&&"||s==="||"||s==="^^")return"bool";if(s==="<"||s===">"||s==="<="||s===">="){const u=t?e.getTypeLength(t):Math.max(e.getTypeLength(i),e.getTypeLength(a));return u>1?`bvec${u}`:"bool"}else return i==="float"&&e.isMatrix(a)?a:e.isMatrix(i)&&e.isVector(a)?e.getVectorFromMatrix(i):e.isVector(i)&&e.isMatrix(a)?e.getVectorFromMatrix(a):e.getTypeLength(a)>e.getTypeLength(i)?a:i}generate(e,t){const s=this.op,n=this.aNode,r=this.bNode,i=this.getNodeType(e,t);let a=null,u=null;i!=="void"?(a=n.getNodeType(e),u=typeof r<"u"?r.getNodeType(e):null,s==="<"||s===">"||s==="<="||s===">="||s==="=="?e.isVector(a)?u=a:a!==u&&(a=u="float"):s===">>"||s==="<<"?(a=i,u=e.changeComponentType(u,"uint")):e.isMatrix(a)&&e.isVector(u)?u=e.getVectorFromMatrix(a):e.isVector(a)&&e.isMatrix(u)?a=e.getVectorFromMatrix(u):a=u=i):a=u=i;const c=n.build(e,a),l=typeof r<"u"?r.build(e,u):null,d=e.getTypeLength(t),h=e.getFunctionOperator(s);if(t!=="void")return s==="<"&&d>1?e.useComparisonMethod?e.format(`${e.getMethod("lessThan",t)}( ${c}, ${l} )`,i,t):e.format(`( ${c} < ${l} )`,i,t):s==="<="&&d>1?e.useComparisonMethod?e.format(`${e.getMethod("lessThanEqual",t)}( ${c}, ${l} )`,i,t):e.format(`( ${c} <= ${l} )`,i,t):s===">"&&d>1?e.useComparisonMethod?e.format(`${e.getMethod("greaterThan",t)}( ${c}, ${l} )`,i,t):e.format(`( ${c} > ${l} )`,i,t):s===">="&&d>1?e.useComparisonMethod?e.format(`${e.getMethod("greaterThanEqual",t)}( ${c}, ${l} )`,i,t):e.format(`( ${c} >= ${l} )`,i,t):s==="!"||s==="~"?e.format(`(${s}${c})`,a,t):h?e.format(`${h}( ${c}, ${l} )`,i,t):e.format(`( ${c} ${s} ${l} )`,i,t);if(a!=="void")return h?e.format(`${h}( ${c}, ${l} )`,i,t):e.format(`${c} ${s} ${l}`,i,t)}serialize(e){super.serialize(e),e.op=this.op}deserialize(e){super.deserialize(e),this.op=e.op}}const _e=C(pe,"+"),Y=C(pe,"-"),$=C(pe,"*"),gt=C(pe,"/"),Ga=C(pe,"%"),Sd=C(pe,"=="),vd=C(pe,"!="),Ad=C(pe,"<"),Oa=C(pe,">"),Rd=C(pe,"<="),Cd=C(pe,">="),Ed=C(pe,"&&"),wd=C(pe,"||"),Md=C(pe,"!"),Bd=C(pe,"^^"),Fd=C(pe,"&"),Ud=C(pe,"~"),Pd=C(pe,"|"),Dd=C(pe,"^"),Ld=C(pe,"<<"),Id=C(pe,">>");R("add",_e);R("sub",Y);R("mul",$);R("div",gt);R("modInt",Ga);R("equal",Sd);R("notEqual",vd);R("lessThan",Ad);R("greaterThan",Oa);R("lessThanEqual",Rd);R("greaterThanEqual",Cd);R("and",Ed);R("or",wd);R("not",Md);R("xor",Bd);R("bitAnd",Fd);R("bitNot",Ud);R("bitOr",Pd);R("bitXor",Dd);R("shiftLeft",Ld);R("shiftRight",Id);const Vd=(...o)=>(console.warn("TSL.OperatorNode: .remainder() has been renamed to .modInt()."),Ga(...o));R("remainder",Vd);class S extends be{static get type(){return"MathNode"}constructor(e,t,s=null,n=null){super(),this.method=e,this.aNode=t,this.bNode=s,this.cNode=n}getInputType(e){const t=this.aNode.getNodeType(e),s=this.bNode?this.bNode.getNodeType(e):null,n=this.cNode?this.cNode.getNodeType(e):null,r=e.isMatrix(t)?0:e.getTypeLength(t),i=e.isMatrix(s)?0:e.getTypeLength(s),a=e.isMatrix(n)?0:e.getTypeLength(n);return r>i&&r>a?t:i>a?s:a>r?n:t}getNodeType(e){const t=this.method;return t===S.LENGTH||t===S.DISTANCE||t===S.DOT?"float":t===S.CROSS?"vec3":t===S.ALL?"bool":t===S.EQUALS?e.changeComponentType(this.aNode.getNodeType(e),"bool"):t===S.MOD?this.aNode.getNodeType(e):this.getInputType(e)}generate(e,t){let s=this.method;const n=this.getNodeType(e),r=this.getInputType(e),i=this.aNode,a=this.bNode,u=this.cNode,c=e.renderer.coordinateSystem;if(s===S.TRANSFORM_DIRECTION){let l=i,d=a;e.isMatrix(l.getNodeType(e))?d=P(T(d),0):l=P(T(l),0);const h=$(l,d).xyz;return qt(h).build(e,t)}else{if(s===S.NEGATE)return e.format("( - "+i.build(e,r)+" )",n,t);if(s===S.ONE_MINUS)return Y(1,i).build(e,t);if(s===S.RECIPROCAL)return gt(1,i).build(e,t);if(s===S.DIFFERENCE)return oe(Y(i,a)).build(e,t);{const l=[];return s===S.CROSS||s===S.MOD?l.push(i.build(e,n),a.build(e,n)):c===Pn&&s===S.STEP?l.push(i.build(e,e.getTypeLength(i.getNodeType(e))===1?"float":r),a.build(e,r)):c===Pn&&(s===S.MIN||s===S.MAX)||s===S.MOD?l.push(i.build(e,r),a.build(e,e.getTypeLength(a.getNodeType(e))===1?"float":r)):s===S.REFRACT?l.push(i.build(e,r),a.build(e,r),u.build(e,"float")):s===S.MIX?l.push(i.build(e,r),a.build(e,r),u.build(e,e.getTypeLength(u.getNodeType(e))===1?"float":r)):(c===ln&&s===S.ATAN&&a!==null&&(s="atan2"),l.push(i.build(e,r)),a!==null&&l.push(a.build(e,r)),u!==null&&l.push(u.build(e,r))),e.format(`${e.getMethod(s,n)}( ${l.join(", ")} )`,n,t)}}}serialize(e){super.serialize(e),e.method=this.method}deserialize(e){super.deserialize(e),this.method=e.method}}S.ALL="all";S.ANY="any";S.RADIANS="radians";S.DEGREES="degrees";S.EXP="exp";S.EXP2="exp2";S.LOG="log";S.LOG2="log2";S.SQRT="sqrt";S.INVERSE_SQRT="inversesqrt";S.FLOOR="floor";S.CEIL="ceil";S.NORMALIZE="normalize";S.FRACT="fract";S.SIN="sin";S.COS="cos";S.TAN="tan";S.ASIN="asin";S.ACOS="acos";S.ATAN="atan";S.ABS="abs";S.SIGN="sign";S.LENGTH="length";S.NEGATE="negate";S.ONE_MINUS="oneMinus";S.DFDX="dFdx";S.DFDY="dFdy";S.ROUND="round";S.RECIPROCAL="reciprocal";S.TRUNC="trunc";S.FWIDTH="fwidth";S.TRANSPOSE="transpose";S.BITCAST="bitcast";S.EQUALS="equals";S.MIN="min";S.MAX="max";S.MOD="mod";S.STEP="step";S.REFLECT="reflect";S.DISTANCE="distance";S.DIFFERENCE="difference";S.DOT="dot";S.CROSS="cross";S.POW="pow";S.TRANSFORM_DIRECTION="transformDirection";S.MIX="mix";S.CLAMP="clamp";S.REFRACT="refract";S.SMOOTHSTEP="smoothstep";S.FACEFORWARD="faceforward";const Gd=g(1e-6),Ny=g(1e6),Qr=g(Math.PI),Sy=g(Math.PI*2),ka=C(S,S.ALL),Od=C(S,S.ANY),kd=C(S,S.RADIANS),zd=C(S,S.DEGREES),za=C(S,S.EXP),rn=C(S,S.EXP2),yi=C(S,S.LOG),vt=C(S,S.LOG2),Pt=C(S,S.SQRT),Wa=C(S,S.INVERSE_SQRT),At=C(S,S.FLOOR),xi=C(S,S.CEIL),qt=C(S,S.NORMALIZE),Xt=C(S,S.FRACT),st=C(S,S.SIN),It=C(S,S.COS),Wd=C(S,S.TAN),$d=C(S,S.ASIN),Hd=C(S,S.ACOS),$a=C(S,S.ATAN),oe=C(S,S.ABS),qn=C(S,S.SIGN),zt=C(S,S.LENGTH),qd=C(S,S.NEGATE),Kd=C(S,S.ONE_MINUS),Ha=C(S,S.DFDX),qa=C(S,S.DFDY),Xd=C(S,S.ROUND),Yd=C(S,S.RECIPROCAL),Ka=C(S,S.TRUNC),jd=C(S,S.FWIDTH),Qd=C(S,S.TRANSPOSE),vy=C(S,S.BITCAST),Zd=C(S,S.EQUALS),Re=C(S,S.MIN),ue=C(S,S.MAX),Xa=C(S,S.MOD),Ti=C(S,S.STEP),Jd=C(S,S.REFLECT),eh=C(S,S.DISTANCE),th=C(S,S.DIFFERENCE),is=C(S,S.DOT),_i=C(S,S.CROSS),ht=C(S,S.POW),Ya=C(S,S.POW,2),sh=C(S,S.POW,3),nh=C(S,S.POW,4),rh=C(S,S.TRANSFORM_DIRECTION),ih=o=>$(qn(o),ht(oe(o),1/3)),ja=o=>is(o,o),Q=C(S,S.MIX),Et=(o,e=0,t=1)=>E(new S(S.CLAMP,E(o),E(e),E(t))),oh=o=>Et(o),Qa=C(S,S.REFRACT),ct=C(S,S.SMOOTHSTEP),Za=C(S,S.FACEFORWARD),ah=b(([o])=>{const s=43758.5453,n=is(o.xy,M(12.9898,78.233)),r=Xa(n,Qr);return Xt(st(r).mul(s))}),uh=(o,e,t)=>Q(e,t,o),ch=(o,e,t)=>ct(e,t,o),lh=(o,e)=>(console.warn('THREE.TSL: "atan2" is overloaded. Use "atan" instead.'),$a(o,e)),Ay=Za,Ry=Wa;R("all",ka);R("any",Od);R("equals",Zd);R("radians",kd);R("degrees",zd);R("exp",za);R("exp2",rn);R("log",yi);R("log2",vt);R("sqrt",Pt);R("inverseSqrt",Wa);R("floor",At);R("ceil",xi);R("normalize",qt);R("fract",Xt);R("sin",st);R("cos",It);R("tan",Wd);R("asin",$d);R("acos",Hd);R("atan",$a);R("abs",oe);R("sign",qn);R("length",zt);R("lengthSq",ja);R("negate",qd);R("oneMinus",Kd);R("dFdx",Ha);R("dFdy",qa);R("round",Xd);R("reciprocal",Yd);R("trunc",Ka);R("fwidth",jd);R("atan2",lh);R("min",Re);R("max",ue);R("mod",Xa);R("step",Ti);R("reflect",Jd);R("distance",eh);R("dot",is);R("cross",_i);R("pow",ht);R("pow2",Ya);R("pow3",sh);R("pow4",nh);R("transformDirection",rh);R("mix",uh);R("clamp",Et);R("refract",Qa);R("smoothstep",ch);R("faceForward",Za);R("difference",th);R("saturate",oh);R("cbrt",ih);R("transpose",Qd);R("rand",ah);class Cy extends O{static get type(){return"ConditionalNode"}constructor(e,t,s=null){super(),this.condNode=e,this.ifNode=t,this.elseNode=s}getNodeType(e){const{ifNode:t,elseNode:s}=e.getNodeProperties(this);if(t===void 0)return this.setup(e),this.getNodeType(e);const n=t.getNodeType(e);if(s!==null){const r=s.getNodeType(e);if(e.getTypeLength(r)>e.getTypeLength(n))return r}return n}setup(e){const t=this.condNode.cache(),s=this.ifNode.cache(),n=this.elseNode?this.elseNode.cache():null,r=e.context.nodeBlock;e.getDataFromNode(s).parentNodeBlock=r,n!==null&&(e.getDataFromNode(n).parentNodeBlock=r);const i=e.getNodeProperties(this);i.condNode=t,i.ifNode=s.context({nodeBlock:s}),i.elseNode=n?n.context({nodeBlock:n}):null}generate(e,t){const s=this.getNodeType(e),n=e.getDataFromNode(this);if(n.nodeProperty!==void 0)return n.nodeProperty;const{condNode:r,ifNode:i,elseNode:a}=e.getNodeProperties(this),u=t!=="void",c=u?Fa(s).build(e):"";n.nodeProperty=c;const l=r.build(e,"bool");e.addFlowCode(` -${e.tab}if ( ${l} ) { - -`).addFlowTab();let d=i.build(e,s);if(d&&(u?d=c+" = "+d+";":d="return "+d+";"),e.removeFlowTab().addFlowCode(e.tab+" "+d+` - -`+e.tab+"}"),a!==null){e.addFlowCode(` else { - -`).addFlowTab();let h=a.build(e,s);h&&(u?h=c+" = "+h+";":h="return "+h+";"),e.removeFlowTab().addFlowCode(e.tab+" "+h+` - -`+e.tab+`} - -`)}else e.addFlowCode(` - -`);return e.format(c,s,t)}}const Ue=C(Cy);R("select",Ue);const dh=(...o)=>(console.warn("TSL.ConditionalNode: cond() has been renamed to select()."),Ue(...o));R("cond",dh);class hh extends O{static get type(){return"ContextNode"}constructor(e,t={}){super(),this.isContextNode=!0,this.node=e,this.value=t}getScope(){return this.node.getScope()}getNodeType(e){return this.node.getNodeType(e)}analyze(e){this.node.build(e)}setup(e){const t=e.getContext();e.setContext({...e.context,...this.value});const s=this.node.build(e);return e.setContext(t),s}generate(e,t){const s=e.getContext();e.setContext({...e.context,...this.value});const n=this.node.build(e,t);return e.setContext(s),n}}const bi=C(hh),ph=(o,e)=>bi(o,{label:e});R("context",bi);R("label",ph);class Ir extends O{static get type(){return"VarNode"}constructor(e,t=null){super(),this.node=e,this.name=t,this.global=!0,this.isVarNode=!0}getHash(e){return this.name||super.getHash(e)}getNodeType(e){return this.node.getNodeType(e)}generate(e){const{node:t,name:s}=this,n=e.getVarFromNode(this,s,e.getVectorType(this.getNodeType(e))),r=e.getPropertyName(n),i=t.build(e,n.type);return e.addLineFlowCode(`${r} = ${i}`,this),r}}const fh=C(Ir);R("toVar",(...o)=>fh(...o).append());const gh=o=>(console.warn('TSL: "temp" is deprecated. Use ".toVar()" instead.'),fh(o));R("temp",gh);class Ey extends O{static get type(){return"VaryingNode"}constructor(e,t=null){super(),this.node=e,this.name=t,this.isVaryingNode=!0}isGlobal(){return!0}getHash(e){return this.name||super.getHash(e)}getNodeType(e){return this.node.getNodeType(e)}setupVarying(e){const t=e.getNodeProperties(this);let s=t.varying;if(s===void 0){const n=this.name,r=this.getNodeType(e);t.varying=s=e.getVaryingFromNode(this,n,r),t.node=this.node}return s.needsInterpolation||(s.needsInterpolation=e.shaderStage==="fragment"),s}setup(e){this.setupVarying(e)}analyze(e){return this.setupVarying(e),this.node.analyze(e)}generate(e){const t=e.getNodeProperties(this),s=this.setupVarying(e),n=e.shaderStage==="fragment"&&t.reassignPosition===!0&&e.context.needsPositionReassign;if(t.propertyName===void 0||n){const r=this.getNodeType(e),i=e.getPropertyName(s,Oo.VERTEX);e.flowNodeFromShaderStage(Oo.VERTEX,this.node,r,i),t.propertyName=i,n?t.reassignPosition=!1:t.reassignPosition===void 0&&e.context.isPositionNodeInput&&(t.reassignPosition=!0)}return e.getPropertyName(s)}}const ke=C(Ey),mh=o=>ke(o);R("varying",ke);R("vertexStage",mh);const yh=b(([o])=>{const e=o.mul(.9478672986).add(.0521327014).pow(2.4),t=o.mul(.0773993808),s=o.lessThanEqual(.04045);return Q(e,t,s)}).setLayout({name:"sRGBTransferEOTF",type:"vec3",inputs:[{name:"color",type:"vec3"}]}),xh=b(([o])=>{const e=o.pow(.41666).mul(1.055).sub(.055),t=o.mul(12.92),s=o.lessThanEqual(.0031308);return Q(e,t,s)}).setLayout({name:"sRGBTransferOETF",type:"vec3",inputs:[{name:"color",type:"vec3"}]}),tr="WorkingColorSpace",Ja="OutputColorSpace";class sr extends be{static get type(){return"ColorSpaceNode"}constructor(e,t,s){super("vec4"),this.colorNode=e,this.source=t,this.target=s}resolveColorSpace(e,t){return t===tr?_t.workingColorSpace:t===Ja?e.context.outputColorSpace||e.renderer.outputColorSpace:t}setup(e){const{colorNode:t}=this,s=this.resolveColorSpace(e,this.source),n=this.resolveColorSpace(e,this.target);let r=t;return _t.enabled===!1||s===n||!s||!n||(_t.getTransfer(s)===Ku&&(r=P(yh(r.rgb),r.a)),_t.getPrimaries(s)!==_t.getPrimaries(n)&&(r=P(Oe(_t._getMatrix(new Yn,s,n)).mul(r.rgb),r.a)),_t.getTransfer(n)===Ku&&(r=P(xh(r.rgb),r.a))),r}}const Th=o=>E(new sr(E(o),tr,Ja)),_h=o=>E(new sr(E(o),Ja,tr)),bh=(o,e)=>E(new sr(E(o),tr,e)),eu=(o,e)=>E(new sr(E(o),e,tr)),wy=(o,e,t)=>E(new sr(E(o),e,t));R("toOutputColorSpace",Th);R("toWorkingColorSpace",_h);R("workingToColorSpace",bh);R("colorSpaceToWorking",eu);let My=class extends Rs{static get type(){return"ReferenceElementNode"}constructor(e,t){super(e,t),this.referenceNode=e,this.isReferenceElementNode=!0}getNodeType(){return this.referenceNode.uniformType}generate(e){const t=super.generate(e),s=this.referenceNode.getNodeType(),n=this.getNodeType();return e.format(t,s,n)}};class Nh extends O{static get type(){return"ReferenceBaseNode"}constructor(e,t,s=null,n=null){super(),this.property=e,this.uniformType=t,this.object=s,this.count=n,this.properties=e.split("."),this.reference=s,this.node=null,this.group=null,this.updateType=W.OBJECT}setGroup(e){return this.group=e,this}element(e){return E(new My(this,E(e)))}setNodeType(e){const t=V(null,e).getSelf();this.group!==null&&t.setGroup(this.group),this.node=t}getNodeType(e){return this.node===null&&(this.updateReference(e),this.updateValue()),this.node.getNodeType(e)}getValueFromReference(e=this.reference){const{properties:t}=this;let s=e[t[0]];for(let n=1;nE(new Nh(o,e,t));class Fy extends Nh{static get type(){return"RendererReferenceNode"}constructor(e,t,s=null){super(e,t,s),this.renderer=s,this.setGroup(k)}updateReference(e){return this.reference=this.renderer!==null?this.renderer:e.renderer,this.reference}}const Sh=(o,e,t=null)=>E(new Fy(o,e,t));class Uy extends be{static get type(){return"ToneMappingNode"}constructor(e,t=Ah,s=null){super("vec3"),this.toneMapping=e,this.exposureNode=t,this.colorNode=s}customCacheKey(){return di(this.toneMapping)}setup(e){const t=this.colorNode||e.context.color,s=this.toneMapping;if(s===ss)return t;let n=null;const r=e.renderer.library.getToneMappingFunction(s);return r!==null?n=P(r(t.rgb,this.exposureNode),t.a):(console.error("ToneMappingNode: Unsupported Tone Mapping configuration.",s),n=t),n}}const vh=(o,e,t)=>E(new Uy(o,E(e),E(t))),Ah=Sh("toneMappingExposure","float");R("toneMapping",(o,e,t)=>vh(e,t,o));class Py extends va{static get type(){return"BufferAttributeNode"}constructor(e,t=null,s=0,n=0){super(e,t),this.isBufferNode=!0,this.bufferType=t,this.bufferStride=s,this.bufferOffset=n,this.usage=vg,this.instanced=!1,this.attribute=null,this.global=!0,e&&e.isBufferAttribute===!0&&(this.attribute=e,this.usage=e.usage,this.instanced=e.isInstancedBufferAttribute)}getHash(e){if(this.bufferStride===0&&this.bufferOffset===0){let t=e.globalCache.getData(this.value);return t===void 0&&(t={node:this},e.globalCache.setData(this.value,t)),t.node.uuid}return this.uuid}getNodeType(e){return this.bufferType===null&&(this.bufferType=e.getTypeFromAttribute(this.attribute)),this.bufferType}setup(e){if(this.attribute!==null)return;const t=this.getNodeType(e),s=this.value,n=e.getTypeLength(t),r=this.bufferStride||n,i=this.bufferOffset,a=s.isInterleavedBuffer===!0?s:new Ag(s,r),u=new Rg(a,n,i);a.setUsage(this.usage),this.attribute=u,this.attribute.isInstancedBufferAttribute=this.instanced}generate(e){const t=this.getNodeType(e),s=e.getBufferAttributeFromNode(this,t),n=e.getPropertyName(s);let r=null;return e.shaderStage==="vertex"||e.shaderStage==="compute"?(this.name=n,r=n):r=ke(this).build(e,t),r}getInputType(){return"bufferAttribute"}setUsage(e){return this.usage=e,this.attribute&&this.attribute.isBufferAttribute===!0&&(this.attribute.usage=e),this}setInstanced(e){return this.instanced=e,this}}const nr=(o,e=null,t=0,s=0)=>E(new Py(o,e,t,s)),Rh=(o,e=null,t=0,s=0)=>nr(o,e,t,s).setUsage(zs),Zr=(o,e=null,t=0,s=0)=>nr(o,e,t,s).setInstanced(!0),qo=(o,e=null,t=0,s=0)=>Rh(o,e,t,s).setInstanced(!0);R("toAttribute",o=>nr(o.value));class Dy extends O{static get type(){return"ComputeNode"}constructor(e,t,s=[64]){super("void"),this.isComputeNode=!0,this.computeNode=e,this.count=t,this.workgroupSize=s,this.dispatchCount=0,this.version=1,this.name="",this.updateBeforeType=W.OBJECT,this.onInitFunction=null,this.updateDispatchCount()}dispose(){this.dispatchEvent({type:"dispose"})}label(e){return this.name=e,this}updateDispatchCount(){const{count:e,workgroupSize:t}=this;let s=t[0];for(let n=1;nE(new Dy(E(o),e,t));R("compute",Ch);class Ly extends O{static get type(){return"CacheNode"}constructor(e,t=!0){super(),this.node=e,this.parent=t,this.isCacheNode=!0}getNodeType(e){const t=e.getCache(),s=e.getCacheFromNode(this,this.parent);e.setCache(s);const n=this.node.getNodeType(e);return e.setCache(t),n}build(e,...t){const s=e.getCache(),n=e.getCacheFromNode(this,this.parent);e.setCache(n);const r=this.node.build(e,...t);return e.setCache(s),r}}const Mn=(o,e)=>E(new Ly(E(o),e));R("cache",Mn);class Iy extends O{static get type(){return"BypassNode"}constructor(e,t){super(),this.isBypassNode=!0,this.outputNode=e,this.callNode=t}getNodeType(e){return this.outputNode.getNodeType(e)}generate(e){const t=this.callNode.build(e,"void");return t!==""&&e.addLineFlowCode(t,this),this.outputNode.build(e)}}const Eh=C(Iy);R("bypass",Eh);class wh extends O{static get type(){return"RemapNode"}constructor(e,t,s,n=g(0),r=g(1)){super(),this.node=e,this.inLowNode=t,this.inHighNode=s,this.outLowNode=n,this.outHighNode=r,this.doClamp=!0}setup(){const{node:e,inLowNode:t,inHighNode:s,outLowNode:n,outHighNode:r,doClamp:i}=this;let a=e.sub(t).div(s.sub(t));return i===!0&&(a=a.clamp()),a.mul(r.sub(n)).add(n)}}const Mh=C(wh,null,null,{doClamp:!1}),Bh=C(wh);R("remap",Mh);R("remapClamp",Bh);class Vr extends O{static get type(){return"ExpressionNode"}constructor(e="",t="void"){super(t),this.snippet=e}generate(e,t){const s=this.getNodeType(e),n=this.snippet;if(s==="void")e.addLineFlowCode(n,this);else return e.format(`( ${n} )`,s,t)}}const rs=C(Vr),Fh=o=>(o?Ue(o,rs("discard")):rs("discard")).append(),Vy=()=>rs("return").append();R("discard",Fh);class Gy extends be{static get type(){return"RenderOutputNode"}constructor(e,t,s){super("vec4"),this.colorNode=e,this.toneMapping=t,this.outputColorSpace=s,this.isRenderOutputNode=!0}setup({context:e}){let t=this.colorNode||e.color;const s=(this.toneMapping!==null?this.toneMapping:e.toneMapping)||ss,n=(this.outputColorSpace!==null?this.outputColorSpace:e.outputColorSpace)||Dn;return s!==ss&&(t=t.toneMapping(s)),n!==Dn&&n!==_t.workingColorSpace&&(t=t.workingToColorSpace(n)),t}}const tu=(o,e=null,t=null)=>E(new Gy(E(o),e,t));R("renderOutput",tu);function Oy(o){console.warn("THREE.TSLBase: AddNodeElement has been removed in favor of tree-shaking. Trying add",o)}class Uh extends O{static get type(){return"AttributeNode"}constructor(e,t=null){super(t),this.global=!0,this._attributeName=e}getHash(e){return this.getAttributeName(e)}getNodeType(e){let t=this.nodeType;if(t===null){const s=this.getAttributeName(e);if(e.hasGeometryAttribute(s)){const n=e.geometry.getAttribute(s);t=e.getTypeFromAttribute(n)}else t="float"}return t}setAttributeName(e){return this._attributeName=e,this}getAttributeName(){return this._attributeName}generate(e){const t=this.getAttributeName(e),s=this.getNodeType(e);if(e.hasGeometryAttribute(t)===!0){const r=e.geometry.getAttribute(t),i=e.getTypeFromAttribute(r),a=e.getAttribute(t,i);return e.shaderStage==="vertex"?e.format(a.name,i,s):ke(this).build(e,s)}else return console.warn(`AttributeNode: Vertex attribute "${t}" not found on geometry.`),e.generateConst(s)}serialize(e){super.serialize(e),e.global=this.global,e._attributeName=this._attributeName}deserialize(e){super.deserialize(e),this.global=e.global,this._attributeName=e._attributeName}}const we=(o,e)=>E(new Uh(o,e)),le=(o=0)=>we("uv"+(o>0?o:""),"vec2");class ky extends O{static get type(){return"TextureSizeNode"}constructor(e,t=null){super("uvec2"),this.isTextureSizeNode=!0,this.textureNode=e,this.levelNode=t}generate(e,t){const s=this.textureNode.build(e,"property"),n=this.levelNode===null?"0":this.levelNode.build(e,"int");return e.format(`${e.getMethod("textureDimensions")}( ${s}, ${n} )`,this.getNodeType(e),t)}}const ns=C(ky);class zy extends er{static get type(){return"MaxMipLevelNode"}constructor(e){super(0),this._textureNode=e,this.updateType=W.FRAME}get textureNode(){return this._textureNode}get texture(){return this._textureNode.value}update(){const e=this.texture,t=e.images,s=t&&t.length>0?t[0]&&t[0].image||t[0]:e.image;if(s&&s.width!==void 0){const{width:n,height:r}=s;this.value=Math.log2(Math.max(n,r))}}}const Ph=C(zy);class wt extends er{static get type(){return"TextureNode"}constructor(e,t=null,s=null,n=null){super(e),this.isTextureNode=!0,this.uvNode=t,this.levelNode=s,this.biasNode=n,this.compareNode=null,this.depthNode=null,this.gradNode=null,this.sampler=!0,this.updateMatrix=!1,this.updateType=W.NONE,this.referenceNode=null,this._value=e,this._matrixUniform=null,this.setUpdateMatrix(t===null)}set value(e){this.referenceNode?this.referenceNode.value=e:this._value=e}get value(){return this.referenceNode?this.referenceNode.value:this._value}getUniformHash(){return this.value.uuid}getNodeType(){return this.value.isDepthTexture===!0?"float":this.value.type===Le?"uvec4":this.value.type===ze?"ivec4":"vec4"}getInputType(){return"texture"}getDefaultUV(){return le(this.value.channel)}updateReference(){return this.value}getTransformedUV(e){return this._matrixUniform===null&&(this._matrixUniform=V(this.value.matrix)),this._matrixUniform.mul(T(e,1)).xy}setUpdateMatrix(e){return this.updateMatrix=e,this.updateType=e?W.RENDER:W.NONE,this}setupUV(e,t){const s=this.value;return e.isFlipY()&&(s.image instanceof ImageBitmap&&s.flipY===!0||s.isRenderTargetTexture===!0||s.isFramebufferTexture===!0||s.isDepthTexture===!0)&&(this.sampler?t=t.flipY():t=t.setY(y(ns(this,this.levelNode).y).sub(t.y).sub(1))),t}setup(e){const t=e.getNodeProperties(this);t.referenceNode=this.referenceNode;const s=this.value;if(!s||s.isTexture!==!0)throw new Error("THREE.TSL: `texture( value )` function expects a valid instance of THREE.Texture().");let n=this.uvNode;(n===null||e.context.forceUVContext===!0)&&e.context.getUV&&(n=e.context.getUV(this)),n||(n=this.getDefaultUV()),this.updateMatrix===!0&&(n=this.getTransformedUV(n)),n=this.setupUV(e,n);let r=this.levelNode;r===null&&e.context.getTextureLevel&&(r=e.context.getTextureLevel(this)),t.uvNode=n,t.levelNode=r,t.biasNode=this.biasNode,t.compareNode=this.compareNode,t.gradNode=this.gradNode,t.depthNode=this.depthNode}generateUV(e,t){return t.build(e,this.sampler===!0?"vec2":"ivec2")}generateSnippet(e,t,s,n,r,i,a,u){const c=this.value;let l;return n?l=e.generateTextureLevel(c,t,s,n,i):r?l=e.generateTextureBias(c,t,s,r,i):u?l=e.generateTextureGrad(c,t,s,u,i):a?l=e.generateTextureCompare(c,t,s,a,i):this.sampler===!1?l=e.generateTextureLoad(c,t,s,i):l=e.generateTexture(c,t,s,i),l}generate(e,t){const s=this.value,n=e.getNodeProperties(this),r=super.generate(e,"property");if(t==="sampler")return r+"_sampler";if(e.isReference(t))return r;{const i=e.getDataFromNode(this);let a=i.propertyName;if(a===void 0){const{uvNode:l,levelNode:d,biasNode:h,compareNode:p,depthNode:f,gradNode:m}=n,x=this.generateUV(e,l),N=d?d.build(e,"float"):null,v=h?h.build(e,"float"):null,w=f?f.build(e,"int"):null,B=p?p.build(e,"float"):null,F=m?[m[0].build(e,"vec2"),m[1].build(e,"vec2")]:null,L=e.getVarFromNode(this);a=e.getPropertyName(L);const I=this.generateSnippet(e,r,x,N,v,w,B,F);e.addLineFlowCode(`${a} = ${I}`,this),i.snippet=I,i.propertyName=a}let u=a;const c=this.getNodeType(e);return e.needsToWorkingColorSpace(s)&&(u=eu(rs(u,c),s.colorSpace).setup(e).build(e,c)),e.format(u,c,t)}}setSampler(e){return this.sampler=e,this}getSampler(){return this.sampler}uv(e){return console.warn("THREE.TextureNode: .uv() has been renamed. Use .sample() instead."),this.sample(e)}sample(e){const t=this.clone();return t.uvNode=E(e),t.referenceNode=this.getSelf(),E(t)}blur(e){const t=this.clone();return t.biasNode=E(e).mul(Ph(t)),t.referenceNode=this.getSelf(),E(t)}level(e){const t=this.clone();return t.levelNode=E(e),t.referenceNode=this.getSelf(),E(t)}size(e){return ns(this,e)}bias(e){const t=this.clone();return t.biasNode=E(e),t.referenceNode=this.getSelf(),E(t)}compare(e){const t=this.clone();return t.compareNode=E(e),t.referenceNode=this.getSelf(),E(t)}grad(e,t){const s=this.clone();return s.gradNode=[E(e),E(t)],s.referenceNode=this.getSelf(),E(s)}depth(e){const t=this.clone();return t.depthNode=E(e),t.referenceNode=this.getSelf(),E(t)}serialize(e){super.serialize(e),e.value=this.value.toJSON(e.meta).uuid,e.sampler=this.sampler,e.updateMatrix=this.updateMatrix,e.updateType=this.updateType}deserialize(e){super.deserialize(e),this.value=e.meta.textures[e.value],this.sampler=e.sampler,this.updateMatrix=e.updateMatrix,this.updateType=e.updateType}update(){const e=this.value,t=this._matrixUniform;t!==null&&(t.value=e.matrix),e.matrixAutoUpdate===!0&&e.updateMatrix()}clone(){const e=new this.constructor(this.value,this.uvNode,this.levelNode,this.biasNode);return e.sampler=this.sampler,e}}const K=C(wt),ge=(...o)=>K(...o).setSampler(!1),Wy=o=>(o.isNode===!0?o:K(o)).convert("sampler"),Jt=V("float").label("cameraNear").setGroup(k).onRenderUpdate(({camera:o})=>o.near),es=V("float").label("cameraFar").setGroup(k).onRenderUpdate(({camera:o})=>o.far),He=V("mat4").label("cameraProjectionMatrix").setGroup(k).onRenderUpdate(({camera:o})=>o.projectionMatrix),$y=V("mat4").label("cameraProjectionMatrixInverse").setGroup(k).onRenderUpdate(({camera:o})=>o.projectionMatrixInverse),je=V("mat4").label("cameraViewMatrix").setGroup(k).onRenderUpdate(({camera:o})=>o.matrixWorldInverse),Hy=V("mat4").label("cameraWorldMatrix").setGroup(k).onRenderUpdate(({camera:o})=>o.matrixWorld),qy=V("mat3").label("cameraNormalMatrix").setGroup(k).onRenderUpdate(({camera:o})=>o.normalMatrix),su=V(new j).label("cameraPosition").setGroup(k).onRenderUpdate(({camera:o},e)=>e.value.setFromMatrixPosition(o.matrixWorld));class J extends O{static get type(){return"Object3DNode"}constructor(e,t=null){super(),this.scope=e,this.object3d=t,this.updateType=W.OBJECT,this._uniformNode=new er(null)}getNodeType(){const e=this.scope;if(e===J.WORLD_MATRIX)return"mat4";if(e===J.POSITION||e===J.VIEW_POSITION||e===J.DIRECTION||e===J.SCALE)return"vec3"}update(e){const t=this.object3d,s=this._uniformNode,n=this.scope;if(n===J.WORLD_MATRIX)s.value=t.matrixWorld;else if(n===J.POSITION)s.value=s.value||new j,s.value.setFromMatrixPosition(t.matrixWorld);else if(n===J.SCALE)s.value=s.value||new j,s.value.setFromMatrixScale(t.matrixWorld);else if(n===J.DIRECTION)s.value=s.value||new j,t.getWorldDirection(s.value);else if(n===J.VIEW_POSITION){const r=e.camera;s.value=s.value||new j,s.value.setFromMatrixPosition(t.matrixWorld),s.value.applyMatrix4(r.matrixWorldInverse)}}generate(e){const t=this.scope;return t===J.WORLD_MATRIX?this._uniformNode.nodeType="mat4":(t===J.POSITION||t===J.VIEW_POSITION||t===J.DIRECTION||t===J.SCALE)&&(this._uniformNode.nodeType="vec3"),this._uniformNode.build(e)}serialize(e){super.serialize(e),e.scope=this.scope}deserialize(e){super.deserialize(e),this.scope=e.scope}}J.WORLD_MATRIX="worldMatrix";J.POSITION="position";J.SCALE="scale";J.VIEW_POSITION="viewPosition";J.DIRECTION="direction";const Ky=C(J,J.DIRECTION),Xy=C(J,J.WORLD_MATRIX),Dh=C(J,J.POSITION),Yy=C(J,J.SCALE),jy=C(J,J.VIEW_POSITION);class Mt extends J{static get type(){return"ModelNode"}constructor(e){super(e)}update(e){this.object3d=e.object,super.update(e)}}const Qy=U(Mt,Mt.DIRECTION),at=U(Mt,Mt.WORLD_MATRIX),Zy=U(Mt,Mt.POSITION),Jy=U(Mt,Mt.SCALE),ex=U(Mt,Mt.VIEW_POSITION),Lh=V(new Yn).onObjectUpdate(({object:o},e)=>e.value.getNormalMatrix(o.matrixWorld)),Ih=V(new Ie).onObjectUpdate(({object:o},e)=>e.value.copy(o.matrixWorld).invert()),Kt=b(o=>o.renderer.nodes.modelViewMatrix||Vh).once()().toVar("modelViewMatrix"),Vh=je.mul(at),tx=b(o=>(o.context.isHighPrecisionModelViewMatrix=!0,V("mat4").onObjectUpdate(({object:e,camera:t})=>e.modelViewMatrix.multiplyMatrices(t.matrixWorldInverse,e.matrixWorld)))).once()().toVar("highpModelViewMatrix"),sx=b(o=>{const e=o.context.isHighPrecisionModelViewMatrix;return V("mat3").onObjectUpdate(({object:t,camera:s})=>(e!==!0&&t.modelViewMatrix.multiplyMatrices(s.matrixWorldInverse,t.matrixWorld),t.normalMatrix.getNormalMatrix(t.modelViewMatrix)))}).once()().toVar("highpModelNormalViewMatrix"),Ce=we("position","vec3"),me=Ce.varying("positionLocal"),Jr=Ce.varying("positionPrevious"),Wt=at.mul(me).xyz.varying("v_positionWorld").context({needsPositionReassign:!0}),nu=me.transformDirection(at).varying("v_positionWorldDirection").normalize().toVar("positionWorldDirection").context({needsPositionReassign:!0}),xe=b(o=>o.context.setupPositionView(),"vec3").once()().varying("v_positionView").context({needsPositionReassign:!0}),ae=xe.negate().varying("v_positionViewDirection").normalize().toVar("positionViewDirection");class nx extends O{static get type(){return"FrontFacingNode"}constructor(){super("bool"),this.isFrontFacingNode=!0}generate(e){const{renderer:t,material:s}=e;return t.coordinateSystem===Pn&&s.side===Ct?"false":e.getFrontFacing()}}const Gh=U(nx),rr=g(Gh).mul(2).sub(1),Ni=we("normal","vec3"),Xe=b(o=>o.geometry.hasAttribute("normal")===!1?(console.warn('TSL.NormalNode: Vertex attribute "normal" not found on geometry.'),T(0,1,0)):Ni,"vec3").once()().toVar("normalLocal"),Oh=xe.dFdx().cross(xe.dFdy()).normalize().toVar("normalFlat"),lt=b(o=>{let e;return o.material.flatShading===!0?e=Oh:e=ke(ru(Xe),"v_normalView").normalize(),e},"vec3").once()().toVar("normalView"),Si=ke(lt.transformDirection(je),"v_normalWorld").normalize().toVar("normalWorld"),fe=b(o=>o.context.setupNormal(),"vec3").once()().mul(rr).toVar("transformedNormalView"),vi=fe.transformDirection(je).toVar("transformedNormalWorld"),Hs=b(o=>o.context.setupClearcoatNormal(),"vec3").once()().mul(rr).toVar("transformedClearcoatNormalView"),kh=b(([o,e=at])=>{const t=Oe(e),s=o.div(T(t[0].dot(t[0]),t[1].dot(t[1]),t[2].dot(t[2])));return t.mul(s).xyz}),ru=b(([o],e)=>{const t=e.renderer.nodes.modelNormalViewMatrix;if(t!==null)return t.transformDirection(o);const s=Lh.mul(o);return je.transformDirection(s)}),zh=V(0).onReference(({material:o})=>o).onRenderUpdate(({material:o})=>o.refractionRatio),Wh=ae.negate().reflect(fe),$h=ae.negate().refract(fe,zh),Hh=Wh.transformDirection(je).toVar("reflectVector"),qh=$h.transformDirection(je).toVar("reflectVector");class rx extends wt{static get type(){return"CubeTextureNode"}constructor(e,t=null,s=null,n=null){super(e,t,s,n),this.isCubeTextureNode=!0}getInputType(){return"cubeTexture"}getDefaultUV(){const e=this.value;return e.mapping===Zs?Hh:e.mapping===Js?qh:(console.error('THREE.CubeTextureNode: Mapping "%s" not supported.',e.mapping),T(0,0,0))}setUpdateMatrix(){}setupUV(e,t){const s=this.value;return e.renderer.coordinateSystem===ln||!s.isRenderTargetTexture?T(t.x.negate(),t.yz):t}generateUV(e,t){return t.build(e,"vec3")}}const on=C(rx);class iu extends er{static get type(){return"BufferNode"}constructor(e,t,s=0){super(e,t),this.isBufferNode=!0,this.bufferType=t,this.bufferCount=s}getElementType(e){return this.getNodeType(e)}getInputType(){return"buffer"}}const ir=(o,e,t)=>E(new iu(o,e,t));class ix extends Rs{static get type(){return"UniformArrayElementNode"}constructor(e,t){super(e,t),this.isArrayBufferElementNode=!0}generate(e){const t=super.generate(e),s=this.getNodeType(),n=this.node.getPaddedType();return e.format(t,n,s)}}class Kh extends iu{static get type(){return"UniformArrayNode"}constructor(e,t=null){super(null),this.array=e,this.elementType=t===null?Ot(e[0]):t,this.paddedType=this.getPaddedType(),this.updateType=W.RENDER,this.isArrayBufferNode=!0}getNodeType(){return this.paddedType}getElementType(){return this.elementType}getPaddedType(){const e=this.elementType;let t="vec4";return e==="mat2"?t="mat2":/mat/.test(e)===!0?t="mat4":e.charAt(0)==="i"?t="ivec4":e.charAt(0)==="u"&&(t="uvec4"),t}update(){const{array:e,value:t}=this,s=this.elementType;if(s==="float"||s==="int"||s==="uint")for(let n=0;nE(new Kh(o,e)),ox=(o,e)=>(console.warn("TSL.UniformArrayNode: uniforms() has been renamed to uniformArray()."),E(new Kh(o,e)));class ax extends Rs{static get type(){return"ReferenceElementNode"}constructor(e,t){super(e,t),this.referenceNode=e,this.isReferenceElementNode=!0}getNodeType(){return this.referenceNode.uniformType}generate(e){const t=super.generate(e),s=this.referenceNode.getNodeType(),n=this.getNodeType();return e.format(t,s,n)}}class Ai extends O{static get type(){return"ReferenceNode"}constructor(e,t,s=null,n=null){super(),this.property=e,this.uniformType=t,this.object=s,this.count=n,this.properties=e.split("."),this.reference=s,this.node=null,this.group=null,this.name=null,this.updateType=W.OBJECT}element(e){return E(new ax(this,E(e)))}setGroup(e){return this.group=e,this}label(e){return this.name=e,this}setNodeType(e){let t=null;this.count!==null?t=ir(null,e,this.count):Array.isArray(this.getValueFromReference())?t=Gt(null,e):e==="texture"?t=K(null):e==="cubeTexture"?t=on(null):t=V(null,e),this.group!==null&&t.setGroup(this.group),this.name!==null&&t.label(this.name),this.node=t.getSelf()}getNodeType(e){return this.node===null&&(this.updateReference(e),this.updateValue()),this.node.getNodeType(e)}getValueFromReference(e=this.reference){const{properties:t}=this;let s=e[t[0]];for(let n=1;nE(new Ai(o,e,t)),Ko=(o,e,t,s)=>E(new Ai(o,e,s,t));class ux extends Ai{static get type(){return"MaterialReferenceNode"}constructor(e,t,s=null){super(e,t,s),this.material=s,this.isMaterialReferenceNode=!0}updateReference(e){return this.reference=this.material!==null?this.material:e.material,this.reference}}const dt=(o,e,t=null)=>E(new ux(o,e,t)),Ri=b(o=>(o.geometry.hasAttribute("tangent")===!1&&o.geometry.computeTangents(),we("tangent","vec4")))(),or=Ri.xyz.toVar("tangentLocal"),ar=Kt.mul(P(or,0)).xyz.varying("v_tangentView").normalize().toVar("tangentView"),Xh=ar.transformDirection(je).varying("v_tangentWorld").normalize().toVar("tangentWorld"),ou=ar.toVar("transformedTangentView"),cx=ou.transformDirection(je).normalize().toVar("transformedTangentWorld"),ur=o=>o.mul(Ri.w).xyz,lx=ke(ur(Ni.cross(Ri)),"v_bitangentGeometry").normalize().toVar("bitangentGeometry"),dx=ke(ur(Xe.cross(or)),"v_bitangentLocal").normalize().toVar("bitangentLocal"),Yh=ke(ur(lt.cross(ar)),"v_bitangentView").normalize().toVar("bitangentView"),hx=ke(ur(Si.cross(Xh)),"v_bitangentWorld").normalize().toVar("bitangentWorld"),jh=ur(fe.cross(ou)).normalize().toVar("transformedBitangentView"),px=jh.transformDirection(je).normalize().toVar("transformedBitangentWorld"),gs=Oe(ar,Yh,lt),Qh=ae.mul(gs),fx=(o,e)=>o.sub(Qh.mul(e)),Zh=(()=>{let o=_s.cross(ae);return o=o.cross(_s).normalize(),o=Q(o,fe,Zt.mul(Nt.oneMinus()).oneMinus().pow2().pow2()).normalize(),o})(),gx=b(o=>{const{eye_pos:e,surf_norm:t,mapN:s,uv:n}=o,r=e.dFdx(),i=e.dFdy(),a=n.dFdx(),u=n.dFdy(),c=t,l=i.cross(c),d=c.cross(r),h=l.mul(a.x).add(d.mul(u.x)),p=l.mul(a.y).add(d.mul(u.y)),f=h.dot(h).max(p.dot(p)),m=rr.mul(f.inverseSqrt());return _e(h.mul(s.x,m),p.mul(s.y,m),c.mul(s.z)).normalize()});class mx extends be{static get type(){return"NormalMapNode"}constructor(e,t=null){super("vec3"),this.node=e,this.scaleNode=t,this.normalMapType=Yu}setup(e){const{normalMapType:t,scaleNode:s}=this;let n=this.node.mul(2).sub(1);s!==null&&(n=T(n.xy.mul(s),n.z));let r=null;return t===Og?r=ru(n):t===Yu&&(e.hasGeometryAttribute("tangent")===!0?r=gs.mul(n).normalize():r=gx({eye_pos:xe,surf_norm:lt,mapN:n,uv:le()})),r}}const Xo=C(mx),yx=b(({textureNode:o,bumpScale:e})=>{const t=n=>o.cache().context({getUV:r=>n(r.uvNode||le()),forceUVContext:!0}),s=g(t(n=>n));return M(g(t(n=>n.add(n.dFdx()))).sub(s),g(t(n=>n.add(n.dFdy()))).sub(s)).mul(e)}),xx=b(o=>{const{surf_pos:e,surf_norm:t,dHdxy:s}=o,n=e.dFdx().normalize(),r=e.dFdy().normalize(),i=t,a=r.cross(i),u=i.cross(n),c=n.dot(a).mul(rr),l=c.sign().mul(s.x.mul(a).add(s.y.mul(u)));return c.abs().mul(t).sub(l).normalize()});class Tx extends be{static get type(){return"BumpMapNode"}constructor(e,t=null){super("vec3"),this.textureNode=e,this.scaleNode=t}setup(){const e=this.scaleNode!==null?this.scaleNode:1,t=yx({textureNode:this.textureNode,bumpScale:e});return xx({surf_pos:xe,surf_norm:lt,dHdxy:t})}}const Jh=C(Tx),cc=new Map;class A extends O{static get type(){return"MaterialNode"}constructor(e){super(),this.scope=e}getCache(e,t){let s=cc.get(e);return s===void 0&&(s=dt(e,t),cc.set(e,s)),s}getFloat(e){return this.getCache(e,"float")}getColor(e){return this.getCache(e,"color")}getTexture(e){return this.getCache(e==="map"?"map":e+"Map","texture")}setup(e){const t=e.context.material,s=this.scope;let n=null;if(s===A.COLOR){const r=t.color!==void 0?this.getColor(s):T();t.map&&t.map.isTexture===!0?n=r.mul(this.getTexture("map")):n=r}else if(s===A.OPACITY){const r=this.getFloat(s);t.alphaMap&&t.alphaMap.isTexture===!0?n=r.mul(this.getTexture("alpha")):n=r}else if(s===A.SPECULAR_STRENGTH)t.specularMap&&t.specularMap.isTexture===!0?n=this.getTexture("specular").r:n=g(1);else if(s===A.SPECULAR_INTENSITY){const r=this.getFloat(s);t.specularIntensityMap&&t.specularIntensityMap.isTexture===!0?n=r.mul(this.getTexture(s).a):n=r}else if(s===A.SPECULAR_COLOR){const r=this.getColor(s);t.specularColorMap&&t.specularColorMap.isTexture===!0?n=r.mul(this.getTexture(s).rgb):n=r}else if(s===A.ROUGHNESS){const r=this.getFloat(s);t.roughnessMap&&t.roughnessMap.isTexture===!0?n=r.mul(this.getTexture(s).g):n=r}else if(s===A.METALNESS){const r=this.getFloat(s);t.metalnessMap&&t.metalnessMap.isTexture===!0?n=r.mul(this.getTexture(s).b):n=r}else if(s===A.EMISSIVE){const r=this.getFloat("emissiveIntensity"),i=this.getColor(s).mul(r);t.emissiveMap&&t.emissiveMap.isTexture===!0?n=i.mul(this.getTexture(s)):n=i}else if(s===A.NORMAL)t.normalMap?(n=Xo(this.getTexture("normal"),this.getCache("normalScale","vec2")),n.normalMapType=t.normalMapType):t.bumpMap?n=Jh(this.getTexture("bump").r,this.getFloat("bumpScale")):n=lt;else if(s===A.CLEARCOAT){const r=this.getFloat(s);t.clearcoatMap&&t.clearcoatMap.isTexture===!0?n=r.mul(this.getTexture(s).r):n=r}else if(s===A.CLEARCOAT_ROUGHNESS){const r=this.getFloat(s);t.clearcoatRoughnessMap&&t.clearcoatRoughnessMap.isTexture===!0?n=r.mul(this.getTexture(s).r):n=r}else if(s===A.CLEARCOAT_NORMAL)t.clearcoatNormalMap?n=Xo(this.getTexture(s),this.getCache(s+"Scale","vec2")):n=lt;else if(s===A.SHEEN){const r=this.getColor("sheenColor").mul(this.getFloat("sheen"));t.sheenColorMap&&t.sheenColorMap.isTexture===!0?n=r.mul(this.getTexture("sheenColor").rgb):n=r}else if(s===A.SHEEN_ROUGHNESS){const r=this.getFloat(s);t.sheenRoughnessMap&&t.sheenRoughnessMap.isTexture===!0?n=r.mul(this.getTexture(s).a):n=r,n=n.clamp(.07,1)}else if(s===A.ANISOTROPY)if(t.anisotropyMap&&t.anisotropyMap.isTexture===!0){const r=this.getTexture(s);n=fi(Os.x,Os.y,Os.y.negate(),Os.x).mul(r.rg.mul(2).sub(M(1)).normalize().mul(r.b))}else n=Os;else if(s===A.IRIDESCENCE_THICKNESS){const r=ne("1","float",t.iridescenceThicknessRange);if(t.iridescenceThicknessMap){const i=ne("0","float",t.iridescenceThicknessRange);n=r.sub(i).mul(this.getTexture(s).g).add(i)}else n=r}else if(s===A.TRANSMISSION){const r=this.getFloat(s);t.transmissionMap?n=r.mul(this.getTexture(s).r):n=r}else if(s===A.THICKNESS){const r=this.getFloat(s);t.thicknessMap?n=r.mul(this.getTexture(s).g):n=r}else if(s===A.IOR)n=this.getFloat(s);else if(s===A.LIGHT_MAP)n=this.getTexture(s).rgb.mul(this.getFloat("lightMapIntensity"));else if(s===A.AO)n=this.getTexture(s).r.sub(1).mul(this.getFloat("aoMapIntensity")).add(1);else{const r=this.getNodeType(e);n=this.getCache(s,r)}return n}}A.ALPHA_TEST="alphaTest";A.COLOR="color";A.OPACITY="opacity";A.SHININESS="shininess";A.SPECULAR="specular";A.SPECULAR_STRENGTH="specularStrength";A.SPECULAR_INTENSITY="specularIntensity";A.SPECULAR_COLOR="specularColor";A.REFLECTIVITY="reflectivity";A.ROUGHNESS="roughness";A.METALNESS="metalness";A.NORMAL="normal";A.CLEARCOAT="clearcoat";A.CLEARCOAT_ROUGHNESS="clearcoatRoughness";A.CLEARCOAT_NORMAL="clearcoatNormal";A.EMISSIVE="emissive";A.ROTATION="rotation";A.SHEEN="sheen";A.SHEEN_ROUGHNESS="sheenRoughness";A.ANISOTROPY="anisotropy";A.IRIDESCENCE="iridescence";A.IRIDESCENCE_IOR="iridescenceIOR";A.IRIDESCENCE_THICKNESS="iridescenceThickness";A.IOR="ior";A.TRANSMISSION="transmission";A.THICKNESS="thickness";A.ATTENUATION_DISTANCE="attenuationDistance";A.ATTENUATION_COLOR="attenuationColor";A.LINE_SCALE="scale";A.LINE_DASH_SIZE="dashSize";A.LINE_GAP_SIZE="gapSize";A.LINE_WIDTH="linewidth";A.LINE_DASH_OFFSET="dashOffset";A.POINT_WIDTH="pointWidth";A.DISPERSION="dispersion";A.LIGHT_MAP="light";A.AO="ao";const ep=U(A,A.ALPHA_TEST),an=U(A,A.COLOR),tp=U(A,A.SHININESS),sp=U(A,A.EMISSIVE),cr=U(A,A.OPACITY),np=U(A,A.SPECULAR),Yo=U(A,A.SPECULAR_INTENSITY),rp=U(A,A.SPECULAR_COLOR),Bn=U(A,A.SPECULAR_STRENGTH),Gr=U(A,A.REFLECTIVITY),ip=U(A,A.ROUGHNESS),op=U(A,A.METALNESS),ap=U(A,A.NORMAL).context({getUV:null}),up=U(A,A.CLEARCOAT),cp=U(A,A.CLEARCOAT_ROUGHNESS),lp=U(A,A.CLEARCOAT_NORMAL).context({getUV:null}),dp=U(A,A.ROTATION),hp=U(A,A.SHEEN),pp=U(A,A.SHEEN_ROUGHNESS),fp=U(A,A.ANISOTROPY),gp=U(A,A.IRIDESCENCE),mp=U(A,A.IRIDESCENCE_IOR),yp=U(A,A.IRIDESCENCE_THICKNESS),xp=U(A,A.TRANSMISSION),Tp=U(A,A.THICKNESS),_p=U(A,A.IOR),bp=U(A,A.ATTENUATION_DISTANCE),Np=U(A,A.ATTENUATION_COLOR),au=U(A,A.LINE_SCALE),uu=U(A,A.LINE_DASH_SIZE),cu=U(A,A.LINE_GAP_SIZE),Or=U(A,A.LINE_WIDTH),lu=U(A,A.LINE_DASH_OFFSET),Sp=U(A,A.POINT_WIDTH),vp=U(A,A.DISPERSION),du=U(A,A.LIGHT_MAP),Ap=U(A,A.AO),Os=V(new Ye).onReference(function(o){return o.material}).onRenderUpdate(function({material:o}){this.value.set(o.anisotropy*Math.cos(o.anisotropyRotation),o.anisotropy*Math.sin(o.anisotropyRotation))}),hu=b(o=>o.context.setupModelViewProjection(),"vec4").once()().varying("v_modelViewProjection");class de extends O{static get type(){return"IndexNode"}constructor(e){super("uint"),this.scope=e,this.isIndexNode=!0}generate(e){const t=this.getNodeType(e),s=this.scope;let n;if(s===de.VERTEX)n=e.getVertexIndex();else if(s===de.INSTANCE)n=e.getInstanceIndex();else if(s===de.DRAW)n=e.getDrawIndex();else if(s===de.INVOCATION_LOCAL)n=e.getInvocationLocalIndex();else if(s===de.INVOCATION_SUBGROUP)n=e.getInvocationSubgroupIndex();else if(s===de.SUBGROUP)n=e.getSubgroupIndex();else throw new Error("THREE.IndexNode: Unknown scope: "+s);let r;return e.shaderStage==="vertex"||e.shaderStage==="compute"?r=n:r=ke(this).build(e,t),r}}de.VERTEX="vertex";de.INSTANCE="instance";de.SUBGROUP="subgroup";de.INVOCATION_LOCAL="invocationLocal";de.INVOCATION_SUBGROUP="invocationSubgroup";de.DRAW="draw";const Rp=U(de,de.VERTEX),lr=U(de,de.INSTANCE),_x=U(de,de.SUBGROUP),bx=U(de,de.INVOCATION_SUBGROUP),Nx=U(de,de.INVOCATION_LOCAL),Cp=U(de,de.DRAW);class Ep extends O{static get type(){return"InstanceNode"}constructor(e,t,s){super("void"),this.count=e,this.instanceMatrix=t,this.instanceColor=s,this.instanceMatrixNode=null,this.instanceColorNode=null,this.updateType=W.FRAME,this.buffer=null,this.bufferColor=null}setup(e){const{count:t,instanceMatrix:s,instanceColor:n}=this;let{instanceMatrixNode:r,instanceColorNode:i}=this;if(r===null){if(t<=1e3)r=ir(s.array,"mat4",Math.max(t,1)).element(lr);else{const u=new Cg(s.array,16,1);this.buffer=u;const c=s.usage===zs?qo:Zr,l=[c(u,"vec4",16,0),c(u,"vec4",16,4),c(u,"vec4",16,8),c(u,"vec4",16,12)];r=Ts(...l)}this.instanceMatrixNode=r}if(n&&i===null){const u=new oa(n.array,3),c=n.usage===zs?qo:Zr;this.bufferColor=u,i=T(c(u,"vec3",3,0)),this.instanceColorNode=i}const a=r.mul(me).xyz;if(me.assign(a),e.hasGeometryAttribute("normal")){const u=kh(Xe,r);Xe.assign(u)}this.instanceColorNode!==null&&et("vec3","vInstanceColor").assign(this.instanceColorNode)}update(){this.instanceMatrix.usage!==zs&&this.buffer!==null&&this.instanceMatrix.version!==this.buffer.version&&(this.buffer.version=this.instanceMatrix.version),this.instanceColor&&this.instanceColor.usage!==zs&&this.bufferColor!==null&&this.instanceColor.version!==this.bufferColor.version&&(this.bufferColor.version=this.instanceColor.version)}}const Sx=C(Ep);class vx extends Ep{static get type(){return"InstancedMeshNode"}constructor(e){const{count:t,instanceMatrix:s,instanceColor:n}=e;super(t,s,n),this.instancedMesh=e}}const wp=C(vx);class Ax extends O{static get type(){return"BatchNode"}constructor(e){super("void"),this.batchMesh=e,this.batchingIdNode=null}setup(e){this.batchingIdNode===null&&(e.getDrawIndex()===null?this.batchingIdNode=lr:this.batchingIdNode=Cp);const s=b(([f])=>{const m=ns(ge(this.batchMesh._indirectTexture),0),x=y(f).modInt(y(m)),N=y(f).div(y(m));return ge(this.batchMesh._indirectTexture,Ae(x,N)).x}).setLayout({name:"getIndirectIndex",type:"uint",inputs:[{name:"id",type:"int"}]})(y(this.batchingIdNode)),n=this.batchMesh._matricesTexture,r=ns(ge(n),0),i=g(s).mul(4).toInt().toVar(),a=i.modInt(r),u=i.div(y(r)),c=Ts(ge(n,Ae(a,u)),ge(n,Ae(a.add(1),u)),ge(n,Ae(a.add(2),u)),ge(n,Ae(a.add(3),u))),l=this.batchMesh._colorsTexture;if(l!==null){const m=b(([x])=>{const N=ns(ge(l),0).x,v=x,w=v.modInt(N),B=v.div(N);return ge(l,Ae(w,B)).rgb}).setLayout({name:"getBatchingColor",type:"vec3",inputs:[{name:"id",type:"int"}]})(s);et("vec3","vBatchColor").assign(m)}const d=Oe(c);me.assign(c.mul(me));const h=Xe.div(T(d[0].dot(d[0]),d[1].dot(d[1]),d[2].dot(d[2]))),p=d.mul(h).xyz;Xe.assign(p),e.hasGeometryAttribute("tangent")&&or.mulAssign(d)}}const Mp=C(Ax),lc=new WeakMap;class Bp extends O{static get type(){return"SkinningNode"}constructor(e,t=!1){super("void"),this.skinnedMesh=e,this.useReference=t,this.updateType=W.OBJECT,this.skinIndexNode=we("skinIndex","uvec4"),this.skinWeightNode=we("skinWeight","vec4");let s,n,r;t?(s=ne("bindMatrix","mat4"),n=ne("bindMatrixInverse","mat4"),r=Ko("skeleton.boneMatrices","mat4",e.skeleton.bones.length)):(s=V(e.bindMatrix,"mat4"),n=V(e.bindMatrixInverse,"mat4"),r=ir(e.skeleton.boneMatrices,"mat4",e.skeleton.bones.length)),this.bindMatrixNode=s,this.bindMatrixInverseNode=n,this.boneMatricesNode=r,this.previousBoneMatricesNode=null}getSkinnedPosition(e=this.boneMatricesNode,t=me){const{skinIndexNode:s,skinWeightNode:n,bindMatrixNode:r,bindMatrixInverseNode:i}=this,a=e.element(s.x),u=e.element(s.y),c=e.element(s.z),l=e.element(s.w),d=r.mul(t),h=_e(a.mul(n.x).mul(d),u.mul(n.y).mul(d),c.mul(n.z).mul(d),l.mul(n.w).mul(d));return i.mul(h).xyz}getSkinnedNormal(e=this.boneMatricesNode,t=Xe){const{skinIndexNode:s,skinWeightNode:n,bindMatrixNode:r,bindMatrixInverseNode:i}=this,a=e.element(s.x),u=e.element(s.y),c=e.element(s.z),l=e.element(s.w);let d=_e(n.x.mul(a),n.y.mul(u),n.z.mul(c),n.w.mul(l));return d=i.mul(d).mul(r),d.transformDirection(t).xyz}getPreviousSkinnedPosition(e){const t=e.object;return this.previousBoneMatricesNode===null&&(t.skeleton.previousBoneMatrices=new Float32Array(t.skeleton.boneMatrices),this.previousBoneMatricesNode=Ko("skeleton.previousBoneMatrices","mat4",t.skeleton.bones.length)),this.getSkinnedPosition(this.previousBoneMatricesNode,Jr)}needsPreviousBoneMatrices(e){const t=e.renderer.getMRT();return t&&t.has("velocity")||ba(e.object).useVelocity===!0}setup(e){this.needsPreviousBoneMatrices(e)&&Jr.assign(this.getPreviousSkinnedPosition(e));const t=this.getSkinnedPosition();if(me.assign(t),e.hasGeometryAttribute("normal")){const s=this.getSkinnedNormal();Xe.assign(s),e.hasGeometryAttribute("tangent")&&or.assign(s)}}generate(e,t){if(t!=="void")return me.build(e,t)}update(e){const s=(this.useReference?e.object:this.skinnedMesh).skeleton;lc.get(s)!==e.frameId&&(lc.set(s,e.frameId),this.previousBoneMatricesNode!==null&&s.previousBoneMatrices.set(s.boneMatrices),s.update())}}const Rx=o=>E(new Bp(o)),Fp=o=>E(new Bp(o,!0));class Cx extends O{static get type(){return"LoopNode"}constructor(e=[]){super(),this.params=e}getVarName(e){return String.fromCharCode(105+e)}getProperties(e){const t=e.getNodeProperties(this);if(t.stackNode!==void 0)return t;const s={};for(let r=0,i=this.params.length-1;rNumber(d)?f=">=":f="<"));const x={start:l,end:d},N=x.start,v=x.end;let w="",B="",F="";m||(p==="int"||p==="uint"?f.includes("<")?m="++":m="--":f.includes("<")?m="+= 1.":m="-= 1."),w+=e.getVar(p,h)+" = "+N,B+=h+" "+f+" "+v,F+=h+" "+m;const L=`for ( ${w}; ${B}; ${F} )`;e.addFlowCode((a===0?` -`:"")+e.tab+L+` { - -`).addFlowTab()}const r=n.build(e,"void"),i=t.returnsNode?t.returnsNode.build(e):"";e.removeFlowTab().addFlowCode(` -`+e.tab+r);for(let a=0,u=this.params.length-1;aE(new Cx(xs(o,"int"))).append(),Ex=()=>rs("continue").append(),pu=()=>rs("break").append(),wx=(...o)=>(console.warn("TSL.LoopNode: loop() has been renamed to Loop()."),te(...o)),Gi=new WeakMap,Je=new Me,dc=b(({bufferMap:o,influence:e,stride:t,width:s,depth:n,offset:r})=>{const i=y(Rp).mul(t).add(r),a=i.div(s),u=i.sub(a.mul(s));return ge(o,Ae(u,a)).depth(n).mul(e)});function Mx(o){const e=o.morphAttributes.position!==void 0,t=o.morphAttributes.normal!==void 0,s=o.morphAttributes.color!==void 0,n=o.morphAttributes.position||o.morphAttributes.normal||o.morphAttributes.color,r=n!==void 0?n.length:0;let i=Gi.get(o);if(i===void 0||i.count!==r){let N=function(){m.dispose(),Gi.delete(o),o.removeEventListener("dispose",N)};i!==void 0&&i.texture.dispose();const a=o.morphAttributes.position||[],u=o.morphAttributes.normal||[],c=o.morphAttributes.color||[];let l=0;e===!0&&(l=1),t===!0&&(l=2),s===!0&&(l=3);let d=o.attributes.position.count*l,h=1;const p=4096;d>p&&(h=Math.ceil(d/p),d=p);const f=new Float32Array(d*h*4*r),m=new Yg(f,d,h,r);m.type=ot,m.needsUpdate=!0;const x=l*4;for(let v=0;v{const h=g(0).toVar();this.mesh.count>1&&this.mesh.morphTexture!==null&&this.mesh.morphTexture!==void 0?h.assign(ge(this.mesh.morphTexture,Ae(y(d).add(1),y(lr))).r):h.assign(ne("morphTargetInfluences","float").element(d).toVar()),s===!0&&me.addAssign(dc({bufferMap:a,influence:h,stride:u,width:l,depth:d,offset:y(0)})),n===!0&&Xe.addAssign(dc({bufferMap:a,influence:h,stride:u,width:l,depth:d,offset:y(1)}))})}update(){const e=this.morphBaseInfluence;this.mesh.geometry.morphTargetsRelative?e.value=1:e.value=1-this.mesh.morphTargetInfluences.reduce((t,s)=>t+s,0)}}const Up=C(Bx);class hn extends O{static get type(){return"LightingNode"}constructor(){super("vec3"),this.isLightingNode=!0}}class Fx extends hn{static get type(){return"AONode"}constructor(e=null){super(),this.aoNode=e}setup(e){e.context.ambientOcclusion.mulAssign(this.aoNode)}}class Ux extends hh{static get type(){return"LightingContextNode"}constructor(e,t=null,s=null,n=null){super(e),this.lightingModel=t,this.backdropNode=s,this.backdropAlphaNode=n,this._value=null}getContext(){const{backdropNode:e,backdropAlphaNode:t}=this,s=T().toVar("directDiffuse"),n=T().toVar("directSpecular"),r=T().toVar("indirectDiffuse"),i=T().toVar("indirectSpecular"),a={directDiffuse:s,directSpecular:n,indirectDiffuse:r,indirectSpecular:i};return{radiance:T().toVar("radiance"),irradiance:T().toVar("irradiance"),iblIrradiance:T().toVar("iblIrradiance"),ambientOcclusion:g(1).toVar("ambientOcclusion"),reflectedLight:a,backdrop:e,backdropAlpha:t}}setup(e){return this.value=this._value||(this._value=this.getContext()),this.value.lightingModel=this.lightingModel||e.context.lightingModel,super.setup(e)}}const Pp=C(Ux);class Px extends hn{static get type(){return"IrradianceNode"}constructor(e){super(),this.node=e}setup(e){e.context.irradiance.addAssign(this.node)}}let fn,gn;class Se extends O{static get type(){return"ScreenNode"}constructor(e){super(),this.scope=e,this.isViewportNode=!0}getNodeType(){return this.scope===Se.VIEWPORT?"vec4":"vec2"}getUpdateType(){let e=W.NONE;return(this.scope===Se.SIZE||this.scope===Se.VIEWPORT)&&(e=W.RENDER),this.updateType=e,e}update({renderer:e}){const t=e.getRenderTarget();this.scope===Se.VIEWPORT?t!==null?gn.copy(t.viewport):(e.getViewport(gn),gn.multiplyScalar(e.getPixelRatio())):t!==null?(fn.width=t.width,fn.height=t.height):e.getDrawingBufferSize(fn)}setup(){const e=this.scope;let t=null;return e===Se.SIZE?t=V(fn||(fn=new Ye)):e===Se.VIEWPORT?t=V(gn||(gn=new Me)):t=M(dr.div(Kn)),t}generate(e){if(this.scope===Se.COORDINATE){let t=e.getFragCoord();if(e.isFlipY()){const s=e.getNodeProperties(Kn).outputNode.build(e);t=`${e.getType("vec2")}( ${t}.x, ${s}.y - ${t}.y )`}return t}return super.generate(e)}}Se.COORDINATE="coordinate";Se.VIEWPORT="viewport";Se.SIZE="size";Se.UV="uv";const Bt=U(Se,Se.UV),Kn=U(Se,Se.SIZE),dr=U(Se,Se.COORDINATE),$t=U(Se,Se.VIEWPORT),Dp=$t.zw,Lp=dr.sub($t.xy),Dx=Lp.div(Dp),Lx=b(()=>(console.warn('TSL.ViewportNode: "viewportResolution" is deprecated. Use "screenSize" instead.'),Kn),"vec2").once()(),Ix=b(()=>(console.warn('TSL.ViewportNode: "viewportTopLeft" is deprecated. Use "screenUV" instead.'),Bt),"vec2").once()(),Vx=b(()=>(console.warn('TSL.ViewportNode: "viewportBottomLeft" is deprecated. Use "screenUV.flipY()" instead.'),Bt.flipY()),"vec2").once()(),mn=new Ye;class Ci extends wt{static get type(){return"ViewportTextureNode"}constructor(e=Bt,t=null,s=null){s===null&&(s=new bl,s.minFilter=ms),super(s,e,t),this.generateMipmaps=!1,this.isOutputTextureNode=!0,this.updateBeforeType=W.FRAME}updateBefore(e){const t=e.renderer;t.getDrawingBufferSize(mn);const s=this.value;(s.image.width!==mn.width||s.image.height!==mn.height)&&(s.image.width=mn.width,s.image.height=mn.height,s.needsUpdate=!0);const n=s.generateMipmaps;s.generateMipmaps=this.generateMipmaps,t.copyFramebufferToTexture(s),s.generateMipmaps=n}clone(){const e=new this.constructor(this.uvNode,this.levelNode,this.value);return e.generateMipmaps=this.generateMipmaps,e}}const Gx=C(Ci),fu=C(Ci,null,null,{generateMipmaps:!0});let Oi=null;class Ox extends Ci{static get type(){return"ViewportDepthTextureNode"}constructor(e=Bt,t=null){Oi===null&&(Oi=new vs),super(e,t,Oi)}}const gu=C(Ox);class qe extends O{static get type(){return"ViewportDepthNode"}constructor(e,t=null){super("float"),this.scope=e,this.valueNode=t,this.isViewportDepthNode=!0}generate(e){const{scope:t}=this;return t===qe.DEPTH_BASE?e.getFragDepth():super.generate(e)}setup({camera:e}){const{scope:t}=this,s=this.valueNode;let n=null;if(t===qe.DEPTH_BASE)s!==null&&(n=Vp().assign(s));else if(t===qe.DEPTH)e.isPerspectiveCamera?n=Ip(xe.z,Jt,es):n=Qs(xe.z,Jt,es);else if(t===qe.LINEAR_DEPTH)if(s!==null)if(e.isPerspectiveCamera){const r=mu(s,Jt,es);n=Qs(r,Jt,es)}else n=s;else n=Qs(xe.z,Jt,es);return n}}qe.DEPTH_BASE="depthBase";qe.DEPTH="depth";qe.LINEAR_DEPTH="linearDepth";const Qs=(o,e,t)=>o.add(e).div(e.sub(t)),kx=(o,e,t)=>e.sub(t).mul(o).sub(e),Ip=(o,e,t)=>e.add(o).mul(t).div(t.sub(e).mul(o)),mu=(o,e,t)=>e.mul(t).div(t.sub(e).mul(o).sub(t)),yu=(o,e,t)=>{e=e.max(1e-6).toVar();const s=vt(o.negate().div(e)),n=vt(t.div(e));return s.div(n)},zx=(o,e,t)=>{const s=o.mul(yi(t.div(e)));return g(Math.E).pow(s).mul(e).negate()},Vp=C(qe,qe.DEPTH_BASE),xu=U(qe,qe.DEPTH),ei=C(qe,qe.LINEAR_DEPTH),Wx=ei(gu());xu.assign=o=>Vp(o);class $x extends O{constructor(e){super("float"),this.name=e,this.isBuiltinNode=!0}generate(){return this.name}}const Hx=C($x);class ut extends O{static get type(){return"ClippingNode"}constructor(e=ut.DEFAULT){super(),this.scope=e}setup(e){super.setup(e);const t=e.clippingContext,{intersectionPlanes:s,unionPlanes:n}=t;return this.hardwareClipping=e.material.hardwareClipping,this.scope===ut.ALPHA_TO_COVERAGE?this.setupAlphaToCoverage(s,n):this.scope===ut.HARDWARE?this.setupHardwareClipping(n,e):this.setupDefault(s,n)}setupAlphaToCoverage(e,t){return b(()=>{const s=g().toVar("distanceToPlane"),n=g().toVar("distanceToGradient"),r=g(1).toVar("clipOpacity"),i=t.length;if(this.hardwareClipping===!1&&i>0){const u=Gt(t);te(i,({i:c})=>{const l=u.element(c);s.assign(xe.dot(l.xyz).negate().add(l.w)),n.assign(s.fwidth().div(2)),r.mulAssign(ct(n.negate(),n,s))})}const a=e.length;if(a>0){const u=Gt(e),c=g(1).toVar("intersectionClipOpacity");te(a,({i:l})=>{const d=u.element(l);s.assign(xe.dot(d.xyz).negate().add(d.w)),n.assign(s.fwidth().div(2)),c.mulAssign(ct(n.negate(),n,s).oneMinus())}),r.mulAssign(c.oneMinus())}ee.a.mulAssign(r),ee.a.equal(0).discard()})()}setupDefault(e,t){return b(()=>{const s=t.length;if(this.hardwareClipping===!1&&s>0){const r=Gt(t);te(s,({i})=>{const a=r.element(i);xe.dot(a.xyz).greaterThan(a.w).discard()})}const n=e.length;if(n>0){const r=Gt(e),i=Ht(!0).toVar("clipped");te(n,({i:a})=>{const u=r.element(a);i.assign(xe.dot(u.xyz).greaterThan(u.w).and(i))}),i.discard()}})()}setupHardwareClipping(e,t){const s=e.length;return t.enableHardwareClipping(s),b(()=>{const n=Gt(e),r=Hx(t.getClipDistance());te(s,({i})=>{const a=n.element(i),u=xe.dot(a.xyz).sub(a.w).negate();r.element(i).assign(u)})})()}}ut.ALPHA_TO_COVERAGE="alphaToCoverage";ut.DEFAULT="default";ut.HARDWARE="hardware";const qx=()=>E(new ut),Kx=()=>E(new ut(ut.ALPHA_TO_COVERAGE)),Xx=()=>E(new ut(ut.HARDWARE)),Yx=.05,hc=b(([o])=>Xt($(1e4,st($(17,o.x).add($(.1,o.y)))).mul(_e(.1,oe(st($(13,o.y).add(o.x))))))),pc=b(([o])=>hc(M(hc(o.xy),o.z))),jx=b(([o])=>{const e=ue(zt(Ha(o.xyz)),zt(qa(o.xyz))),t=g(1).div(g(Yx).mul(e)).toVar("pixScale"),s=M(rn(At(vt(t))),rn(xi(vt(t)))),n=M(pc(At(s.x.mul(o.xyz))),pc(At(s.y.mul(o.xyz)))),r=Xt(vt(t)),i=_e($(r.oneMinus(),n.x),$(r,n.y)),a=Re(r,r.oneMinus()),u=T(i.mul(i).div($(2,a).mul(Y(1,a))),i.sub($(.5,a)).div(Y(1,a)),Y(1,Y(1,i).mul(Y(1,i)).div($(2,a).mul(Y(1,a))))),c=i.lessThan(a.oneMinus()).select(i.lessThan(a).select(u.x,u.y),u.z);return Et(c,1e-6,1)}).setLayout({name:"getAlphaHashThreshold",type:"float",inputs:[{name:"position",type:"vec3"}]});class ce extends Xu{static get type(){return"NodeMaterial"}get type(){return this.constructor.type}set type(e){}constructor(){super(),this.isNodeMaterial=!0,this.fog=!0,this.lights=!1,this.hardwareClipping=!1,this.lightsNode=null,this.envNode=null,this.aoNode=null,this.colorNode=null,this.normalNode=null,this.opacityNode=null,this.backdropNode=null,this.backdropAlphaNode=null,this.alphaTestNode=null,this.positionNode=null,this.geometryNode=null,this.depthNode=null,this.shadowPositionNode=null,this.receivedShadowNode=null,this.castShadowNode=null,this.outputNode=null,this.mrtNode=null,this.fragmentNode=null,this.vertexNode=null}customProgramCacheKey(){return this.type+ma(this)}build(e){this.setup(e)}setupObserver(e){return new jm(e)}setup(e){e.context.setupNormal=()=>this.setupNormal(e),e.context.setupPositionView=()=>this.setupPositionView(e),e.context.setupModelViewProjection=()=>this.setupModelViewProjection(e);const t=e.renderer,s=t.getRenderTarget();e.addStack();const n=this.vertexNode||this.setupVertex(e);e.stack.outputNode=n,this.setupHardwareClipping(e),this.geometryNode!==null&&(e.stack.outputNode=e.stack.outputNode.bypass(this.geometryNode)),e.addFlow("vertex",e.removeStack()),e.addStack();let r;const i=this.setupClipping(e);if((this.depthWrite===!0||this.depthTest===!0)&&(s!==null?s.depthBuffer===!0&&this.setupDepth(e):t.depth===!0&&this.setupDepth(e)),this.fragmentNode===null){this.setupDiffuseColor(e),this.setupVariants(e);const a=this.setupLighting(e);i!==null&&e.stack.add(i);const u=P(a,ee.a).max(0);if(r=this.setupOutput(e,u),$n.assign(r),this.outputNode!==null&&(r=this.outputNode),s!==null){const c=t.getMRT(),l=this.mrtNode;c!==null?(r=c,l!==null&&(r=c.merge(l))):l!==null&&(r=l)}}else{let a=this.fragmentNode;a.isOutputStructNode!==!0&&(a=P(a)),r=this.setupOutput(e,a)}e.stack.outputNode=r,e.addFlow("fragment",e.removeStack()),e.monitor=this.setupObserver(e)}setupClipping(e){if(e.clippingContext===null)return null;const{unionPlanes:t,intersectionPlanes:s}=e.clippingContext;let n=null;if(t.length>0||s.length>0){const r=e.renderer.samples;this.alphaToCoverage&&r>1?n=Kx():e.stack.add(qx())}return n}setupHardwareClipping(e){if(this.hardwareClipping=!1,e.clippingContext===null)return;const t=e.clippingContext.unionPlanes.length;t>0&&t<=8&&e.isAvailable("clipDistance")&&(e.stack.add(Xx()),this.hardwareClipping=!0)}setupDepth(e){const{renderer:t,camera:s}=e;let n=this.depthNode;if(n===null){const r=t.getMRT();r&&r.has("depth")?n=r.get("depth"):t.logarithmicDepthBuffer===!0&&(s.isPerspectiveCamera?n=yu(xe.z,Jt,es):n=Qs(xe.z,Jt,es))}n!==null&&xu.assign(n).append()}setupPositionView(){return Kt.mul(me).xyz}setupModelViewProjection(){return He.mul(xe)}setupVertex(e){return e.addStack(),this.setupPosition(e),e.context.vertex=e.removeStack(),hu}setupPosition(e){const{object:t,geometry:s}=e;if((s.morphAttributes.position||s.morphAttributes.normal||s.morphAttributes.color)&&Up(t).append(),t.isSkinnedMesh===!0&&Fp(t).append(),this.displacementMap){const n=dt("displacementMap","texture"),r=dt("displacementScale","float"),i=dt("displacementBias","float");me.addAssign(Xe.normalize().mul(n.x.mul(r).add(i)))}return t.isBatchedMesh&&Mp(t).append(),t.isInstancedMesh&&t.instanceMatrix&&t.instanceMatrix.isInstancedBufferAttribute===!0&&wp(t).append(),this.positionNode!==null&&me.assign(this.positionNode.context({isPositionNodeInput:!0})),me}setupDiffuseColor({object:e,geometry:t}){let s=this.colorNode?P(this.colorNode):an;this.vertexColors===!0&&t.hasAttribute("color")&&(s=P(s.xyz.mul(we("color","vec3")),s.a)),e.instanceColor&&(s=et("vec3","vInstanceColor").mul(s)),e.isBatchedMesh&&e._colorsTexture&&(s=et("vec3","vBatchColor").mul(s)),ee.assign(s);const n=this.opacityNode?g(this.opacityNode):cr;if(ee.a.assign(ee.a.mul(n)),this.alphaTestNode!==null||this.alphaTest>0){const r=this.alphaTestNode!==null?g(this.alphaTestNode):ep;ee.a.lessThanEqual(r).discard()}this.alphaHash===!0&&ee.a.lessThan(jx(me)).discard(),this.transparent===!1&&this.blending===Ys&&this.alphaToCoverage===!1&&ee.a.assign(1)}setupVariants(){}setupOutgoingLight(){return this.lights===!0?T(0):ee.rgb}setupNormal(){return this.normalNode?T(this.normalNode):ap}setupEnvironment(){let e=null;return this.envNode?e=this.envNode:this.envMap&&(e=this.envMap.isCubeTexture?dt("envMap","cubeTexture"):dt("envMap","texture")),e}setupLightMap(e){let t=null;return e.material.lightMap&&(t=new Px(du)),t}setupLights(e){const t=[],s=this.setupEnvironment(e);s&&s.isLightingNode&&t.push(s);const n=this.setupLightMap(e);if(n&&n.isLightingNode&&t.push(n),this.aoNode!==null||e.material.aoMap){const i=this.aoNode!==null?this.aoNode:Ap;t.push(new Fx(i))}let r=this.lightsNode||e.lightsNode;return t.length>0&&(r=e.renderer.lighting.createNode([...r.getLights(),...t])),r}setupLightingModel(){}setupLighting(e){const{material:t}=e,{backdropNode:s,backdropAlphaNode:n,emissiveNode:r}=this,a=this.lights===!0||this.lightsNode!==null?this.setupLights(e):null;let u=this.setupOutgoingLight(e);if(a&&a.getScope().hasLights){const c=this.setupLightingModel(e);u=Pp(a,c,s,n)}else s!==null&&(u=T(n!==null?Q(u,s,n):s));return(r&&r.isNode===!0||t.emissive&&t.emissive.isColor===!0)&&(Ho.assign(T(r||sp)),u=u.add(Ho)),u}setupOutput(e,t){if(this.fog===!0){const s=e.fogNode;s&&($n.assign(t),t=P(s))}return t}setDefaultValues(e){for(const s in e){const n=e[s];this[s]===void 0&&(this[s]=n,n&&n.clone&&(this[s]=n.clone()))}const t=Object.getOwnPropertyDescriptors(e.constructor.prototype);for(const s in t)Object.getOwnPropertyDescriptor(this.constructor.prototype,s)===void 0&&t[s].get!==void 0&&Object.defineProperty(this.constructor.prototype,s,t[s])}toJSON(e){const t=e===void 0||typeof e=="string";t&&(e={textures:{},images:{},nodes:{}});const s=Xu.prototype.toJSON.call(this,e),n=Vn(this);s.inputNodes={};for(const{property:i,childNode:a}of n)s.inputNodes[i]=a.toJSON(e).uuid;function r(i){const a=[];for(const u in i){const c=i[u];delete c.metadata,a.push(c)}return a}if(t){const i=r(e.textures),a=r(e.images),u=r(e.nodes);i.length>0&&(s.textures=i),a.length>0&&(s.images=a),u.length>0&&(s.nodes=u)}return s}copy(e){return this.lightsNode=e.lightsNode,this.envNode=e.envNode,this.colorNode=e.colorNode,this.normalNode=e.normalNode,this.opacityNode=e.opacityNode,this.backdropNode=e.backdropNode,this.backdropAlphaNode=e.backdropAlphaNode,this.alphaTestNode=e.alphaTestNode,this.positionNode=e.positionNode,this.geometryNode=e.geometryNode,this.depthNode=e.depthNode,this.shadowPositionNode=e.shadowPositionNode,this.receivedShadowNode=e.receivedShadowNode,this.castShadowNode=e.castShadowNode,this.outputNode=e.outputNode,this.mrtNode=e.mrtNode,this.fragmentNode=e.fragmentNode,this.vertexNode=e.vertexNode,super.copy(e)}}const Qx=new hl;class dR extends ce{static get type(){return"InstancedPointsNodeMaterial"}constructor(e={}){super(),this.isInstancedPointsNodeMaterial=!0,this.useColor=e.vertexColors,this.pointWidth=1,this.pointColorNode=null,this.pointWidthNode=null,this._useAlphaToCoverage=!0,this.setDefaultValues(Qx),this.setValues(e)}setup(e){const{renderer:t}=e,s=this._useAlphaToCoverage,n=this.useColor;this.vertexNode=b(()=>{const r=we("instancePosition").xyz,i=P(Kt.mul(P(r,1))),a=$t.z.div($t.w),u=He.mul(i),c=Ce.xy.toVar();return c.mulAssign(this.pointWidthNode?this.pointWidthNode:Sp),c.assign(c.div($t.z)),c.y.assign(c.y.mul(a)),c.assign(c.mul(u.w)),u.addAssign(P(c,0,0)),u})(),this.fragmentNode=b(()=>{const r=g(1).toVar(),i=ja(le().mul(2).sub(1));if(s&&t.samples>1){const u=g(i.fwidth()).toVar();r.assign(ct(u.oneMinus(),u.add(1),i).oneMinus())}else i.greaterThan(1).discard();let a;return this.pointColorNode?a=this.pointColorNode:n?a=we("instanceColor").mul(an):a=an,r.mulAssign(cr),P(a,r)})(),super.setup(e)}get alphaToCoverage(){return this._useAlphaToCoverage}set alphaToCoverage(e){this._useAlphaToCoverage!==e&&(this._useAlphaToCoverage=e,this.needsUpdate=!0)}}const Zx=new Eg;class Jx extends ce{static get type(){return"LineBasicNodeMaterial"}constructor(e){super(),this.isLineBasicNodeMaterial=!0,this.setDefaultValues(Zx),this.setValues(e)}}const eT=new pl;class tT extends ce{static get type(){return"LineDashedNodeMaterial"}constructor(e){super(),this.isLineDashedNodeMaterial=!0,this.setDefaultValues(eT),this.dashOffset=0,this.offsetNode=null,this.dashScaleNode=null,this.dashSizeNode=null,this.gapSizeNode=null,this.setValues(e)}setupVariants(){const e=this.offsetNode?g(this.offsetNode):lu,t=this.dashScaleNode?g(this.dashScaleNode):au,s=this.dashSizeNode?g(this.dashSizeNode):uu,n=this.gapSizeNode?g(this.gapSizeNode):cu;bs.assign(s),Hn.assign(n);const r=ke(we("lineDistance").mul(t));(e?r.add(e):r).mod(bs.add(Hn)).greaterThan(bs).discard()}}let ki=null;class sT extends Ci{static get type(){return"ViewportSharedTextureNode"}constructor(e=Bt,t=null){ki===null&&(ki=new bl),super(e,t,ki)}updateReference(){return this}}const Gp=C(sT),nT=new pl;class hR extends ce{static get type(){return"Line2NodeMaterial"}constructor(e={}){super(),this.isLine2NodeMaterial=!0,this.setDefaultValues(nT),this.useColor=e.vertexColors,this.dashOffset=0,this.lineWidth=1,this.lineColorNode=null,this.offsetNode=null,this.dashScaleNode=null,this.dashSizeNode=null,this.gapSizeNode=null,this.blending=en,this._useDash=e.dashed,this._useAlphaToCoverage=!0,this._useWorldUnits=!1,this.setValues(e)}setup(e){const{renderer:t}=e,s=this._useAlphaToCoverage,n=this.useColor,r=this._useDash,i=this._useWorldUnits,a=b(({start:c,end:l})=>{const d=He.element(2).element(2),f=He.element(3).element(2).mul(-.5).div(d).sub(c.z).div(l.z.sub(c.z));return P(Q(c.xyz,l.xyz,f),l.w)}).setLayout({name:"trimSegment",type:"vec4",inputs:[{name:"start",type:"vec4"},{name:"end",type:"vec4"}]});this.vertexNode=b(()=>{const c=we("instanceStart"),l=we("instanceEnd"),d=P(Kt.mul(P(c,1))).toVar("start"),h=P(Kt.mul(P(l,1))).toVar("end");if(r){const F=this.dashScaleNode?g(this.dashScaleNode):au,L=this.offsetNode?g(this.offsetNode):lu,I=we("instanceDistanceStart"),G=we("instanceDistanceEnd");let X=Ce.y.lessThan(.5).select(F.mul(I),F.mul(G));X=X.add(L),et("float","lineDistance").assign(X)}i&&(et("vec3","worldStart").assign(d.xyz),et("vec3","worldEnd").assign(h.xyz));const p=$t.z.div($t.w),f=He.element(2).element(3).equal(-1);z(f,()=>{z(d.z.lessThan(0).and(h.z.greaterThan(0)),()=>{h.assign(a({start:d,end:h}))}).ElseIf(h.z.lessThan(0).and(d.z.greaterThanEqual(0)),()=>{d.assign(a({start:h,end:d}))})});const m=He.mul(d),x=He.mul(h),N=m.xyz.div(m.w),v=x.xyz.div(x.w),w=v.xy.sub(N.xy).toVar();w.x.assign(w.x.mul(p)),w.assign(w.normalize());const B=P().toVar();if(i){const F=h.xyz.sub(d.xyz).normalize(),L=Q(d.xyz,h.xyz,.5).normalize(),I=F.cross(L).normalize(),G=F.cross(I),X=et("vec4","worldPos");X.assign(Ce.y.lessThan(.5).select(d,h));const ie=Or.mul(.5);X.addAssign(P(Ce.x.lessThan(0).select(I.mul(ie),I.mul(ie).negate()),0)),r||(X.addAssign(P(Ce.y.lessThan(.5).select(F.mul(ie).negate(),F.mul(ie)),0)),X.addAssign(P(G.mul(ie),0)),z(Ce.y.greaterThan(1).or(Ce.y.lessThan(0)),()=>{X.subAssign(P(G.mul(2).mul(ie),0))})),B.assign(He.mul(X));const Z=T().toVar();Z.assign(Ce.y.lessThan(.5).select(N,v)),B.z.assign(Z.z.mul(B.w))}else{const F=M(w.y,w.x.negate()).toVar("offset");w.x.assign(w.x.div(p)),F.x.assign(F.x.div(p)),F.assign(Ce.x.lessThan(0).select(F.negate(),F)),z(Ce.y.lessThan(0),()=>{F.assign(F.sub(w))}).ElseIf(Ce.y.greaterThan(1),()=>{F.assign(F.add(w))}),F.assign(F.mul(Or)),F.assign(F.div($t.w)),B.assign(Ce.y.lessThan(.5).select(m,x)),F.assign(F.mul(B.w)),B.assign(B.add(P(F,0,0)))}return B})();const u=b(({p1:c,p2:l,p3:d,p4:h})=>{const p=c.sub(d),f=h.sub(d),m=l.sub(c),x=p.dot(f),N=f.dot(m),v=p.dot(m),w=f.dot(f),F=m.dot(m).mul(w).sub(N.mul(N)),I=x.mul(N).sub(v.mul(w)).div(F).clamp(),G=x.add(N.mul(I)).div(w).clamp();return M(I,G)});if(this.colorNode=b(()=>{const c=le();if(r){const h=this.dashSizeNode?g(this.dashSizeNode):uu,p=this.gapSizeNode?g(this.gapSizeNode):cu;bs.assign(h),Hn.assign(p);const f=et("float","lineDistance");c.y.lessThan(-1).or(c.y.greaterThan(1)).discard(),f.mod(bs.add(Hn)).greaterThan(bs).discard()}const l=g(1).toVar("alpha");if(i){const h=et("vec3","worldStart"),p=et("vec3","worldEnd"),f=et("vec4","worldPos").xyz.normalize().mul(1e5),m=p.sub(h),x=u({p1:h,p2:p,p3:T(0,0,0),p4:f}),N=h.add(m.mul(x.x)),v=f.mul(x.y),F=N.sub(v).length().div(Or);if(!r)if(s&&t.samples>1){const L=F.fwidth();l.assign(ct(L.negate().add(.5),L.add(.5),F).oneMinus())}else F.greaterThan(.5).discard()}else if(s&&t.samples>1){const h=c.x,p=c.y.greaterThan(0).select(c.y.sub(1),c.y.add(1)),f=h.mul(h).add(p.mul(p)),m=g(f.fwidth()).toVar("dlen");z(c.y.abs().greaterThan(1),()=>{l.assign(ct(m.oneMinus(),m.add(1),f).oneMinus())})}else z(c.y.abs().greaterThan(1),()=>{const h=c.x,p=c.y.greaterThan(0).select(c.y.sub(1),c.y.add(1));h.mul(h).add(p.mul(p)).greaterThan(1).discard()});let d;if(this.lineColorNode)d=this.lineColorNode;else if(n){const h=we("instanceColorStart"),p=we("instanceColorEnd");d=Ce.y.lessThan(.5).select(h,p).mul(an)}else d=an;return P(d,l)})(),this.transparent){const c=this.opacityNode?g(this.opacityNode):cr;this.outputNode=P(this.colorNode.rgb.mul(c).add(Gp().rgb.mul(c.oneMinus())),this.colorNode.a)}super.setup(e)}get worldUnits(){return this._useWorldUnits}set worldUnits(e){this._useWorldUnits!==e&&(this._useWorldUnits=e,this.needsUpdate=!0)}get dashed(){return this._useDash}set dashed(e){this._useDash!==e&&(this._useDash=e,this.needsUpdate=!0)}get alphaToCoverage(){return this._useAlphaToCoverage}set alphaToCoverage(e){this._useAlphaToCoverage!==e&&(this._useAlphaToCoverage=e,this.needsUpdate=!0)}}const Op=o=>E(o).mul(.5).add(.5),rT=o=>E(o).mul(2).sub(1),iT=new Bg;class oT extends ce{static get type(){return"MeshNormalNodeMaterial"}constructor(e){super(),this.isMeshNormalNodeMaterial=!0,this.setDefaultValues(iT),this.setValues(e)}setupDiffuseColor(){const e=this.opacityNode?g(this.opacityNode):cr;ee.assign(P(Op(fe),e))}}class aT extends be{static get type(){return"EquirectUVNode"}constructor(e=nu){super("vec2"),this.dirNode=e}setup(){const e=this.dirNode,t=e.z.atan(e.x).mul(1/(Math.PI*2)).add(.5),s=e.y.clamp(-1,1).asin().mul(1/Math.PI).add(.5);return M(t,s)}}const Tu=C(aT);class kp extends jg{constructor(e=1,t={}){super(e,t),this.isCubeRenderTarget=!0}fromEquirectangularTexture(e,t){const s=t.minFilter,n=t.generateMipmaps;t.generateMipmaps=!0,this.texture.type=t.type,this.texture.colorSpace=t.colorSpace,this.texture.generateMipmaps=t.generateMipmaps,this.texture.minFilter=t.minFilter,this.texture.magFilter=t.magFilter;const r=new _l(5,5,5),i=Tu(nu),a=new ce;a.colorNode=K(t,i,0),a.side=Ct,a.blending=en;const u=new tn(r,a),c=new Nl;c.add(u),t.minFilter===ms&&(t.minFilter=ft);const l=new Qg(1,10,this),d=e.getMRT();return e.setMRT(null),l.update(e,c),e.setMRT(d),t.minFilter=s,t.currentGenerateMipmaps=n,u.geometry.dispose(),u.material.dispose(),this}}const Fn=new WeakMap;class uT extends be{static get type(){return"CubeMapNode"}constructor(e){super("vec3"),this.envNode=e,this._cubeTexture=null,this._cubeTextureNode=on();const t=new td;t.isRenderTargetTexture=!0,this._defaultTexture=t,this.updateBeforeType=W.RENDER}updateBefore(e){const{renderer:t,material:s}=e,n=this.envNode;if(n.isTextureNode||n.isMaterialReferenceNode){const r=n.isTextureNode?n.value:s[n.property];if(r&&r.isTexture){const i=r.mapping;if(i===jn||i===Qn){if(Fn.has(r)){const a=Fn.get(r);fc(a,r.mapping),this._cubeTexture=a}else{const a=r.image;if(cT(a)){const u=new kp(a.height);u.fromEquirectangularTexture(t,r),fc(u.texture,r.mapping),this._cubeTexture=u.texture,Fn.set(r,u.texture),r.addEventListener("dispose",zp)}else this._cubeTexture=this._defaultTexture}this._cubeTextureNode.value=this._cubeTexture}else this._cubeTextureNode=this.envNode}}}setup(e){return this.updateBefore(e),this._cubeTextureNode}}function cT(o){return o==null?!1:o.height>0}function zp(o){const e=o.target;e.removeEventListener("dispose",zp);const t=Fn.get(e);t!==void 0&&(Fn.delete(e),t.dispose())}function fc(o,e){e===jn?o.mapping=Zs:e===Qn&&(o.mapping=Js)}const Wp=C(uT);class _u extends hn{static get type(){return"BasicEnvironmentNode"}constructor(e=null){super(),this.envNode=e}setup(e){e.context.environment=Wp(this.envNode)}}class lT extends hn{static get type(){return"BasicLightMapNode"}constructor(e=null){super(),this.lightMapNode=e}setup(e){const t=g(1/Math.PI);e.context.irradianceLightMap=this.lightMapNode.mul(t)}}class Ei{start(){}finish(){}direct(){}directRectArea(){}indirect(){}ambientOcclusion(){}}class $p extends Ei{constructor(){super()}indirect(e,t,s){const n=e.ambientOcclusion,r=e.reflectedLight,i=s.context.irradianceLightMap;r.indirectDiffuse.assign(P(0)),i?r.indirectDiffuse.addAssign(i):r.indirectDiffuse.addAssign(P(1,1,1,0)),r.indirectDiffuse.mulAssign(n),r.indirectDiffuse.mulAssign(ee.rgb)}finish(e,t,s){const n=s.material,r=e.outgoingLight,i=s.context.environment;if(i)switch(n.combine){case Xg:r.rgb.assign(Q(r.rgb,r.rgb.mul(i.rgb),Bn.mul(Gr)));break;case Kg:r.rgb.assign(Q(r.rgb,i.rgb,Bn.mul(Gr)));break;case qg:r.rgb.addAssign(i.rgb.mul(Bn.mul(Gr)));break;default:console.warn("THREE.BasicLightingModel: Unsupported .combine value:",n.combine);break}}}const dT=new fl;class hT extends ce{static get type(){return"MeshBasicNodeMaterial"}constructor(e){super(),this.isMeshBasicNodeMaterial=!0,this.lights=!0,this.setDefaultValues(dT),this.setValues(e)}setupNormal(){return lt}setupEnvironment(e){const t=super.setupEnvironment(e);return t?new _u(t):null}setupLightMap(e){let t=null;return e.material.lightMap&&(t=new lT(du)),t}setupOutgoingLight(){return ee.rgb}setupLightingModel(){return new $p}}const un=b(({f0:o,f90:e,dotVH:t})=>{const s=t.mul(-5.55473).sub(6.98316).mul(t).exp2();return o.mul(s.oneMinus()).add(e.mul(s))}),Ns=b(o=>o.diffuseColor.mul(1/Math.PI)),pT=()=>g(.25),fT=b(({dotNH:o})=>Yr.mul(g(.5)).add(1).mul(g(1/Math.PI)).mul(o.pow(Yr))),gT=b(({lightDirection:o})=>{const e=o.add(ae).normalize(),t=fe.dot(e).clamp(),s=ae.dot(e).clamp(),n=un({f0:We,f90:1,dotVH:s}),r=pT(),i=fT({dotNH:t});return n.mul(r).mul(i)});class Hp extends $p{constructor(e=!0){super(),this.specular=e}direct({lightDirection:e,lightColor:t,reflectedLight:s}){const r=fe.dot(e).clamp().mul(t);s.directDiffuse.addAssign(r.mul(Ns({diffuseColor:ee.rgb}))),this.specular===!0&&s.directSpecular.addAssign(r.mul(gT({lightDirection:e})).mul(Bn))}indirect({ambientOcclusion:e,irradiance:t,reflectedLight:s}){s.indirectDiffuse.addAssign(t.mul(Ns({diffuseColor:ee}))),s.indirectDiffuse.mulAssign(e)}}const mT=new wg;class yT extends ce{static get type(){return"MeshLambertNodeMaterial"}constructor(e){super(),this.isMeshLambertNodeMaterial=!0,this.lights=!0,this.setDefaultValues(mT),this.setValues(e)}setupEnvironment(e){const t=super.setupEnvironment(e);return t?new _u(t):null}setupLightingModel(){return new Hp(!1)}}const xT=new Fg;class TT extends ce{static get type(){return"MeshPhongNodeMaterial"}constructor(e){super(),this.isMeshPhongNodeMaterial=!0,this.lights=!0,this.shininessNode=null,this.specularNode=null,this.setDefaultValues(xT),this.setValues(e)}setupEnvironment(e){const t=super.setupEnvironment(e);return t?new _u(t):null}setupLightingModel(){return new Hp}setupVariants(){const e=(this.shininessNode?g(this.shininessNode):tp).max(1e-4);Yr.assign(e);const t=this.specularNode||np;We.assign(t)}copy(e){return this.shininessNode=e.shininessNode,this.specularNode=e.specularNode,super.copy(e)}}const qp=b(o=>{if(o.geometry.hasAttribute("normal")===!1)return g(0);const e=lt.dFdx().abs().max(lt.dFdy().abs());return e.x.max(e.y).max(e.z)}),bu=b(o=>{const{roughness:e}=o,t=qp();let s=e.max(.0525);return s=s.add(t),s=s.min(1),s}),Kp=b(({alpha:o,dotNL:e,dotNV:t})=>{const s=o.pow2(),n=e.mul(s.add(s.oneMinus().mul(t.pow2())).sqrt()),r=t.mul(s.add(s.oneMinus().mul(e.pow2())).sqrt());return gt(.5,n.add(r).max(Gd))}).setLayout({name:"V_GGX_SmithCorrelated",type:"float",inputs:[{name:"alpha",type:"float"},{name:"dotNL",type:"float"},{name:"dotNV",type:"float"}]}),_T=b(({alphaT:o,alphaB:e,dotTV:t,dotBV:s,dotTL:n,dotBL:r,dotNV:i,dotNL:a})=>{const u=a.mul(T(o.mul(t),e.mul(s),i).length()),c=i.mul(T(o.mul(n),e.mul(r),a).length());return gt(.5,u.add(c)).saturate()}).setLayout({name:"V_GGX_SmithCorrelated_Anisotropic",type:"float",inputs:[{name:"alphaT",type:"float",qualifier:"in"},{name:"alphaB",type:"float",qualifier:"in"},{name:"dotTV",type:"float",qualifier:"in"},{name:"dotBV",type:"float",qualifier:"in"},{name:"dotTL",type:"float",qualifier:"in"},{name:"dotBL",type:"float",qualifier:"in"},{name:"dotNV",type:"float",qualifier:"in"},{name:"dotNL",type:"float",qualifier:"in"}]}),Xp=b(({alpha:o,dotNH:e})=>{const t=o.pow2(),s=e.pow2().mul(t.oneMinus()).oneMinus();return t.div(s.pow2()).mul(1/Math.PI)}).setLayout({name:"D_GGX",type:"float",inputs:[{name:"alpha",type:"float"},{name:"dotNH",type:"float"}]}),bT=g(1/Math.PI),NT=b(({alphaT:o,alphaB:e,dotNH:t,dotTH:s,dotBH:n})=>{const r=o.mul(e),i=T(e.mul(s),o.mul(n),r.mul(t)),a=i.dot(i),u=r.div(a);return bT.mul(r.mul(u.pow2()))}).setLayout({name:"D_GGX_Anisotropic",type:"float",inputs:[{name:"alphaT",type:"float",qualifier:"in"},{name:"alphaB",type:"float",qualifier:"in"},{name:"dotNH",type:"float",qualifier:"in"},{name:"dotTH",type:"float",qualifier:"in"},{name:"dotBH",type:"float",qualifier:"in"}]}),jo=b(o=>{const{lightDirection:e,f0:t,f90:s,roughness:n,f:r,USE_IRIDESCENCE:i,USE_ANISOTROPY:a}=o,u=o.normalView||fe,c=n.pow2(),l=e.add(ae).normalize(),d=u.dot(e).clamp(),h=u.dot(ae).clamp(),p=u.dot(l).clamp(),f=ae.dot(l).clamp();let m=un({f0:t,f90:s,dotVH:f}),x,N;if(Gn(i)&&(m=mi.mix(m,r)),Gn(a)){const v=En.dot(e),w=En.dot(ae),B=En.dot(l),F=_s.dot(e),L=_s.dot(ae),I=_s.dot(l);x=_T({alphaT:Xr,alphaB:c,dotTV:w,dotBV:L,dotTL:v,dotBL:F,dotNV:h,dotNL:d}),N=NT({alphaT:Xr,alphaB:c,dotNH:p,dotTH:B,dotBH:I})}else x=Kp({alpha:c,dotNL:d,dotNV:h}),N=Xp({alpha:c,dotNH:p});return m.mul(x).mul(N)}),Nu=b(({roughness:o,dotNV:e})=>{const t=P(-1,-.0275,-.572,.022),s=P(1,.0425,1.04,-.04),n=o.mul(t).add(s),r=n.x.mul(n.x).min(e.mul(-9.28).exp2()).mul(n.x).add(n.y);return M(-1.04,1.04).mul(r).add(n.zw)}).setLayout({name:"DFGApprox",type:"vec2",inputs:[{name:"roughness",type:"float"},{name:"dotNV",type:"vec3"}]}),Yp=b(o=>{const{dotNV:e,specularColor:t,specularF90:s,roughness:n}=o,r=Nu({dotNV:e,roughness:n});return t.mul(r.x).add(s.mul(r.y))}),jp=b(({f:o,f90:e,dotVH:t})=>{const s=t.oneMinus().saturate(),n=s.mul(s),r=s.mul(n,n).clamp(0,.9999);return o.sub(T(e).mul(r)).div(r.oneMinus())}).setLayout({name:"Schlick_to_F0",type:"vec3",inputs:[{name:"f",type:"vec3"},{name:"f90",type:"float"},{name:"dotVH",type:"float"}]}),ST=b(({roughness:o,dotNH:e})=>{const t=o.pow2(),s=g(1).div(t),r=e.pow2().oneMinus().max(.0078125);return g(2).add(s).mul(r.pow(s.mul(.5))).div(2*Math.PI)}).setLayout({name:"D_Charlie",type:"float",inputs:[{name:"roughness",type:"float"},{name:"dotNH",type:"float"}]}),vT=b(({dotNV:o,dotNL:e})=>g(1).div(g(4).mul(e.add(o).sub(e.mul(o))))).setLayout({name:"V_Neubelt",type:"float",inputs:[{name:"dotNV",type:"float"},{name:"dotNL",type:"float"}]}),AT=b(({lightDirection:o})=>{const e=o.add(ae).normalize(),t=fe.dot(o).clamp(),s=fe.dot(ae).clamp(),n=fe.dot(e).clamp(),r=ST({roughness:gi,dotNH:n}),i=vT({dotNV:s,dotNL:t});return fs.mul(r).mul(i)}),RT=b(({N:o,V:e,roughness:t})=>{const r=.0078125,i=o.dot(e).saturate(),a=M(t,i.oneMinus().sqrt());return a.assign(a.mul(.984375).add(r)),a}).setLayout({name:"LTC_Uv",type:"vec2",inputs:[{name:"N",type:"vec3"},{name:"V",type:"vec3"},{name:"roughness",type:"float"}]}),CT=b(({f:o})=>{const e=o.length();return ue(e.mul(e).add(o.z).div(e.add(1)),0)}).setLayout({name:"LTC_ClippedSphereFormFactor",type:"float",inputs:[{name:"f",type:"vec3"}]}),gr=b(({v1:o,v2:e})=>{const t=o.dot(e),s=t.abs().toVar(),n=s.mul(.0145206).add(.4965155).mul(s).add(.8543985).toVar(),r=s.add(4.1616724).mul(s).add(3.417594).toVar(),i=n.div(r),a=t.greaterThan(0).select(i,ue(t.mul(t).oneMinus(),1e-7).inverseSqrt().mul(.5).sub(i));return o.cross(e).mul(a)}).setLayout({name:"LTC_EdgeVectorFormFactor",type:"vec3",inputs:[{name:"v1",type:"vec3"},{name:"v2",type:"vec3"}]}),gc=b(({N:o,V:e,P:t,mInv:s,p0:n,p1:r,p2:i,p3:a})=>{const u=r.sub(n).toVar(),c=a.sub(n).toVar(),l=u.cross(c),d=T().toVar();return z(l.dot(t.sub(n)).greaterThanEqual(0),()=>{const h=e.sub(o.mul(e.dot(o))).normalize(),p=o.cross(h).negate(),f=s.mul(Oe(h,p,o).transpose()).toVar(),m=f.mul(n.sub(t)).normalize().toVar(),x=f.mul(r.sub(t)).normalize().toVar(),N=f.mul(i.sub(t)).normalize().toVar(),v=f.mul(a.sub(t)).normalize().toVar(),w=T(0).toVar();w.addAssign(gr({v1:m,v2:x})),w.addAssign(gr({v1:x,v2:N})),w.addAssign(gr({v1:N,v2:v})),w.addAssign(gr({v1:v,v2:m})),d.assign(T(CT({f:w})))}),d}).setLayout({name:"LTC_Evaluate",type:"vec3",inputs:[{name:"N",type:"vec3"},{name:"V",type:"vec3"},{name:"P",type:"vec3"},{name:"mInv",type:"mat3"},{name:"p0",type:"vec3"},{name:"p1",type:"vec3"},{name:"p2",type:"vec3"},{name:"p3",type:"vec3"}]}),wi=1/6,Qp=o=>$(wi,$(o,$(o,o.negate().add(3)).sub(3)).add(1)),Qo=o=>$(wi,$(o,$(o,$(3,o).sub(6))).add(4)),Zp=o=>$(wi,$(o,$(o,$(-3,o).add(3)).add(3)).add(1)),Zo=o=>$(wi,ht(o,3)),mc=o=>Qp(o).add(Qo(o)),yc=o=>Zp(o).add(Zo(o)),xc=o=>_e(-1,Qo(o).div(Qp(o).add(Qo(o)))),Tc=o=>_e(1,Zo(o).div(Zp(o).add(Zo(o)))),_c=(o,e,t)=>{const s=o.uvNode,n=$(s,e.zw).add(.5),r=At(n),i=Xt(n),a=mc(i.x),u=yc(i.x),c=xc(i.x),l=Tc(i.x),d=xc(i.y),h=Tc(i.y),p=M(r.x.add(c),r.y.add(d)).sub(.5).mul(e.xy),f=M(r.x.add(l),r.y.add(d)).sub(.5).mul(e.xy),m=M(r.x.add(c),r.y.add(h)).sub(.5).mul(e.xy),x=M(r.x.add(l),r.y.add(h)).sub(.5).mul(e.xy),N=mc(i.y).mul(_e(a.mul(o.sample(p).level(t)),u.mul(o.sample(f).level(t)))),v=yc(i.y).mul(_e(a.mul(o.sample(m).level(t)),u.mul(o.sample(x).level(t))));return N.add(v)},Jp=b(([o,e=g(3)])=>{const t=M(o.size(y(e))),s=M(o.size(y(e.add(1)))),n=gt(1,t),r=gt(1,s),i=_c(o,P(n,t),At(e)),a=_c(o,P(r,s),xi(e));return Xt(e).mix(i,a)}),bc=b(([o,e,t,s,n])=>{const r=T(Qa(e.negate(),qt(o),gt(1,s))),i=T(zt(n[0].xyz),zt(n[1].xyz),zt(n[2].xyz));return qt(r).mul(t.mul(i))}).setLayout({name:"getVolumeTransmissionRay",type:"vec3",inputs:[{name:"n",type:"vec3"},{name:"v",type:"vec3"},{name:"thickness",type:"float"},{name:"ior",type:"float"},{name:"modelMatrix",type:"mat4"}]}),ET=b(([o,e])=>o.mul(Et(e.mul(2).sub(2),0,1))).setLayout({name:"applyIorToRoughness",type:"float",inputs:[{name:"roughness",type:"float"},{name:"ior",type:"float"}]}),wT=fu(),MT=fu(),Nc=b(([o,e,t],{material:s})=>{const r=(s.side===Ct?wT:MT).sample(o),i=vt(Kn.x).mul(ET(e,t));return Jp(r,i)}),Sc=b(([o,e,t])=>(z(t.notEqual(0),()=>{const s=yi(e).negate().div(t);return za(s.negate().mul(o))}),T(1))).setLayout({name:"volumeAttenuation",type:"vec3",inputs:[{name:"transmissionDistance",type:"float"},{name:"attenuationColor",type:"vec3"},{name:"attenuationDistance",type:"float"}]}),BT=b(([o,e,t,s,n,r,i,a,u,c,l,d,h,p,f])=>{let m,x;if(f){m=P().toVar(),x=T().toVar();const F=l.sub(1).mul(f.mul(.025)),L=T(l.sub(F),l,l.add(F));te({start:0,end:3},({i:I})=>{const G=L.element(I),X=bc(o,e,d,G,a),ie=i.add(X),Z=c.mul(u.mul(P(ie,1))),Qe=M(Z.xy.div(Z.w)).toVar();Qe.addAssign(1),Qe.divAssign(2),Qe.assign(M(Qe.x,Qe.y.oneMinus()));const Ze=Nc(Qe,t,G);m.element(I).assign(Ze.element(I)),m.a.addAssign(Ze.a),x.element(I).assign(s.element(I).mul(Sc(zt(X),h,p).element(I)))}),m.a.divAssign(3)}else{const F=bc(o,e,d,l,a),L=i.add(F),I=c.mul(u.mul(P(L,1))),G=M(I.xy.div(I.w)).toVar();G.addAssign(1),G.divAssign(2),G.assign(M(G.x,G.y.oneMinus())),m=Nc(G,t,l),x=s.mul(Sc(zt(F),h,p))}const N=x.rgb.mul(m.rgb),v=o.dot(e).clamp(),w=T(Yp({dotNV:v,specularColor:n,specularF90:r,roughness:t})),B=x.r.add(x.g,x.b).div(3);return P(w.oneMinus().mul(N),m.a.oneMinus().mul(B).oneMinus())}),FT=Oe(3.2404542,-.969266,.0556434,-1.5371385,1.8760108,-.2040259,-.4985314,.041556,1.0572252),UT=o=>{const e=o.sqrt();return T(1).add(e).div(T(1).sub(e))},vc=(o,e)=>o.sub(e).div(o.add(e)).pow2(),PT=(o,e)=>{const t=o.mul(2*Math.PI*1e-9),s=T(54856e-17,44201e-17,52481e-17),n=T(1681e3,1795300,2208400),r=T(43278e5,93046e5,66121e5),i=g(9747e-17*Math.sqrt(2*Math.PI*45282e5)).mul(t.mul(2239900).add(e.x).cos()).mul(t.pow2().mul(-45282e5).exp());let a=s.mul(r.mul(2*Math.PI).sqrt()).mul(n.mul(t).add(e).cos()).mul(t.pow2().negate().mul(r).exp());return a=T(a.x.add(i),a.y,a.z).div(10685e-11),FT.mul(a)},DT=b(({outsideIOR:o,eta2:e,cosTheta1:t,thinFilmThickness:s,baseF0:n})=>{const r=Q(o,e,ct(0,.03,s)),a=o.div(r).pow2().mul(t.pow2().oneMinus()).oneMinus();z(a.lessThan(0),()=>T(1));const u=a.sqrt(),c=vc(r,o),l=un({f0:c,f90:1,dotVH:t}),d=l.oneMinus(),h=r.lessThan(o).select(Math.PI,0),p=g(Math.PI).sub(h),f=UT(n.clamp(0,.9999)),m=vc(f,r.toVec3()),x=un({f0:m,f90:1,dotVH:u}),N=T(f.x.lessThan(r).select(Math.PI,0),f.y.lessThan(r).select(Math.PI,0),f.z.lessThan(r).select(Math.PI,0)),v=r.mul(s,u,2),w=T(p).add(N),B=l.mul(x).clamp(1e-5,.9999),F=B.sqrt(),L=d.pow2().mul(x).div(T(1).sub(B)),G=l.add(L).toVar(),X=L.sub(d).toVar();return te({start:1,end:2,condition:"<=",name:"m"},({m:ie})=>{X.mulAssign(F);const Z=PT(g(ie).mul(v),g(ie).mul(w)).mul(2);G.addAssign(X.mul(Z))}),G.max(T(0))}).setLayout({name:"evalIridescence",type:"vec3",inputs:[{name:"outsideIOR",type:"float"},{name:"eta2",type:"float"},{name:"cosTheta1",type:"float"},{name:"thinFilmThickness",type:"float"},{name:"baseF0",type:"vec3"}]}),LT=b(({normal:o,viewDir:e,roughness:t})=>{const s=o.dot(e).saturate(),n=t.pow2(),r=Ue(t.lessThan(.25),g(-339.2).mul(n).add(g(161.4).mul(t)).sub(25.9),g(-8.48).mul(n).add(g(14.3).mul(t)).sub(9.95)),i=Ue(t.lessThan(.25),g(44).mul(n).sub(g(23.7).mul(t)).add(3.26),g(1.97).mul(n).sub(g(3.27).mul(t)).add(.72));return Ue(t.lessThan(.25),0,g(.1).mul(t).sub(.025)).add(r.mul(s).add(i).exp()).mul(1/Math.PI).saturate()}),zi=T(.04),Wi=g(1);class Su extends Ei{constructor(e=!1,t=!1,s=!1,n=!1,r=!1,i=!1){super(),this.clearcoat=e,this.sheen=t,this.iridescence=s,this.anisotropy=n,this.transmission=r,this.dispersion=i,this.clearcoatRadiance=null,this.clearcoatSpecularDirect=null,this.clearcoatSpecularIndirect=null,this.sheenSpecularDirect=null,this.sheenSpecularIndirect=null,this.iridescenceFresnel=null,this.iridescenceF0=null}start(e){if(this.clearcoat===!0&&(this.clearcoatRadiance=T().toVar("clearcoatRadiance"),this.clearcoatSpecularDirect=T().toVar("clearcoatSpecularDirect"),this.clearcoatSpecularIndirect=T().toVar("clearcoatSpecularIndirect")),this.sheen===!0&&(this.sheenSpecularDirect=T().toVar("sheenSpecularDirect"),this.sheenSpecularIndirect=T().toVar("sheenSpecularIndirect")),this.iridescence===!0){const t=fe.dot(ae).clamp();this.iridescenceFresnel=DT({outsideIOR:g(1),eta2:Ua,cosTheta1:t,thinFilmThickness:Pa,baseF0:We}),this.iridescenceF0=jp({f:this.iridescenceFresnel,f90:1,dotVH:t})}if(this.transmission===!0){const t=Wt,s=su.sub(Wt).normalize(),n=vi;e.backdrop=BT(n,s,Nt,ee,We,Wn,t,at,je,He,wn,Da,Ia,La,this.dispersion?Va:null),e.backdropAlpha=jr,ee.a.mulAssign(Q(1,e.backdrop.a,jr))}}computeMultiscattering(e,t,s){const n=fe.dot(ae).clamp(),r=Nu({roughness:Nt,dotNV:n}),a=(this.iridescenceF0?mi.mix(We,this.iridescenceF0):We).mul(r.x).add(s.mul(r.y)),c=r.x.add(r.y).oneMinus(),l=We.add(We.oneMinus().mul(.047619)),d=a.mul(l).div(c.mul(l).oneMinus());e.addAssign(a),t.addAssign(d.mul(c))}direct({lightDirection:e,lightColor:t,reflectedLight:s}){const r=fe.dot(e).clamp().mul(t);if(this.sheen===!0&&this.sheenSpecularDirect.addAssign(r.mul(AT({lightDirection:e}))),this.clearcoat===!0){const a=Hs.dot(e).clamp().mul(t);this.clearcoatSpecularDirect.addAssign(a.mul(jo({lightDirection:e,f0:zi,f90:Wi,roughness:zn,normalView:Hs})))}s.directDiffuse.addAssign(r.mul(Ns({diffuseColor:ee.rgb}))),s.directSpecular.addAssign(r.mul(jo({lightDirection:e,f0:We,f90:1,roughness:Nt,iridescence:this.iridescence,f:this.iridescenceFresnel,USE_IRIDESCENCE:this.iridescence,USE_ANISOTROPY:this.anisotropy})))}directRectArea({lightColor:e,lightPosition:t,halfWidth:s,halfHeight:n,reflectedLight:r,ltc_1:i,ltc_2:a}){const u=t.add(s).sub(n),c=t.sub(s).sub(n),l=t.sub(s).add(n),d=t.add(s).add(n),h=fe,p=ae,f=xe.toVar(),m=RT({N:h,V:p,roughness:Nt}),x=i.sample(m).toVar(),N=a.sample(m).toVar(),v=Oe(T(x.x,0,x.y),T(0,1,0),T(x.z,0,x.w)).toVar(),w=We.mul(N.x).add(We.oneMinus().mul(N.y)).toVar();r.directSpecular.addAssign(e.mul(w).mul(gc({N:h,V:p,P:f,mInv:v,p0:u,p1:c,p2:l,p3:d}))),r.directDiffuse.addAssign(e.mul(ee).mul(gc({N:h,V:p,P:f,mInv:Oe(1,0,0,0,1,0,0,0,1),p0:u,p1:c,p2:l,p3:d})))}indirect(e,t,s){this.indirectDiffuse(e,t,s),this.indirectSpecular(e,t,s),this.ambientOcclusion(e,t,s)}indirectDiffuse({irradiance:e,reflectedLight:t}){t.indirectDiffuse.addAssign(e.mul(Ns({diffuseColor:ee})))}indirectSpecular({radiance:e,iblIrradiance:t,reflectedLight:s}){if(this.sheen===!0&&this.sheenSpecularIndirect.addAssign(t.mul(fs,LT({normal:fe,viewDir:ae,roughness:gi}))),this.clearcoat===!0){const c=Hs.dot(ae).clamp(),l=Yp({dotNV:c,specularColor:zi,specularF90:Wi,roughness:zn});this.clearcoatSpecularIndirect.addAssign(this.clearcoatRadiance.mul(l))}const n=T().toVar("singleScattering"),r=T().toVar("multiScattering"),i=t.mul(1/Math.PI);this.computeMultiscattering(n,r,Wn);const a=n.add(r),u=ee.mul(a.r.max(a.g).max(a.b).oneMinus());s.indirectSpecular.addAssign(e.mul(n)),s.indirectSpecular.addAssign(r.mul(i)),s.indirectDiffuse.addAssign(u.mul(i))}ambientOcclusion({ambientOcclusion:e,reflectedLight:t}){const n=fe.dot(ae).clamp().add(e),r=Nt.mul(-16).oneMinus().negate().exp2(),i=e.sub(n.pow(r).oneMinus()).clamp();this.clearcoat===!0&&this.clearcoatSpecularIndirect.mulAssign(e),this.sheen===!0&&this.sheenSpecularIndirect.mulAssign(e),t.indirectDiffuse.mulAssign(e),t.indirectSpecular.mulAssign(i)}finish(e){const{outgoingLight:t}=e;if(this.clearcoat===!0){const s=Hs.dot(ae).clamp(),n=un({dotVH:s,f0:zi,f90:Wi}),r=t.mul(Kr.mul(n).oneMinus()).add(this.clearcoatSpecularDirect.add(this.clearcoatSpecularIndirect).mul(Kr));t.assign(r)}if(this.sheen===!0){const s=fs.r.max(fs.g).max(fs.b).mul(.157).oneMinus(),n=t.mul(s).add(this.sheenSpecularDirect,this.sheenSpecularIndirect);t.assign(n)}}}const Ac=g(1),Jo=g(-2),mr=g(.8),$i=g(-1),yr=g(.4),Hi=g(2),xr=g(.305),qi=g(3),Rc=g(.21),IT=g(4),Cc=g(4),VT=g(16),GT=b(([o])=>{const e=T(oe(o)).toVar(),t=g(-1).toVar();return z(e.x.greaterThan(e.z),()=>{z(e.x.greaterThan(e.y),()=>{t.assign(Ue(o.x.greaterThan(0),0,3))}).Else(()=>{t.assign(Ue(o.y.greaterThan(0),1,4))})}).Else(()=>{z(e.z.greaterThan(e.y),()=>{t.assign(Ue(o.z.greaterThan(0),2,5))}).Else(()=>{t.assign(Ue(o.y.greaterThan(0),1,4))})}),t}).setLayout({name:"getFace",type:"float",inputs:[{name:"direction",type:"vec3"}]}),OT=b(([o,e])=>{const t=M().toVar();return z(e.equal(0),()=>{t.assign(M(o.z,o.y).div(oe(o.x)))}).ElseIf(e.equal(1),()=>{t.assign(M(o.x.negate(),o.z.negate()).div(oe(o.y)))}).ElseIf(e.equal(2),()=>{t.assign(M(o.x.negate(),o.y).div(oe(o.z)))}).ElseIf(e.equal(3),()=>{t.assign(M(o.z.negate(),o.y).div(oe(o.x)))}).ElseIf(e.equal(4),()=>{t.assign(M(o.x.negate(),o.z).div(oe(o.y)))}).Else(()=>{t.assign(M(o.x,o.y).div(oe(o.z)))}),$(.5,t.add(1))}).setLayout({name:"getUV",type:"vec2",inputs:[{name:"direction",type:"vec3"},{name:"face",type:"float"}]}),kT=b(([o])=>{const e=g(0).toVar();return z(o.greaterThanEqual(mr),()=>{e.assign(Ac.sub(o).mul($i.sub(Jo)).div(Ac.sub(mr)).add(Jo))}).ElseIf(o.greaterThanEqual(yr),()=>{e.assign(mr.sub(o).mul(Hi.sub($i)).div(mr.sub(yr)).add($i))}).ElseIf(o.greaterThanEqual(xr),()=>{e.assign(yr.sub(o).mul(qi.sub(Hi)).div(yr.sub(xr)).add(Hi))}).ElseIf(o.greaterThanEqual(Rc),()=>{e.assign(xr.sub(o).mul(IT.sub(qi)).div(xr.sub(Rc)).add(qi))}).Else(()=>{e.assign(g(-2).mul(vt($(1.16,o))))}),e}).setLayout({name:"roughnessToMip",type:"float",inputs:[{name:"roughness",type:"float"}]}),ef=b(([o,e])=>{const t=o.toVar();t.assign($(2,t).sub(1));const s=T(t,1).toVar();return z(e.equal(0),()=>{s.assign(s.zyx)}).ElseIf(e.equal(1),()=>{s.assign(s.xzy),s.xz.mulAssign(-1)}).ElseIf(e.equal(2),()=>{s.x.mulAssign(-1)}).ElseIf(e.equal(3),()=>{s.assign(s.zyx),s.xz.mulAssign(-1)}).ElseIf(e.equal(4),()=>{s.assign(s.xzy),s.xy.mulAssign(-1)}).ElseIf(e.equal(5),()=>{s.z.mulAssign(-1)}),s}).setLayout({name:"getDirection",type:"vec3",inputs:[{name:"uv",type:"vec2"},{name:"face",type:"float"}]}),tf=b(([o,e,t,s,n,r])=>{const i=g(t),a=T(e),u=Et(kT(i),Jo,r),c=Xt(u),l=At(u),d=T(ea(o,a,l,s,n,r)).toVar();return z(c.notEqual(0),()=>{const h=T(ea(o,a,l.add(1),s,n,r)).toVar();d.assign(Q(d,h,c))}),d}),ea=b(([o,e,t,s,n,r])=>{const i=g(t).toVar(),a=T(e),u=g(GT(a)).toVar(),c=g(ue(Cc.sub(i),0)).toVar();i.assign(ue(i,Cc));const l=g(rn(i)).toVar(),d=M(OT(a,u).mul(l.sub(2)).add(1)).toVar();return z(u.greaterThan(2),()=>{d.y.addAssign(l),u.subAssign(3)}),d.x.addAssign(u.mul(l)),d.x.addAssign(c.mul($(3,VT))),d.y.addAssign($(4,rn(r).sub(l))),d.x.mulAssign(s),d.y.mulAssign(n),o.sample(d).grad(M(),M())}),Ki=b(({envMap:o,mipInt:e,outputDirection:t,theta:s,axis:n,CUBEUV_TEXEL_WIDTH:r,CUBEUV_TEXEL_HEIGHT:i,CUBEUV_MAX_MIP:a})=>{const u=It(s),c=t.mul(u).add(n.cross(t).mul(st(s))).add(n.mul(n.dot(t).mul(u.oneMinus())));return ea(o,c,e,r,i,a)}),sf=b(({n:o,latitudinal:e,poleAxis:t,outputDirection:s,weights:n,samples:r,dTheta:i,mipInt:a,envMap:u,CUBEUV_TEXEL_WIDTH:c,CUBEUV_TEXEL_HEIGHT:l,CUBEUV_MAX_MIP:d})=>{const h=T(Ue(e,t,_i(t,s))).toVar();z(ka(h.equals(T(0))),()=>{h.assign(T(s.z,0,s.x.negate()))}),h.assign(qt(h));const p=T().toVar();return p.addAssign(n.element(y(0)).mul(Ki({theta:0,axis:h,outputDirection:s,mipInt:a,envMap:u,CUBEUV_TEXEL_WIDTH:c,CUBEUV_TEXEL_HEIGHT:l,CUBEUV_MAX_MIP:d}))),te({start:y(1),end:o},({i:f})=>{z(f.greaterThanEqual(r),()=>{pu()});const m=g(i.mul(g(f))).toVar();p.addAssign(n.element(f).mul(Ki({theta:m.mul(-1),axis:h,outputDirection:s,mipInt:a,envMap:u,CUBEUV_TEXEL_WIDTH:c,CUBEUV_TEXEL_HEIGHT:l,CUBEUV_MAX_MIP:d}))),p.addAssign(n.element(f).mul(Ki({theta:m,axis:h,outputDirection:s,mipInt:a,envMap:u,CUBEUV_TEXEL_WIDTH:c,CUBEUV_TEXEL_HEIGHT:l,CUBEUV_MAX_MIP:d})))}),P(p,1)});let ti=null;const Ec=new WeakMap;function zT(o){const e=Math.log2(o)-2,t=1/o;return{texelWidth:1/(3*Math.max(Math.pow(2,e),112)),texelHeight:t,maxMip:e}}function WT(o){let e=Ec.get(o);if((e!==void 0?e.pmremVersion:-1)!==o.pmremVersion){const s=o.image;if(o.isCubeTexture)if(HT(s))e=ti.fromCubemap(o,e);else return null;else if(qT(s))e=ti.fromEquirectangular(o,e);else return null;e.pmremVersion=o.pmremVersion,Ec.set(o,e)}return e.texture}class $T extends be{static get type(){return"PMREMNode"}constructor(e,t=null,s=null){super("vec3"),this._value=e,this._pmrem=null,this.uvNode=t,this.levelNode=s,this._generator=null;const n=new aa;n.isRenderTargetTexture=!0,this._texture=K(n),this._width=V(0),this._height=V(0),this._maxMip=V(0),this.updateBeforeType=W.RENDER}set value(e){this._value=e,this._pmrem=null}get value(){return this._value}updateFromTexture(e){const t=zT(e.image.height);this._texture.value=e,this._width.value=t.texelWidth,this._height.value=t.texelHeight,this._maxMip.value=t.maxMip}updateBefore(){let e=this._pmrem;const t=e?e.pmremVersion:-1,s=this._value;t!==s.pmremVersion&&(s.isPMREMTexture===!0?e=s:e=WT(s),e!==null&&(this._pmrem=e,this.updateFromTexture(e)))}setup(e){ti===null&&(ti=e.createPMREMGenerator()),this.updateBefore(e);let t=this.uvNode;t===null&&e.context.getUV&&(t=e.context.getUV(this));const s=this.value;e.renderer.coordinateSystem===Pn&&s.isPMREMTexture!==!0&&s.isRenderTargetTexture===!0&&(t=T(t.x.negate(),t.yz)),t=T(t.x,t.y.negate(),t.z);let n=this.levelNode;return n===null&&e.context.getTextureLevel&&(n=e.context.getTextureLevel(this)),tf(this._texture,t,n,this._width,this._height,this._maxMip)}}function HT(o){if(o==null)return!1;let e=0;const t=6;for(let s=0;s0}const vu=C($T),wc=new WeakMap;class KT extends hn{static get type(){return"EnvironmentNode"}constructor(e=null){super(),this.envNode=e}setup(e){const{material:t}=e;let s=this.envNode;if(s.isTextureNode||s.isMaterialReferenceNode){const p=s.isTextureNode?s.value:t[s.property];let f=wc.get(p);f===void 0&&(f=vu(p),wc.set(p,f)),s=f}const r=t.envMap?ne("envMapIntensity","float",e.material):ne("environmentIntensity","float",e.scene),a=t.useAnisotropy===!0||t.anisotropy>0?Zh:fe,u=s.context(Mc(Nt,a)).mul(r),c=s.context(XT(vi)).mul(Math.PI).mul(r),l=Mn(u),d=Mn(c);e.context.radiance.addAssign(l),e.context.iblIrradiance.addAssign(d);const h=e.context.lightingModel.clearcoatRadiance;if(h){const p=s.context(Mc(zn,Hs)).mul(r),f=Mn(p);h.addAssign(f)}}}const Mc=(o,e)=>{let t=null;return{getUV:()=>(t===null&&(t=ae.negate().reflect(e),t=o.mul(o).mix(t,e).normalize(),t=t.transformDirection(je)),t),getTextureLevel:()=>o}},XT=o=>({getUV:()=>o,getTextureLevel:()=>g(1)}),YT=new Pg;class nf extends ce{static get type(){return"MeshStandardNodeMaterial"}constructor(e){super(),this.isMeshStandardNodeMaterial=!0,this.lights=!0,this.emissiveNode=null,this.metalnessNode=null,this.roughnessNode=null,this.setDefaultValues(YT),this.setValues(e)}setupEnvironment(e){let t=super.setupEnvironment(e);return t===null&&e.environmentNode&&(t=e.environmentNode),t?new KT(t):null}setupLightingModel(){return new Su}setupSpecular(){const e=Q(T(.04),ee.rgb,kn);We.assign(e),Wn.assign(1)}setupVariants(){const e=this.metalnessNode?g(this.metalnessNode):op;kn.assign(e);let t=this.roughnessNode?g(this.roughnessNode):ip;t=bu({roughness:t}),Nt.assign(t),this.setupSpecular(),ee.assign(P(ee.rgb.mul(e.oneMinus()),ee.a))}copy(e){return this.emissiveNode=e.emissiveNode,this.metalnessNode=e.metalnessNode,this.roughnessNode=e.roughnessNode,super.copy(e)}}const jT=new Ug;class rf extends nf{static get type(){return"MeshPhysicalNodeMaterial"}constructor(e){super(),this.isMeshPhysicalNodeMaterial=!0,this.clearcoatNode=null,this.clearcoatRoughnessNode=null,this.clearcoatNormalNode=null,this.sheenNode=null,this.sheenRoughnessNode=null,this.iridescenceNode=null,this.iridescenceIORNode=null,this.iridescenceThicknessNode=null,this.specularIntensityNode=null,this.specularColorNode=null,this.iorNode=null,this.transmissionNode=null,this.thicknessNode=null,this.attenuationDistanceNode=null,this.attenuationColorNode=null,this.dispersionNode=null,this.anisotropyNode=null,this.setDefaultValues(jT),this.setValues(e)}get useClearcoat(){return this.clearcoat>0||this.clearcoatNode!==null}get useIridescence(){return this.iridescence>0||this.iridescenceNode!==null}get useSheen(){return this.sheen>0||this.sheenNode!==null}get useAnisotropy(){return this.anisotropy>0||this.anisotropyNode!==null}get useTransmission(){return this.transmission>0||this.transmissionNode!==null}get useDispersion(){return this.dispersion>0||this.dispersionNode!==null}setupSpecular(){const e=this.iorNode?g(this.iorNode):_p;wn.assign(e),We.assign(Q(Re(Ya(wn.sub(1).div(wn.add(1))).mul(rp),T(1)).mul(Yo),ee.rgb,kn)),Wn.assign(Q(Yo,1,kn))}setupLightingModel(){return new Su(this.useClearcoat,this.useSheen,this.useIridescence,this.useAnisotropy,this.useTransmission,this.useDispersion)}setupVariants(e){if(super.setupVariants(e),this.useClearcoat){const t=this.clearcoatNode?g(this.clearcoatNode):up,s=this.clearcoatRoughnessNode?g(this.clearcoatRoughnessNode):cp;Kr.assign(t),zn.assign(bu({roughness:s}))}if(this.useSheen){const t=this.sheenNode?T(this.sheenNode):hp,s=this.sheenRoughnessNode?g(this.sheenRoughnessNode):pp;fs.assign(t),gi.assign(s)}if(this.useIridescence){const t=this.iridescenceNode?g(this.iridescenceNode):gp,s=this.iridescenceIORNode?g(this.iridescenceIORNode):mp,n=this.iridescenceThicknessNode?g(this.iridescenceThicknessNode):yp;mi.assign(t),Ua.assign(s),Pa.assign(n)}if(this.useAnisotropy){const t=(this.anisotropyNode?M(this.anisotropyNode):fp).toVar();Zt.assign(t.length()),z(Zt.equal(0),()=>{t.assign(M(1,0))}).Else(()=>{t.divAssign(M(Zt)),Zt.assign(Zt.saturate())}),Xr.assign(Zt.pow2().mix(Nt.pow2(),1)),En.assign(gs[0].mul(t.x).add(gs[1].mul(t.y))),_s.assign(gs[1].mul(t.x).sub(gs[0].mul(t.y)))}if(this.useTransmission){const t=this.transmissionNode?g(this.transmissionNode):xp,s=this.thicknessNode?g(this.thicknessNode):Tp,n=this.attenuationDistanceNode?g(this.attenuationDistanceNode):bp,r=this.attenuationColorNode?T(this.attenuationColorNode):Np;if(jr.assign(t),Da.assign(s),La.assign(n),Ia.assign(r),this.useDispersion){const i=this.dispersionNode?g(this.dispersionNode):vp;Va.assign(i)}}}setupClearcoatNormal(){return this.clearcoatNormalNode?T(this.clearcoatNormalNode):lp}setup(e){e.context.setupClearcoatNormal=()=>this.setupClearcoatNormal(e),super.setup(e)}copy(e){return this.clearcoatNode=e.clearcoatNode,this.clearcoatRoughnessNode=e.clearcoatRoughnessNode,this.clearcoatNormalNode=e.clearcoatNormalNode,this.sheenNode=e.sheenNode,this.sheenRoughnessNode=e.sheenRoughnessNode,this.iridescenceNode=e.iridescenceNode,this.iridescenceIORNode=e.iridescenceIORNode,this.iridescenceThicknessNode=e.iridescenceThicknessNode,this.specularIntensityNode=e.specularIntensityNode,this.specularColorNode=e.specularColorNode,this.transmissionNode=e.transmissionNode,this.thicknessNode=e.thicknessNode,this.attenuationDistanceNode=e.attenuationDistanceNode,this.attenuationColorNode=e.attenuationColorNode,this.dispersionNode=e.dispersionNode,this.anisotropyNode=e.anisotropyNode,super.copy(e)}}class QT extends Su{constructor(e=!1,t=!1,s=!1,n=!1,r=!1,i=!1,a=!1){super(e,t,s,n,r,i),this.useSSS=a}direct({lightDirection:e,lightColor:t,reflectedLight:s},n,r){if(this.useSSS===!0){const i=r.material,{thicknessColorNode:a,thicknessDistortionNode:u,thicknessAmbientNode:c,thicknessAttenuationNode:l,thicknessPowerNode:d,thicknessScaleNode:h}=i,p=e.add(fe.mul(u)).normalize(),f=g(ae.dot(p.negate()).saturate().pow(d).mul(h)),m=T(f.add(c).mul(a));s.directDiffuse.addAssign(m.mul(l.mul(t)))}super.direct({lightDirection:e,lightColor:t,reflectedLight:s},n,r)}}class pR extends rf{static get type(){return"MeshSSSNodeMaterial"}constructor(e){super(e),this.thicknessColorNode=null,this.thicknessDistortionNode=g(.1),this.thicknessAmbientNode=g(0),this.thicknessAttenuationNode=g(.1),this.thicknessPowerNode=g(2),this.thicknessScaleNode=g(10)}get useSSS(){return this.thicknessColorNode!==null}setupLightingModel(){return new QT(this.useClearcoat,this.useSheen,this.useIridescence,this.useAnisotropy,this.useTransmission,this.useDispersion,this.useSSS)}copy(e){return this.thicknessColorNode=e.thicknessColorNode,this.thicknessDistortionNode=e.thicknessDistortionNode,this.thicknessAmbientNode=e.thicknessAmbientNode,this.thicknessAttenuationNode=e.thicknessAttenuationNode,this.thicknessPowerNode=e.thicknessPowerNode,this.thicknessScaleNode=e.thicknessScaleNode,super.copy(e)}}const ZT=b(({normal:o,lightDirection:e,builder:t})=>{const s=o.dot(e),n=M(s.mul(.5).add(.5),0);if(t.material.gradientMap){const r=dt("gradientMap","texture").context({getUV:()=>n});return T(r.r)}else{const r=n.fwidth().mul(.5);return Q(T(.7),T(1),ct(g(.7).sub(r.x),g(.7).add(r.x),n.x))}});class JT extends Ei{direct({lightDirection:e,lightColor:t,reflectedLight:s},n,r){const i=ZT({normal:Ni,lightDirection:e,builder:r}).mul(t);s.directDiffuse.addAssign(i.mul(Ns({diffuseColor:ee.rgb})))}indirect({ambientOcclusion:e,irradiance:t,reflectedLight:s}){s.indirectDiffuse.addAssign(t.mul(Ns({diffuseColor:ee}))),s.indirectDiffuse.mulAssign(e)}}const e_=new Dg;class t_ extends ce{static get type(){return"MeshToonNodeMaterial"}constructor(e){super(),this.isMeshToonNodeMaterial=!0,this.lights=!0,this.setDefaultValues(e_),this.setValues(e)}setupLightingModel(){return new JT}}class s_ extends be{static get type(){return"MatcapUVNode"}constructor(){super("vec2")}setup(){const e=T(ae.z,0,ae.x.negate()).normalize(),t=ae.cross(e);return M(e.dot(fe),t.dot(fe)).mul(.495).add(.5)}}const of=U(s_),n_=new Mg;class r_ extends ce{static get type(){return"MeshMatcapNodeMaterial"}constructor(e){super(),this.isMeshMatcapNodeMaterial=!0,this.setDefaultValues(n_),this.setValues(e)}setupVariants(e){const t=of;let s;e.material.matcap?s=dt("matcap","texture").context({getUV:()=>t}):s=T(Q(.2,.8,t.y)),ee.rgb.mulAssign(s.rgb)}}const i_=new hl;class o_ extends ce{static get type(){return"PointsNodeMaterial"}constructor(e){super(),this.isPointsNodeMaterial=!0,this.setDefaultValues(i_),this.setValues(e)}}class a_ extends be{static get type(){return"RotateNode"}constructor(e,t){super(),this.positionNode=e,this.rotationNode=t}getNodeType(e){return this.positionNode.getNodeType(e)}setup(e){const{rotationNode:t,positionNode:s}=this;if(this.getNodeType(e)==="vec2"){const r=t.cos(),i=t.sin();return fi(r,i,i.negate(),r).mul(s)}else{const r=t,i=Ts(P(1,0,0,0),P(0,It(r.x),st(r.x).negate(),0),P(0,st(r.x),It(r.x),0),P(0,0,0,1)),a=Ts(P(It(r.y),0,st(r.y),0),P(0,1,0,0),P(st(r.y).negate(),0,It(r.y),0),P(0,0,0,1)),u=Ts(P(It(r.z),st(r.z).negate(),0,0),P(st(r.z),It(r.z),0,0),P(0,0,1,0),P(0,0,0,1));return i.mul(a).mul(u).mul(P(s,1)).xyz}}}const Au=C(a_),u_=new Hg;class c_ extends ce{static get type(){return"SpriteNodeMaterial"}constructor(e){super(),this.isSpriteNodeMaterial=!0,this._useSizeAttenuation=!0,this.positionNode=null,this.rotationNode=null,this.scaleNode=null,this.setDefaultValues(u_),this.setValues(e)}setupPositionView(e){const{object:t,camera:s}=e,n=this.sizeAttenuation,{positionNode:r,rotationNode:i,scaleNode:a}=this,u=Kt.mul(T(r||0));let c=M(at[0].xyz.length(),at[1].xyz.length());if(a!==null&&(c=c.mul(a)),n===!1)if(s.isPerspectiveCamera)c=c.mul(u.z.negate());else{const p=g(2).div(He.element(1).element(1));c=c.mul(p.mul(2))}let l=Ce.xy;if(t.center&&t.center.isVector2===!0){const p=By("center","vec2",t);l=l.sub(p.sub(.5))}l=l.mul(c);const d=g(i||dp),h=Au(l,d);return P(u.xy.add(h),u.zw)}copy(e){return this.positionNode=e.positionNode,this.rotationNode=e.rotationNode,this.scaleNode=e.scaleNode,super.copy(e)}get sizeAttenuation(){return this._useSizeAttenuation}set sizeAttenuation(e){this._useSizeAttenuation!==e&&(this._useSizeAttenuation=e,this.needsUpdate=!0)}}class l_ extends Ei{constructor(){super(),this.shadowNode=g(1).toVar("shadowMask")}direct({shadowMask:e}){this.shadowNode.mulAssign(e)}finish(e){ee.a.mulAssign(this.shadowNode.oneMinus()),e.outgoingLight.rgb.assign(ee.rgb)}}const d_=new $g;class h_ extends ce{static get type(){return"ShadowNodeMaterial"}constructor(e){super(),this.isShadowNodeMaterial=!0,this.lights=!0,this.setDefaultValues(d_),this.setValues(e)}setupLightingModel(){return new l_}}const p_=b(({texture:o,uv:e})=>{const s=T().toVar();return z(e.x.lessThan(1e-4),()=>{s.assign(T(1,0,0))}).ElseIf(e.y.lessThan(1e-4),()=>{s.assign(T(0,1,0))}).ElseIf(e.z.lessThan(1e-4),()=>{s.assign(T(0,0,1))}).ElseIf(e.x.greaterThan(1-1e-4),()=>{s.assign(T(-1,0,0))}).ElseIf(e.y.greaterThan(1-1e-4),()=>{s.assign(T(0,-1,0))}).ElseIf(e.z.greaterThan(1-1e-4),()=>{s.assign(T(0,0,-1))}).Else(()=>{const r=o.sample(e.add(T(-.01,0,0))).r.sub(o.sample(e.add(T(.01,0,0))).r),i=o.sample(e.add(T(0,-.01,0))).r.sub(o.sample(e.add(T(0,.01,0))).r),a=o.sample(e.add(T(0,0,-.01))).r.sub(o.sample(e.add(T(0,0,.01))).r);s.assign(T(r,i,a))}),s.normalize()});class f_ extends wt{static get type(){return"Texture3DNode"}constructor(e,t=null,s=null){super(e,t,s),this.isTexture3DNode=!0}getInputType(){return"texture3D"}getDefaultUV(){return T(.5,.5,.5)}setUpdateMatrix(){}setupUV(e,t){const s=this.value;return e.isFlipY()&&(s.isRenderTargetTexture===!0||s.isFramebufferTexture===!0)&&(this.sampler?t=t.flipY():t=t.setY(y(ns(this,this.levelNode).y).sub(t.y).sub(1))),t}generateUV(e,t){return t.build(e,"vec3")}normal(e){return p_({texture:this,uv:e})}}const af=C(f_);class fR extends ce{static get type(){return"VolumeNodeMaterial"}constructor(e){super(),this.isVolumeNodeMaterial=!0,this.base=new mt(16777215),this.map=null,this.steps=100,this.testNode=null,this.setValues(e)}setup(e){const t=af(this.map,null,0),s=b(({orig:n,dir:r})=>{const i=T(-.5),a=T(.5),u=r.reciprocal(),c=i.sub(n).mul(u),l=a.sub(n).mul(u),d=Re(c,l),h=ue(c,l),p=ue(d.x,ue(d.y,d.z)),f=Re(h.x,Re(h.y,h.z));return M(p,f)});this.fragmentNode=b(()=>{const n=ke(T(Ih.mul(P(su,1)))),i=ke(Ce.sub(n)).normalize(),a=M(s({orig:n,dir:i})).toVar();a.x.greaterThan(a.y).discard(),a.assign(M(ue(a.x,0),a.y));const u=T(n.add(a.x.mul(i))).toVar(),c=T(i.abs().reciprocal()).toVar(),l=g(Re(c.x,Re(c.y,c.z))).toVar("delta");l.divAssign(dt("steps","float"));const d=P(dt("base","color"),0).toVar();return te({type:"float",start:a.x,end:a.y,update:"+= delta"},()=>{const h=Fa("float","d").assign(t.sample(u.add(.5)).r);this.testNode!==null?this.testNode({map:t,mapValue:h,probe:u,finalColor:d}).append():(d.a.assign(1),pu()),u.addAssign(i.mul(l))}),d.a.equal(0).discard(),P(d)})(),super.setup(e)}}class g_{constructor(e,t){this.nodes=e,this.info=t,this._context=self,this._animationLoop=null,this._requestId=null}start(){const e=(t,s)=>{this._requestId=this._context.requestAnimationFrame(e),this.info.autoReset===!0&&this.info.reset(),this.nodes.nodeFrame.update(),this.info.frame=this.nodes.nodeFrame.frameId,this._animationLoop!==null&&this._animationLoop(t,s)};e()}stop(){this._context.cancelAnimationFrame(this._requestId),this._requestId=null}setAnimationLoop(e){this._animationLoop=e}setContext(e){this._context=e}dispose(){this.stop()}}class Ft{constructor(){this.weakMap=new WeakMap}get(e){let t=this.weakMap;for(let s=0;s{this.dispose()},this.material.addEventListener("dispose",this.onMaterialDispose)}updateClipping(e){this.clippingContext=e}get clippingNeedsUpdate(){return this.clippingContext===null||this.clippingContext.cacheKey===this.clippingContextCacheKey?!1:(this.clippingContextCacheKey=this.clippingContext.cacheKey,!0)}get hardwareClippingPlanes(){return this.material.hardwareClipping===!0?this.clippingContext.unionClippingCount:0}getNodeBuilderState(){return this._nodeBuilderState||(this._nodeBuilderState=this._nodes.getForRender(this))}getMonitor(){return this._monitor||(this._monitor=this.getNodeBuilderState().monitor)}getBindings(){return this._bindings||(this._bindings=this.getNodeBuilderState().createBindings())}getIndex(){return this._geometries.getIndex(this)}getIndirect(){return this._geometries.getIndirect(this)}getChainArray(){return[this.object,this.material,this.context,this.lightsNode]}setGeometry(e){this.geometry=e,this.attributes=null}getAttributes(){if(this.attributes!==null)return this.attributes;const e=this.getNodeBuilderState().nodeAttributes,t=this.geometry,s=[],n=new Set;for(const r of e){const i=r.node&&r.node.attribute?r.node.attribute:t.getAttribute(r.name);if(i===void 0)continue;s.push(i);const a=i.isInterleavedBufferAttribute?i.data:i;n.add(a)}return this.attributes=s,this.vertexBuffers=Array.from(n.values()),s}getVertexBuffers(){return this.vertexBuffers===null&&this.getAttributes(),this.vertexBuffers}getDrawParameters(){const{object:e,material:t,geometry:s,group:n,drawRange:r}=this,i=this.drawParams||(this.drawParams={vertexCount:0,firstVertex:0,instanceCount:0,firstInstance:0}),a=this.getIndex(),u=a!==null,c=s.isInstancedBufferGeometry?s.instanceCount:e.count>1?e.count:1;if(c===0)return null;if(i.instanceCount=c,e.isBatchedMesh===!0)return i;let l=1;t.wireframe===!0&&!e.isPoints&&!e.isLineSegments&&!e.isLine&&!e.isLineLoop&&(l=2);let d=r.start*l,h=(r.start+r.count)*l;n!==null&&(d=Math.max(d,n.start*l),h=Math.min(h,(n.start+n.count)*l));const p=s.attributes.position;let f=1/0;u?f=a.count:p!=null&&(f=p.count),d=Math.max(d,0),h=Math.min(h,f);const m=h-d;return m<0||m===1/0?null:(i.vertexCount=m,i.firstVertex=d,i)}getGeometryCacheKey(){const{geometry:e}=this;let t="";for(const s of Object.keys(e.attributes).sort()){const n=e.attributes[s];t+=s+",",n.data&&(t+=n.data.stride+","),n.offset&&(t+=n.offset+","),n.itemSize&&(t+=n.itemSize+","),n.normalized&&(t+="n,")}return e.index&&(t+="index,"),t}getMaterialCacheKey(){const{object:e,material:t}=this;let s=t.customProgramCacheKey();for(const n of y_(t)){if(/^(is[A-Z]|_)|^(visible|version|uuid|name|opacity|userData)$/.test(n))continue;const r=t[n];let i;if(r!==null){const a=typeof r;a==="number"?i=r!==0?"1":"0":a==="object"?(i="{",r.isTexture&&(i+=r.mapping),i+="}"):i=String(r)}else i=String(r);s+=i+","}return s+=this.clippingContextCacheKey+",",e.geometry&&(s+=this.getGeometryCacheKey()),e.skeleton&&(s+=e.skeleton.bones.length+","),e.morphTargetInfluences&&(s+=e.morphTargetInfluences.length+","),e.isBatchedMesh&&(s+=e._matricesTexture.uuid+",",e._colorsTexture!==null&&(s+=e._colorsTexture.uuid+",")),e.count>1&&(s+=e.uuid+","),s+=e.receiveShadow+",",ga(s)}get needsGeometryUpdate(){return this.geometry.id!==this.object.geometry.id}get needsUpdate(){return this.initialNodesCacheKey!==this.getDynamicCacheKey()||this.clippingNeedsUpdate}getDynamicCacheKey(){let e=this._nodes.getCacheKey(this.scene,this.lightsNode);return this.object.receiveShadow&&(e+=1),e}getCacheKey(){return this.getMaterialCacheKey()+this.getDynamicCacheKey()}dispose(){this.material.removeEventListener("dispose",this.onMaterialDispose),this.onDispose()}}const Bs=[];class T_{constructor(e,t,s,n,r,i){this.renderer=e,this.nodes=t,this.geometries=s,this.pipelines=n,this.bindings=r,this.info=i,this.chainMaps={}}get(e,t,s,n,r,i,a,u){const c=this.getChainMap(u);Bs[0]=e,Bs[1]=t,Bs[2]=i,Bs[3]=r;let l=c.get(Bs);return l===void 0?(l=this.createRenderObject(this.nodes,this.geometries,this.renderer,e,t,s,n,r,i,a,u),c.set(Bs,l)):(l.updateClipping(a),l.needsGeometryUpdate&&l.setGeometry(e.geometry),(l.version!==t.version||l.needsUpdate)&&(l.initialCacheKey!==l.getCacheKey()?(l.dispose(),l=this.get(e,t,s,n,r,i,a,u)):l.version=t.version)),l}getChainMap(e="default"){return this.chainMaps[e]||(this.chainMaps[e]=new Ft)}dispose(){this.chainMaps={}}createRenderObject(e,t,s,n,r,i,a,u,c,l,d){const h=this.getChainMap(d),p=new x_(e,t,s,n,r,i,a,u,c,l);return p.onDispose=()=>{this.pipelines.delete(p),this.bindings.delete(p),this.nodes.delete(p),h.delete(p.getChainArray())},p}}class os{constructor(){this.data=new WeakMap}get(e){let t=this.data.get(e);return t===void 0&&(t={},this.data.set(e,t)),t}delete(e){let t=null;return this.data.has(e)&&(t=this.data.get(e),this.data.delete(e)),t}has(e){return this.data.has(e)}dispose(){this.data=new WeakMap}}const St={VERTEX:1,INDEX:2,STORAGE:3,INDIRECT:4},ts=16,__=211,b_=212;class N_ extends os{constructor(e){super(),this.backend=e}delete(e){const t=super.delete(e);return t!==void 0&&this.backend.destroyAttribute(e),t}update(e,t){const s=this.get(e);if(s.version===void 0)t===St.VERTEX?this.backend.createAttribute(e):t===St.INDEX?this.backend.createIndexAttribute(e):t===St.STORAGE?this.backend.createStorageAttribute(e):t===St.INDIRECT&&this.backend.createIndirectStorageAttribute(e),s.version=this._getBufferAttribute(e).version;else{const n=this._getBufferAttribute(e);(s.version=0;--e)if(o[e]>=65535)return!0;return!1}function uf(o){return o.index!==null?o.index.version:o.attributes.position.version}function Bc(o){const e=[],t=o.index,s=o.attributes.position;if(t!==null){const r=t.array;for(let i=0,a=r.length;i{this.info.memory.geometries--;const r=t.index,i=e.getAttributes();r!==null&&this.attributes.delete(r);for(const u of i)this.attributes.delete(u);const a=this.wireframes.get(t);a!==void 0&&this.attributes.delete(a),t.removeEventListener("dispose",n)};t.addEventListener("dispose",n)}updateAttributes(e){const t=e.getAttributes();for(const r of t)r.isStorageBufferAttribute||r.isStorageInstancedBufferAttribute?this.updateAttribute(r,St.STORAGE):this.updateAttribute(r,St.VERTEX);const s=this.getIndex(e);s!==null&&this.updateAttribute(s,St.INDEX);const n=e.geometry.indirect;n!==null&&this.updateAttribute(n,St.INDIRECT)}updateAttribute(e,t){const s=this.info.render.calls;e.isInterleavedBufferAttribute?this.attributeCall.get(e)===void 0?(this.attributes.update(e,t),this.attributeCall.set(e,s)):this.attributeCall.get(e.data)!==s&&(this.attributes.update(e,t),this.attributeCall.set(e.data,s),this.attributeCall.set(e,s)):this.attributeCall.get(e)!==s&&(this.attributes.update(e,t),this.attributeCall.set(e,s))}getIndirect(e){return e.geometry.indirect}getIndex(e){const{geometry:t,material:s}=e;let n=t.index;if(s.wireframe===!0){const r=this.wireframes;let i=r.get(t);i===void 0?(i=Bc(t),r.set(t,i)):i.version!==uf(t)&&(this.attributes.delete(i),i=Bc(t),r.set(t,i)),n=i}return n}}class A_{constructor(){this.autoReset=!0,this.frame=0,this.calls=0,this.render={calls:0,frameCalls:0,drawCalls:0,triangles:0,points:0,lines:0,timestamp:0,previousFrameCalls:0,timestampCalls:0},this.compute={calls:0,frameCalls:0,timestamp:0,previousFrameCalls:0,timestampCalls:0},this.memory={geometries:0,textures:0}}update(e,t,s){this.render.drawCalls++,e.isMesh||e.isSprite?this.render.triangles+=s*(t/3):e.isPoints?this.render.points+=s*t:e.isLineSegments?this.render.lines+=s*(t/2):e.isLine?this.render.lines+=s*(t-1):console.error("THREE.WebGPUInfo: Unknown object type.")}updateTimestamp(e,t){this[e].timestampCalls===0&&(this[e].timestamp=0),this[e].timestamp+=t,this[e].timestampCalls++,this[e].timestampCalls>=this[e].previousFrameCalls&&(this[e].timestampCalls=0)}reset(){const e=this.render.frameCalls;this.render.previousFrameCalls=e;const t=this.compute.frameCalls;this.compute.previousFrameCalls=t,this.render.drawCalls=0,this.render.frameCalls=0,this.compute.frameCalls=0,this.render.triangles=0,this.render.points=0,this.render.lines=0}dispose(){this.reset(),this.calls=0,this.render.calls=0,this.compute.calls=0,this.render.timestamp=0,this.compute.timestamp=0,this.memory.geometries=0,this.memory.textures=0}}class cf{constructor(e){this.cacheKey=e,this.usedTimes=0}}class R_ extends cf{constructor(e,t,s){super(e),this.vertexProgram=t,this.fragmentProgram=s}}class C_ extends cf{constructor(e,t){super(e),this.computeProgram=t,this.isComputePipeline=!0}}let E_=0;class Xi{constructor(e,t,s,n=null,r=null){this.id=E_++,this.code=e,this.stage=t,this.name=s,this.transforms=n,this.attributes=r,this.usedTimes=0}}class w_ extends os{constructor(e,t){super(),this.backend=e,this.nodes=t,this.bindings=null,this.caches=new Map,this.programs={vertex:new Map,fragment:new Map,compute:new Map}}getForCompute(e,t){const{backend:s}=this,n=this.get(e);if(this._needsComputeUpdate(e)){const r=n.pipeline;r&&(r.usedTimes--,r.computeProgram.usedTimes--);const i=this.nodes.getForCompute(e);let a=this.programs.compute.get(i.computeShader);a===void 0&&(r&&r.computeProgram.usedTimes===0&&this._releaseProgram(r.computeProgram),a=new Xi(i.computeShader,"compute",e.name,i.transforms,i.nodeAttributes),this.programs.compute.set(i.computeShader,a),s.createProgram(a));const u=this._getComputeCacheKey(e,a);let c=this.caches.get(u);c===void 0&&(r&&r.usedTimes===0&&this._releasePipeline(r),c=this._getComputePipeline(e,a,u,t)),c.usedTimes++,a.usedTimes++,n.version=e.version,n.pipeline=c}return n.pipeline}getForRender(e,t=null){const{backend:s}=this,n=this.get(e);if(this._needsRenderUpdate(e)){const r=n.pipeline;r&&(r.usedTimes--,r.vertexProgram.usedTimes--,r.fragmentProgram.usedTimes--);const i=e.getNodeBuilderState(),a=e.material?e.material.name:"";let u=this.programs.vertex.get(i.vertexShader);u===void 0&&(r&&r.vertexProgram.usedTimes===0&&this._releaseProgram(r.vertexProgram),u=new Xi(i.vertexShader,"vertex",a),this.programs.vertex.set(i.vertexShader,u),s.createProgram(u));let c=this.programs.fragment.get(i.fragmentShader);c===void 0&&(r&&r.fragmentProgram.usedTimes===0&&this._releaseProgram(r.fragmentProgram),c=new Xi(i.fragmentShader,"fragment",a),this.programs.fragment.set(i.fragmentShader,c),s.createProgram(c));const l=this._getRenderCacheKey(e,u,c);let d=this.caches.get(l);d===void 0?(r&&r.usedTimes===0&&this._releasePipeline(r),d=this._getRenderPipeline(e,u,c,l,t)):e.pipeline=d,d.usedTimes++,u.usedTimes++,c.usedTimes++,n.pipeline=d}return n.pipeline}delete(e){const t=this.get(e).pipeline;return t&&(t.usedTimes--,t.usedTimes===0&&this._releasePipeline(t),t.isComputePipeline?(t.computeProgram.usedTimes--,t.computeProgram.usedTimes===0&&this._releaseProgram(t.computeProgram)):(t.fragmentProgram.usedTimes--,t.vertexProgram.usedTimes--,t.vertexProgram.usedTimes===0&&this._releaseProgram(t.vertexProgram),t.fragmentProgram.usedTimes===0&&this._releaseProgram(t.fragmentProgram))),super.delete(e)}dispose(){super.dispose(),this.caches=new Map,this.programs={vertex:new Map,fragment:new Map,compute:new Map}}updateForRender(e){this.getForRender(e)}_getComputePipeline(e,t,s,n){s=s||this._getComputeCacheKey(e,t);let r=this.caches.get(s);return r===void 0&&(r=new C_(s,t),this.caches.set(s,r),this.backend.createComputePipeline(r,n)),r}_getRenderPipeline(e,t,s,n,r){n=n||this._getRenderCacheKey(e,t,s);let i=this.caches.get(n);return i===void 0&&(i=new R_(n,t,s),this.caches.set(n,i),e.pipeline=i,this.backend.createRenderPipeline(e,r)),i}_getComputeCacheKey(e,t){return e.id+","+t.id}_getRenderCacheKey(e,t,s){return t.id+","+s.id+","+this.backend.getRenderCacheKey(e)}_releasePipeline(e){this.caches.delete(e.cacheKey)}_releaseProgram(e){const t=e.code,s=e.stage;this.programs[s].delete(t)}_needsComputeUpdate(e){const t=this.get(e);return t.pipeline===void 0||t.version!==e.version}_needsRenderUpdate(e){return this.get(e).pipeline===void 0||this.backend.needsRenderUpdate(e)}}class M_ extends os{constructor(e,t,s,n,r,i){super(),this.backend=e,this.textures=s,this.pipelines=r,this.attributes=n,this.nodes=t,this.info=i,this.pipelines.bindings=this}getForRender(e){const t=e.getBindings();for(const s of t){const n=this.get(s);n.bindGroup===void 0&&(this._init(s),this.backend.createBindings(s,t,0),n.bindGroup=s)}return t}getForCompute(e){const t=this.nodes.getForCompute(e).bindings;for(const s of t){const n=this.get(s);n.bindGroup===void 0&&(this._init(s),this.backend.createBindings(s,t,0),n.bindGroup=s)}return t}updateForCompute(e){this._updateBindings(this.getForCompute(e))}updateForRender(e){this._updateBindings(this.getForRender(e))}_updateBindings(e){for(const t of e)this._update(t,e)}_init(e){for(const t of e.bindings)if(t.isSampledTexture)this.textures.updateTexture(t.texture);else if(t.isStorageBuffer){const s=t.attribute,n=s.isIndirectStorageBufferAttribute?St.INDIRECT:St.STORAGE;this.attributes.update(s,n)}}_update(e,t){const{backend:s}=this;let n=!1,r=!0,i=0,a=0;for(const u of e.bindings)if(!(u.isNodeUniformsGroup&&this.nodes.updateGroup(u)===!1)){if(u.isUniformBuffer)u.update()&&s.updateBinding(u);else if(u.isSampler)u.update();else if(u.isSampledTexture){const c=this.textures.get(u.texture);u.needsBindingsUpdate(c.generation)&&(n=!0);const l=u.update(),d=u.texture;l&&this.textures.updateTexture(d);const h=s.get(d);if(h.externalTexture!==void 0||c.isDefaultTexture?r=!1:(i=i*10+d.id,a+=d.version),s.isWebGPUBackend===!0&&h.texture===void 0&&h.externalTexture===void 0&&(console.error("Bindings._update: binding should be available:",u,l,d,u.textureNode.value,n),this.textures.updateTexture(d),n=!0),d.isStorageTexture===!0){const p=this.get(d);u.store===!0?p.needsMipmap=!0:this.textures.needsMipmaps(d)&&p.needsMipmap===!0&&(this.backend.generateMipmaps(d),p.needsMipmap=!1)}}}n===!0&&this.backend.updateBindings(e,t,r?i:0,a)}}function B_(o,e){return o.groupOrder!==e.groupOrder?o.groupOrder-e.groupOrder:o.renderOrder!==e.renderOrder?o.renderOrder-e.renderOrder:o.material.id!==e.material.id?o.material.id-e.material.id:o.z!==e.z?o.z-e.z:o.id-e.id}function Fc(o,e){return o.groupOrder!==e.groupOrder?o.groupOrder-e.groupOrder:o.renderOrder!==e.renderOrder?o.renderOrder-e.renderOrder:o.z!==e.z?e.z-o.z:o.id-e.id}function Uc(o){return(o.transmission>0||o.transmissionNode)&&o.side===js&&o.forceSinglePass===!1}class F_{constructor(e,t,s){this.renderItems=[],this.renderItemsIndex=0,this.opaque=[],this.transparentDoublePass=[],this.transparent=[],this.bundles=[],this.lightsNode=e.getNode(t,s),this.lightsArray=[],this.scene=t,this.camera=s,this.occlusionQueryCount=0}begin(){return this.renderItemsIndex=0,this.opaque.length=0,this.transparentDoublePass.length=0,this.transparent.length=0,this.bundles.length=0,this.lightsArray.length=0,this.occlusionQueryCount=0,this}getNextRenderItem(e,t,s,n,r,i,a){let u=this.renderItems[this.renderItemsIndex];return u===void 0?(u={id:e.id,object:e,geometry:t,material:s,groupOrder:n,renderOrder:e.renderOrder,z:r,group:i,clippingContext:a},this.renderItems[this.renderItemsIndex]=u):(u.id=e.id,u.object=e,u.geometry=t,u.material=s,u.groupOrder=n,u.renderOrder=e.renderOrder,u.z=r,u.group=i,u.clippingContext=a),this.renderItemsIndex++,u}push(e,t,s,n,r,i,a){const u=this.getNextRenderItem(e,t,s,n,r,i,a);e.occlusionTest===!0&&this.occlusionQueryCount++,s.transparent===!0||s.transmission>0?(Uc(s)&&this.transparentDoublePass.push(u),this.transparent.push(u)):this.opaque.push(u)}unshift(e,t,s,n,r,i,a){const u=this.getNextRenderItem(e,t,s,n,r,i,a);s.transparent===!0||s.transmission>0?(Uc(s)&&this.transparentDoublePass.unshift(u),this.transparent.unshift(u)):this.opaque.unshift(u)}pushBundle(e){this.bundles.push(e)}pushLight(e){this.lightsArray.push(e)}sort(e,t){this.opaque.length>1&&this.opaque.sort(e||B_),this.transparentDoublePass.length>1&&this.transparentDoublePass.sort(t||Fc),this.transparent.length>1&&this.transparent.sort(t||Fc)}finish(){this.lightsNode.setLights(this.lightsArray);for(let e=this.renderItemsIndex,t=this.renderItems.length;e>t,c=a.height>>t;let l=e.depthTexture||r[t];const d=e.depthBuffer===!0||e.stencilBuffer===!0;let h=!1;l===void 0&&d&&(l=new vs,l.format=e.stencilBuffer?oi:ai,l.type=e.stencilBuffer?ui:Le,l.image.width=u,l.image.height=c,r[t]=l),(s.width!==a.width||a.height!==s.height)&&(h=!0,l&&(l.needsUpdate=!0,l.image.width=u,l.image.height=c)),s.width=a.width,s.height=a.height,s.textures=i,s.depthTexture=l||null,s.depth=e.depthBuffer,s.stencil=e.stencilBuffer,s.renderTarget=e,s.sampleCount!==n&&(h=!0,l&&(l.needsUpdate=!0),s.sampleCount=n);const p={sampleCount:n};for(let f=0;f{e.removeEventListener("dispose",f);for(let m=0;m0){const l=e.image;if(l===void 0)console.warn("THREE.Renderer: Texture marked for update but image is undefined.");else if(l.complete===!1)console.warn("THREE.Renderer: Texture marked for update but image is incomplete.");else{if(e.images){const d=[];for(const h of e.images)d.push(h);t.images=d}else t.image=l;(s.isDefaultTexture===void 0||s.isDefaultTexture===!0)&&(r.createTexture(e,t),s.isDefaultTexture=!1,s.generation=e.version),e.source.dataReady===!0&&r.updateTexture(e,t),t.needsMipmaps&&e.mipmaps.length===0&&r.generateMipmaps(e)}}else r.createDefaultTexture(e),s.isDefaultTexture=!0,s.generation=e.version;if(s.initialized!==!0){s.initialized=!0,s.generation=e.version,this.info.memory.textures++;const c=()=>{e.removeEventListener("dispose",c),this._destroyTexture(e),this.info.memory.textures--};e.addEventListener("dispose",c)}s.version=e.version}getSize(e,t=I_){let s=e.images?e.images[0]:e.image;return s?(s.image!==void 0&&(s=s.image),t.width=s.width||1,t.height=s.height||1,t.depth=e.isCubeTexture?6:s.depth||1):t.width=t.height=t.depth=1,t}getMipLevels(e,t,s){let n;return e.isCompressedTexture?e.mipmaps?n=e.mipmaps.length:n=1:n=Math.floor(Math.log2(Math.max(t,s)))+1,n}needsMipmaps(e){return this.isEnvironmentTexture(e)||e.isCompressedTexture===!0||e.generateMipmaps}isEnvironmentTexture(e){const t=e.mapping;return t===jn||t===Qn||t===Zs||t===Js}_destroyTexture(e){this.backend.destroySampler(e),this.backend.destroyTexture(e),this.delete(e)}}class Ru extends mt{constructor(e,t,s,n=1){super(e,t,s),this.a=n}set(e,t,s,n=1){return this.a=n,super.set(e,t,s)}copy(e){return e.a!==void 0&&(this.a=e.a),super.copy(e)}clone(){return new this.constructor(this.r,this.g,this.b,this.a)}}class df extends se{static get type(){return"ParameterNode"}constructor(e,t=null){super(e,t),this.isParameterNode=!0}getHash(){return this.uuid}generate(){return this.name}}const G_=(o,e)=>E(new df(o,e));class O_ extends O{static get type(){return"StackNode"}constructor(e=null){super(),this.nodes=[],this.outputNode=null,this.parent=e,this._currentCond=null,this.isStackNode=!0}getNodeType(e){return this.outputNode?this.outputNode.getNodeType(e):"void"}add(e){return this.nodes.push(e),this}If(e,t){const s=new Cn(t);return this._currentCond=Ue(e,s),this.add(this._currentCond)}ElseIf(e,t){const s=new Cn(t),n=Ue(e,s);return this._currentCond.elseNode=n,this._currentCond=n,this}Else(e){return this._currentCond.elseNode=new Cn(e),this}build(e,...t){const s=Ea();On(this);for(const n of this.nodes)n.build(e,"void");return On(s),this.outputNode?this.outputNode.build(e,...t):super.build(e,...t)}else(...e){return console.warn("TSL.StackNode: .else() has been renamed to .Else()."),this.Else(...e)}elseif(...e){return console.warn("TSL.StackNode: .elseif() has been renamed to .ElseIf()."),this.ElseIf(...e)}}const kr=C(O_);class hf extends O{static get type(){return"OutputStructNode"}constructor(...e){super(),this.members=e,this.isOutputStructNode=!0}setup(e){super.setup(e);const t=this.members,s=[];for(let n=0;n{const e=o.toUint().mul(747796405).add(2891336453),t=e.shiftRight(e.shiftRight(28).add(4)).bitXor(e).mul(277803737);return t.shiftRight(22).bitXor(t).toFloat().mul(1/2**32)}),ta=(o,e)=>ht($(4,o.mul(Y(1,o))),e),$_=(o,e)=>o.lessThan(.5)?ta(o.mul(2),e).div(2):Y(1,ta($(Y(1,o),2),e).div(2)),H_=(o,e,t)=>ht(gt(ht(o,e),_e(ht(o,e),ht(Y(1,o),t))),1/e),q_=(o,e)=>st(Qr.mul(e.mul(o).sub(1))).div(Qr.mul(e.mul(o).sub(1))),Vt=b(([o])=>o.fract().sub(.5).abs()).setLayout({name:"tri",type:"float",inputs:[{name:"x",type:"float"}]}),K_=b(([o])=>T(Vt(o.z.add(Vt(o.y.mul(1)))),Vt(o.z.add(Vt(o.x.mul(1)))),Vt(o.y.add(Vt(o.x.mul(1)))))).setLayout({name:"tri3",type:"vec3",inputs:[{name:"p",type:"vec3"}]}),X_=b(([o,e,t])=>{const s=T(o).toVar(),n=g(1.4).toVar(),r=g(0).toVar(),i=T(s).toVar();return te({start:g(0),end:g(3),type:"float",condition:"<="},()=>{const a=T(K_(i.mul(2))).toVar();s.addAssign(a.add(t.mul(g(.1).mul(e)))),i.mulAssign(1.8),n.mulAssign(1.5),s.mulAssign(1.2);const u=g(Vt(s.z.add(Vt(s.x.add(Vt(s.y)))))).toVar();r.addAssign(u.div(n)),i.addAssign(.14)}),r}).setLayout({name:"triNoise3D",type:"float",inputs:[{name:"position",type:"vec3"},{name:"speed",type:"float"},{name:"time",type:"float"}]});class Y_ extends O{static get type(){return"FunctionOverloadingNode"}constructor(e=[],...t){super(),this.functionNodes=e,this.parametersNodes=t,this._candidateFnCall=null,this.global=!0}getNodeType(){return this.functionNodes[0].shaderNode.layout.type}setup(e){const t=this.parametersNodes;let s=this._candidateFnCall;if(s===null){let n=null,r=-1;for(const i of this.functionNodes){const u=i.shaderNode.layout;if(u===null)throw new Error("FunctionOverloadingNode: FunctionNode must be a layout.");const c=u.inputs;if(t.length===c.length){let l=0;for(let d=0;dr&&(n=i,r=l)}}this._candidateFnCall=s=n(...t)}return s}}const j_=C(Y_),Pe=o=>(...e)=>j_(o,...e),Cs=V(0).setGroup(k).onRenderUpdate(o=>o.time),gf=V(0).setGroup(k).onRenderUpdate(o=>o.deltaTime),Q_=V(0,"uint").setGroup(k).onRenderUpdate(o=>o.frameId),Z_=(o=1)=>(console.warn('TSL: timerLocal() is deprecated. Use "time" instead.'),Cs.mul(o)),J_=(o=1)=>(console.warn('TSL: timerGlobal() is deprecated. Use "time" instead.'),Cs.mul(o)),eb=(o=1)=>(console.warn('TSL: timerDelta() is deprecated. Use "deltaTime" instead.'),gf.mul(o)),tb=(o=Cs)=>o.add(.75).mul(Math.PI*2).sin().mul(.5).add(.5),sb=(o=Cs)=>o.fract().round(),nb=(o=Cs)=>o.add(.5).fract().mul(2).sub(1).abs(),rb=(o=Cs)=>o.fract(),ib=b(([o,e,t=M(.5)])=>Au(o.sub(t),e).add(t)),ob=b(([o,e,t=M(.5)])=>{const s=o.sub(t),n=s.dot(s),i=n.mul(n).mul(e);return o.add(s.mul(i))}),ab=b(({position:o=null,horizontal:e=!0,vertical:t=!1})=>{let s;o!==null?(s=at.toVar(),s[3][0]=o.x,s[3][1]=o.y,s[3][2]=o.z):s=at;const n=je.mul(s);return Gn(e)&&(n[0][0]=at[0].length(),n[0][1]=0,n[0][2]=0),Gn(t)&&(n[1][0]=0,n[1][1]=at[1].length(),n[1][2]=0),n[2][0]=0,n[2][1]=0,n[2][2]=1,He.mul(n).mul(me)}),ub=b(([o=null])=>{const e=ei();return ei(gu(o)).sub(e).lessThan(0).select(Bt,o)});class cb extends O{static get type(){return"SpriteSheetUVNode"}constructor(e,t=le(),s=g(0)){super("vec2"),this.countNode=e,this.uvNode=t,this.frameNode=s}setup(){const{frameNode:e,uvNode:t,countNode:s}=this,{width:n,height:r}=s,i=e.mod(n.mul(r)).floor(),a=i.mod(n),u=r.sub(i.add(1).div(n).ceil()),c=s.reciprocal(),l=M(a,u);return t.add(l).mul(c)}}const lb=C(cb);class db extends O{static get type(){return"TriplanarTexturesNode"}constructor(e,t=null,s=null,n=g(1),r=me,i=Xe){super("vec4"),this.textureXNode=e,this.textureYNode=t,this.textureZNode=s,this.scaleNode=n,this.positionNode=r,this.normalNode=i}setup(){const{textureXNode:e,textureYNode:t,textureZNode:s,scaleNode:n,positionNode:r,normalNode:i}=this;let a=i.abs().normalize();a=a.div(a.dot(T(1)));const u=r.yz.mul(n),c=r.zx.mul(n),l=r.xy.mul(n),d=e.value,h=t!==null?t.value:d,p=s!==null?s.value:d,f=K(d,u).mul(a.x),m=K(h,c).mul(a.y),x=K(p,l).mul(a.z);return _e(f,m,x)}}const mf=C(db),hb=(...o)=>mf(...o),Fs=new vl,as=new j,Us=new j,Yi=new j,yn=new Ie,Tr=new j(0,0,-1),xt=new Me,xn=new j,_r=new j,Tn=new Me,br=new Ye,si=new Ss,pb=Bt.flipX();si.depthTexture=new vs(1,1);let ji=!1;class Cu extends wt{static get type(){return"ReflectorNode"}constructor(e={}){super(e.defaultTexture||si.texture,pb),this._reflectorBaseNode=e.reflector||new fb(this,e),this._depthNode=null,this.setUpdateMatrix(!1)}get reflector(){return this._reflectorBaseNode}get target(){return this._reflectorBaseNode.target}getDepthNode(){if(this._depthNode===null){if(this._reflectorBaseNode.depth!==!0)throw new Error("THREE.ReflectorNode: Depth node can only be requested when the reflector is created with { depth: true }. ");this._depthNode=E(new Cu({defaultTexture:si.depthTexture,reflector:this._reflectorBaseNode}))}return this._depthNode}setup(e){return e.object.isQuadMesh||this._reflectorBaseNode.build(e),super.setup(e)}clone(){const e=new this.constructor(this.reflectorNode);return e._reflectorBaseNode=this._reflectorBaseNode,e}}class fb extends O{static get type(){return"ReflectorBaseNode"}constructor(e,t={}){super();const{target:s=new Zg,resolution:n=1,generateMipmaps:r=!1,bounces:i=!0,depth:a=!1}=t;this.textureNode=e,this.target=s,this.resolution=n,this.generateMipmaps=r,this.bounces=i,this.depth=a,this.updateBeforeType=i?W.RENDER:W.FRAME,this.virtualCameras=new WeakMap,this.renderTargets=new WeakMap}_updateResolution(e,t){const s=this.resolution;t.getDrawingBufferSize(br),e.setSize(Math.round(br.width*s),Math.round(br.height*s))}setup(e){return this._updateResolution(si,e.renderer),super.setup(e)}getVirtualCamera(e){let t=this.virtualCameras.get(e);return t===void 0&&(t=e.clone(),this.virtualCameras.set(e,t)),t}getRenderTarget(e){let t=this.renderTargets.get(e);return t===void 0&&(t=new Ss(0,0,{type:pt}),this.generateMipmaps===!0&&(t.texture.minFilter=Jg,t.texture.generateMipmaps=!0),this.depth===!0&&(t.depthTexture=new vs),this.renderTargets.set(e,t)),t}updateBefore(e){if(this.bounces===!1&&ji)return!1;ji=!0;const{scene:t,camera:s,renderer:n,material:r}=e,{target:i}=this,a=this.getVirtualCamera(s),u=this.getRenderTarget(a);if(n.getDrawingBufferSize(br),this._updateResolution(u,n),Us.setFromMatrixPosition(i.matrixWorld),Yi.setFromMatrixPosition(s.matrixWorld),yn.extractRotation(i.matrixWorld),as.set(0,0,1),as.applyMatrix4(yn),xn.subVectors(Us,Yi),xn.dot(as)>0)return;xn.reflect(as).negate(),xn.add(Us),yn.extractRotation(s.matrixWorld),Tr.set(0,0,-1),Tr.applyMatrix4(yn),Tr.add(Yi),_r.subVectors(Us,Tr),_r.reflect(as).negate(),_r.add(Us),a.coordinateSystem=s.coordinateSystem,a.position.copy(xn),a.up.set(0,1,0),a.up.applyMatrix4(yn),a.up.reflect(as),a.lookAt(_r),a.near=s.near,a.far=s.far,a.updateMatrixWorld(),a.projectionMatrix.copy(s.projectionMatrix),Fs.setFromNormalAndCoplanarPoint(as,Us),Fs.applyMatrix4(a.matrixWorldInverse),xt.set(Fs.normal.x,Fs.normal.y,Fs.normal.z,Fs.constant);const c=a.projectionMatrix;Tn.x=(Math.sign(xt.x)+c.elements[8])/c.elements[0],Tn.y=(Math.sign(xt.y)+c.elements[9])/c.elements[5],Tn.z=-1,Tn.w=(1+c.elements[10])/c.elements[14],xt.multiplyScalar(1/xt.dot(Tn));const l=0;c.elements[2]=xt.x,c.elements[6]=xt.y,c.elements[10]=n.coordinateSystem===ln?xt.z-l:xt.z+1-l,c.elements[14]=xt.w,this.textureNode.value=u.texture,this.depth===!0&&(this.textureNode.getDepthNode().value=u.depthTexture),r.visible=!1;const d=n.getRenderTarget(),h=n.getMRT(),p=n.autoClear;n.setMRT(null),n.setRenderTarget(u),n.autoClear=!0,n.render(t,a),n.setMRT(h),n.setRenderTarget(d),n.autoClear=p,r.visible=!0,ji=!1}}const gb=o=>E(new Cu(o)),Qi=new Tl(-1,1,1,-1,0,1);class mb extends Sl{constructor(e=!1){super();const t=e===!1?[0,-1,0,1,2,1]:[0,2,0,0,2,0];this.setAttribute("position",new ju([-1,3,0,-1,-1,0,3,-1,0],3)),this.setAttribute("uv",new ju(t,2))}}const yb=new mb;class Mi extends tn{constructor(e=null){super(yb,e),this.camera=Qi,this.isQuadMesh=!0}async renderAsync(e){return e.renderAsync(this,Qi)}render(e){e.render(this,Qi)}}const xb=new Ye;class Tb extends wt{static get type(){return"RTTNode"}constructor(e,t=null,s=null,n={type:pt}){const r=new Ss(t,s,n);super(r.texture,le()),this.node=e,this.width=t,this.height=s,this.pixelRatio=1,this.renderTarget=r,this.textureNeedsUpdate=!0,this.autoUpdate=!0,this._rttNode=null,this._quadMesh=new Mi(new ce),this.updateBeforeType=W.RENDER}get autoSize(){return this.width===null}setup(e){return this._rttNode=this.node.context(e.getSharedContext()),this._quadMesh.material.name="RTT",this._quadMesh.material.needsUpdate=!0,super.setup(e)}setSize(e,t){this.width=e,this.height=t;const s=e*this.pixelRatio,n=t*this.pixelRatio;this.renderTarget.setSize(s,n),this.textureNeedsUpdate=!0}setPixelRatio(e){this.pixelRatio=e,this.setSize(this.width,this.height)}updateBefore({renderer:e}){if(this.textureNeedsUpdate===!1&&this.autoUpdate===!1)return;if(this.textureNeedsUpdate=!1,this.autoSize===!0){this.pixelRatio=e.getPixelRatio();const s=e.getSize(xb);this.setSize(s.width,s.height)}this._quadMesh.material.fragmentNode=this._rttNode;const t=e.getRenderTarget();e.setRenderTarget(this.renderTarget),this._quadMesh.render(e),e.setRenderTarget(t)}clone(){const e=new wt(this.value,this.uvNode,this.levelNode);return e.sampler=this.sampler,e.referenceNode=this,e}}const yf=(o,...e)=>E(new Tb(E(o),...e)),_b=(o,...e)=>o.isTextureNode?o:o.isPassNode?o.getTextureNode():yf(o,...e),ks=b(([o,e,t],s)=>{let n;s.renderer.coordinateSystem===ln?(o=M(o.x,o.y.oneMinus()).mul(2).sub(1),n=P(T(o,e),1)):n=P(T(o.x,o.y.oneMinus(),e).mul(2).sub(1),1);const r=P(t.mul(n));return r.xyz.div(r.w)}),bb=b(([o,e])=>{const t=e.mul(P(o,1)),s=t.xy.div(t.w).mul(.5).add(.5).toVar();return M(s.x,s.y.oneMinus())}),Nb=b(([o,e,t])=>{const s=ns(ge(e)),n=Ae(o.mul(s)).toVar(),r=ge(e,n).toVar(),i=ge(e,n.sub(Ae(2,0))).toVar(),a=ge(e,n.sub(Ae(1,0))).toVar(),u=ge(e,n.add(Ae(1,0))).toVar(),c=ge(e,n.add(Ae(2,0))).toVar(),l=ge(e,n.add(Ae(0,2))).toVar(),d=ge(e,n.add(Ae(0,1))).toVar(),h=ge(e,n.sub(Ae(0,1))).toVar(),p=ge(e,n.sub(Ae(0,2))).toVar(),f=oe(Y(g(2).mul(a).sub(i),r)).toVar(),m=oe(Y(g(2).mul(u).sub(c),r)).toVar(),x=oe(Y(g(2).mul(d).sub(l),r)).toVar(),N=oe(Y(g(2).mul(h).sub(p),r)).toVar(),v=ks(o,r,t).toVar(),w=f.lessThan(m).select(v.sub(ks(o.sub(M(g(1).div(s.x),0)),a,t)),v.negate().add(ks(o.add(M(g(1).div(s.x),0)),u,t))),B=x.lessThan(N).select(v.sub(ks(o.add(M(0,g(1).div(s.y))),d,t)),v.negate().add(ks(o.sub(M(0,g(1).div(s.y))),h,t)));return qt(_i(w,B))});class Sb extends oa{constructor(e,t,s=Float32Array){const n=ArrayBuffer.isView(e)?e:new s(e*t);super(n,t),this.isStorageInstancedBufferAttribute=!0}}class xf extends Ur{constructor(e,t,s=Float32Array){const n=ArrayBuffer.isView(e)?e:new s(e*t);super(n,t),this.isStorageBufferAttribute=!0}}class vb extends Rs{static get type(){return"StorageArrayElementNode"}constructor(e,t){super(e,t),this.isStorageArrayElementNode=!0}set storageBufferNode(e){this.node=e}get storageBufferNode(){return this.node}setup(e){return e.isAvailable("storageBuffer")===!1&&this.node.isPBO===!0&&e.setupPBO(this.node),super.setup(e)}generate(e,t){let s;const n=e.context.assign;if(e.isAvailable("storageBuffer")===!1?this.node.isPBO===!0&&n!==!0&&(this.node.value.isInstancedBufferAttribute||e.shaderStage!=="compute")?s=e.generatePBO(this):s=this.node.build(e):s=super.generate(e),n!==!0){const r=this.getNodeType(e);s=e.format(s,r,t)}return s}}const Ab=C(vb);class Rb extends iu{static get type(){return"StorageBufferNode"}constructor(e,t=null,s=0){t===null&&(e.isStorageBufferAttribute||e.isStorageInstancedBufferAttribute)&&(t=ya(e.itemSize),s=e.count),super(e,t,s),this.isStorageBufferNode=!0,this.access=Ve.READ_WRITE,this.isAtomic=!1,this.isPBO=!1,this._attribute=null,this._varying=null,this.global=!0,e.isStorageBufferAttribute!==!0&&e.isStorageInstancedBufferAttribute!==!0&&(e.isInstancedBufferAttribute?e.isStorageInstancedBufferAttribute=!0:e.isStorageBufferAttribute=!0)}getHash(e){if(this.bufferCount===0){let t=e.globalCache.getData(this.value);return t===void 0&&(t={node:this},e.globalCache.setData(this.value,t)),t.node.uuid}return this.uuid}getInputType(){return this.value.isIndirectStorageBufferAttribute?"indirectStorageBuffer":"storageBuffer"}element(e){return Ab(this,e)}setPBO(e){return this.isPBO=e,this}getPBO(){return this.isPBO}setAccess(e){return this.access=e,this}toReadOnly(){return this.setAccess(Ve.READ_ONLY)}setAtomic(e){return this.isAtomic=e,this}toAtomic(){return this.setAtomic(!0)}getAttributeData(){return this._attribute===null&&(this._attribute=nr(this.value),this._varying=ke(this._attribute)),{attribute:this._attribute,varying:this._varying}}getNodeType(e){if(e.isAvailable("storageBuffer")||e.isAvailable("indirectStorageBuffer"))return super.getNodeType(e);const{attribute:t}=this.getAttributeData();return t.getNodeType(e)}generate(e){if(e.isAvailable("storageBuffer")||e.isAvailable("indirectStorageBuffer"))return super.generate(e);const{attribute:t,varying:s}=this.getAttributeData(),n=s.build(e);return e.registerTransform(n,t),n}}const Bi=(o,e=null,t=0)=>E(new Rb(o,e,t)),Cb=(o,e,t)=>(console.warn('THREE.TSL: "storageObject()" is deprecated. Use "storage().setPBO( true )" instead.'),Bi(o,e,t).setPBO(!0)),Eb=(o,e="float")=>{const t=Ta(e),s=xa(e),n=new xf(o,t,s);return Bi(n,e,o)},wb=(o,e="float")=>{const t=Ta(e),s=xa(e),n=new Sb(o,t,s);return Bi(n,e,o)};class Mb extends Uh{static get type(){return"VertexColorNode"}constructor(e=0){super(null,"vec4"),this.isVertexColorNode=!0,this.index=e}getAttributeName(){const e=this.index;return"color"+(e>0?e:"")}generate(e){const t=this.getAttributeName(e),s=e.hasGeometryAttribute(t);let n;return s===!0?n=super.generate(e):n=e.generateConst(this.nodeType,new Me(1,1,1,1)),n}serialize(e){super.serialize(e),e.index=this.index}deserialize(e){super.deserialize(e),this.index=e.index}}const Bb=o=>E(new Mb(o));class Fb extends O{static get type(){return"PointUVNode"}constructor(){super("vec2"),this.isPointUVNode=!0}generate(){return"vec2( gl_PointCoord.x, 1.0 - gl_PointCoord.y )"}}const Ub=U(Fb),_n=new Wg,Zi=new Ie;class Ke extends O{static get type(){return"SceneNode"}constructor(e=Ke.BACKGROUND_BLURRINESS,t=null){super(),this.scope=e,this.scene=t}setup(e){const t=this.scope,s=this.scene!==null?this.scene:e.scene;let n;return t===Ke.BACKGROUND_BLURRINESS?n=ne("backgroundBlurriness","float",s):t===Ke.BACKGROUND_INTENSITY?n=ne("backgroundIntensity","float",s):t===Ke.BACKGROUND_ROTATION?n=V("mat4").label("backgroundRotation").setGroup(k).onRenderUpdate(()=>{const r=s.background;return r!==null&&r.isTexture&&r.mapping!==zg?(_n.copy(s.backgroundRotation),_n.x*=-1,_n.y*=-1,_n.z*=-1,Zi.makeRotationFromEuler(_n)):Zi.identity(),Zi}):console.error("THREE.SceneNode: Unknown scope:",t),n}}Ke.BACKGROUND_BLURRINESS="backgroundBlurriness";Ke.BACKGROUND_INTENSITY="backgroundIntensity";Ke.BACKGROUND_ROTATION="backgroundRotation";const Tf=U(Ke,Ke.BACKGROUND_BLURRINESS),sa=U(Ke,Ke.BACKGROUND_INTENSITY),_f=U(Ke,Ke.BACKGROUND_ROTATION);class Pb extends wt{static get type(){return"StorageTextureNode"}constructor(e,t,s=null){super(e,t),this.storeNode=s,this.isStorageTextureNode=!0,this.access=Ve.WRITE_ONLY}getInputType(){return"storageTexture"}setup(e){super.setup(e);const t=e.getNodeProperties(this);t.storeNode=this.storeNode}setAccess(e){return this.access=e,this}generate(e,t){let s;return this.storeNode!==null?s=this.generateStore(e):s=super.generate(e,t),s}toReadWrite(){return this.setAccess(Ve.READ_WRITE)}toReadOnly(){return this.setAccess(Ve.READ_ONLY)}toWriteOnly(){return this.setAccess(Ve.WRITE_ONLY)}generateStore(e){const t=e.getNodeProperties(this),{uvNode:s,storeNode:n}=t,r=super.generate(e,"property"),i=s.build(e,"uvec2"),a=n.build(e,"vec4"),u=e.generateTextureStore(e,r,i,a);e.addLineFlowCode(u,this)}}const bf=C(Pb),Db=(o,e,t)=>{const s=bf(o,e,t);return t!==null&&s.append(),s};class Lb extends Ai{static get type(){return"UserDataNode"}constructor(e,t,s=null){super(e,t,s),this.userData=s}updateReference(e){return this.reference=this.userData!==null?this.userData:e.object.userData,this.reference}}const Ib=(o,e,t)=>E(new Lb(o,e,t)),Pc=new WeakMap;class Vb extends be{static get type(){return"VelocityNode"}constructor(){super("vec2"),this.projectionMatrix=null,this.updateType=W.OBJECT,this.updateAfterType=W.OBJECT,this.previousModelWorldMatrix=V(new Ie),this.previousProjectionMatrix=V(new Ie).setGroup(k),this.previousCameraViewMatrix=V(new Ie)}setProjectionMatrix(e){this.projectionMatrix=e}update({frameId:e,camera:t,object:s}){const n=Dc(s);this.previousModelWorldMatrix.value.copy(n);const r=Nf(t);r.frameId!==e&&(r.frameId=e,r.previousProjectionMatrix===void 0?(r.previousProjectionMatrix=new Ie,r.previousCameraViewMatrix=new Ie,r.currentProjectionMatrix=new Ie,r.currentCameraViewMatrix=new Ie,r.previousProjectionMatrix.copy(this.projectionMatrix||t.projectionMatrix),r.previousCameraViewMatrix.copy(t.matrixWorldInverse)):(r.previousProjectionMatrix.copy(r.currentProjectionMatrix),r.previousCameraViewMatrix.copy(r.currentCameraViewMatrix)),r.currentProjectionMatrix.copy(this.projectionMatrix||t.projectionMatrix),r.currentCameraViewMatrix.copy(t.matrixWorldInverse),this.previousProjectionMatrix.value.copy(r.previousProjectionMatrix),this.previousCameraViewMatrix.value.copy(r.previousCameraViewMatrix))}updateAfter({object:e}){Dc(e).copy(e.matrixWorld)}setup(){const e=this.projectionMatrix===null?He:V(this.projectionMatrix),t=this.previousCameraViewMatrix.mul(this.previousModelWorldMatrix),s=e.mul(Kt).mul(me),n=this.previousProjectionMatrix.mul(t).mul(Jr),r=s.xy.div(s.w),i=n.xy.div(n.w);return Y(r,i)}}function Nf(o){let e=Pc.get(o);return e===void 0&&(e={},Pc.set(o,e)),e}function Dc(o,e=0){const t=Nf(o);let s=t[e];return s===void 0&&(t[e]=s=new Ie),s}const Gb=U(Vb),Sf=b(([o,e])=>Re(1,o.oneMinus().div(e)).oneMinus()).setLayout({name:"blendBurn",type:"vec3",inputs:[{name:"base",type:"vec3"},{name:"blend",type:"vec3"}]}),vf=b(([o,e])=>Re(o.div(e.oneMinus()),1)).setLayout({name:"blendDodge",type:"vec3",inputs:[{name:"base",type:"vec3"},{name:"blend",type:"vec3"}]}),Af=b(([o,e])=>o.oneMinus().mul(e.oneMinus()).oneMinus()).setLayout({name:"blendScreen",type:"vec3",inputs:[{name:"base",type:"vec3"},{name:"blend",type:"vec3"}]}),Rf=b(([o,e])=>Q(o.mul(2).mul(e),o.oneMinus().mul(2).mul(e.oneMinus()).oneMinus(),Ti(.5,o))).setLayout({name:"blendOverlay",type:"vec3",inputs:[{name:"base",type:"vec3"},{name:"blend",type:"vec3"}]}),Ob=b(([o,e])=>{const t=e.a.add(o.a.mul(e.a.oneMinus()));return P(e.rgb.mul(e.a).add(o.rgb.mul(o.a).mul(e.a.oneMinus())).div(t),t)}).setLayout({name:"blendColor",type:"vec4",inputs:[{name:"base",type:"vec4"},{name:"blend",type:"vec4"}]}),kb=(...o)=>(console.warn('THREE.TSL: "burn" has been renamed. Use "blendBurn" instead.'),Sf(o)),zb=(...o)=>(console.warn('THREE.TSL: "dodge" has been renamed. Use "blendDodge" instead.'),vf(o)),Wb=(...o)=>(console.warn('THREE.TSL: "screen" has been renamed. Use "blendScreen" instead.'),Af(o)),$b=(...o)=>(console.warn('THREE.TSL: "overlay" has been renamed. Use "blendOverlay" instead.'),Rf(o)),Hb=b(([o])=>Eu(o.rgb)),qb=b(([o,e=g(1)])=>e.mix(Eu(o.rgb),o.rgb)),Kb=b(([o,e=g(1)])=>{const t=_e(o.r,o.g,o.b).div(3),s=o.r.max(o.g.max(o.b)),n=s.sub(t).mul(e).mul(-3);return Q(o.rgb,s,n)}),Xb=b(([o,e=g(1)])=>{const t=T(.57735,.57735,.57735),s=e.cos();return T(o.rgb.mul(s).add(t.cross(o.rgb).mul(e.sin()).add(t.mul(is(t,o.rgb).mul(s.oneMinus())))))}),Eu=(o,e=T(_t.getLuminanceCoefficients(new j)))=>is(o,e),Yb=b(([o,e=T(1),t=T(0),s=T(1),n=g(1),r=T(_t.getLuminanceCoefficients(new j,kt))])=>{const i=o.rgb.dot(T(r)),a=ue(o.rgb.mul(e).add(t),0).toVar(),u=a.pow(s).toVar();return z(a.r.greaterThan(0),()=>{a.r.assign(u.r)}),z(a.g.greaterThan(0),()=>{a.g.assign(u.g)}),z(a.b.greaterThan(0),()=>{a.b.assign(u.b)}),a.assign(i.add(a.sub(i).mul(n))),P(a.rgb,o.a)});class jb extends be{static get type(){return"PosterizeNode"}constructor(e,t){super(),this.sourceNode=e,this.stepsNode=t}setup(){const{sourceNode:e,stepsNode:t}=this;return e.mul(t).floor().div(t)}}const Qb=C(jb),Zb=new Ye;class Cf extends wt{static get type(){return"PassTextureNode"}constructor(e,t){super(t),this.passNode=e,this.setUpdateMatrix(!1)}setup(e){return e.object.isQuadMesh&&this.passNode.build(e),super.setup(e)}clone(){return new this.constructor(this.passNode,this.value)}}class Lc extends Cf{static get type(){return"PassMultipleTextureNode"}constructor(e,t,s=!1){super(e,null),this.textureName=t,this.previousTexture=s}updateTexture(){this.value=this.previousTexture?this.passNode.getPreviousTexture(this.textureName):this.passNode.getTexture(this.textureName)}setup(e){return this.updateTexture(),super.setup(e)}clone(){return new this.constructor(this.passNode,this.textureName,this.previousTexture)}}class Ut extends be{static get type(){return"PassNode"}constructor(e,t,s,n={}){super("vec4"),this.scope=e,this.scene=t,this.camera=s,this.options=n,this._pixelRatio=1,this._width=1,this._height=1;const r=new vs;r.isRenderTargetTexture=!0,r.name="depth";const i=new Ss(this._width*this._pixelRatio,this._height*this._pixelRatio,{type:pt,...n});i.texture.name="output",i.depthTexture=r,this.renderTarget=i,this._textures={output:i.texture,depth:r},this._textureNodes={},this._linearDepthNodes={},this._viewZNodes={},this._previousTextures={},this._previousTextureNodes={},this._cameraNear=V(0),this._cameraFar=V(0),this._mrt=null,this.isPassNode=!0,this.updateBeforeType=W.FRAME}setMRT(e){return this._mrt=e,this}getMRT(){return this._mrt}isGlobal(){return!0}getTexture(e){let t=this._textures[e];return t===void 0&&(t=this.renderTarget.texture.clone(),t.name=e,this._textures[e]=t,this.renderTarget.textures.push(t)),t}getPreviousTexture(e){let t=this._previousTextures[e];return t===void 0&&(t=this.getTexture(e).clone(),this._previousTextures[e]=t),t}toggleTexture(e){const t=this._previousTextures[e];if(t!==void 0){const s=this._textures[e],n=this.renderTarget.textures.indexOf(s);this.renderTarget.textures[n]=t,this._textures[e]=t,this._previousTextures[e]=s,this._textureNodes[e].updateTexture(),this._previousTextureNodes[e].updateTexture()}}getTextureNode(e="output"){let t=this._textureNodes[e];return t===void 0&&(t=E(new Lc(this,e)),t.updateTexture(),this._textureNodes[e]=t),t}getPreviousTextureNode(e="output"){let t=this._previousTextureNodes[e];return t===void 0&&(this._textureNodes[e]===void 0&&this.getTextureNode(e),t=E(new Lc(this,e,!0)),t.updateTexture(),this._previousTextureNodes[e]=t),t}getViewZNode(e="depth"){let t=this._viewZNodes[e];if(t===void 0){const s=this._cameraNear,n=this._cameraFar;this._viewZNodes[e]=t=mu(this.getTextureNode(e),s,n)}return t}getLinearDepthNode(e="depth"){let t=this._linearDepthNodes[e];if(t===void 0){const s=this._cameraNear,n=this._cameraFar,r=this.getViewZNode(e);this._linearDepthNodes[e]=t=Qs(r,s,n)}return t}setup({renderer:e}){return this.renderTarget.samples=this.options.samples===void 0?e.samples:this.options.samples,e.backend.isWebGLBackend===!0&&(this.renderTarget.samples=0),this.scope===Ut.COLOR?this.getTextureNode():this.getLinearDepthNode()}updateBefore(e){const{renderer:t}=e,{scene:s,camera:n}=this;this._pixelRatio=t.getPixelRatio();const r=t.getSize(Zb);this.setSize(r.width,r.height);const i=t.getRenderTarget(),a=t.getMRT();this._cameraNear.value=n.near,this._cameraFar.value=n.far;for(const u in this._previousTextures)this.toggleTexture(u);t.setRenderTarget(this.renderTarget),t.setMRT(this._mrt),t.render(s,n),t.setRenderTarget(i),t.setMRT(a)}setSize(e,t){this._width=e,this._height=t;const s=this._width*this._pixelRatio,n=this._height*this._pixelRatio;this.renderTarget.setSize(s,n)}setPixelRatio(e){this._pixelRatio=e,this.setSize(this._width,this._height)}dispose(){this.renderTarget.dispose()}}Ut.COLOR="color";Ut.DEPTH="depth";const Jb=(o,e,t)=>E(new Ut(Ut.COLOR,o,e,t)),eN=(o,e)=>E(new Cf(o,e)),tN=(o,e,t)=>E(new Ut(Ut.DEPTH,o,e,t));class sN extends Ut{static get type(){return"ToonOutlinePassNode"}constructor(e,t,s,n,r){super(Ut.COLOR,e,t),this.colorNode=s,this.thicknessNode=n,this.alphaNode=r,this._materialCache=new WeakMap}updateBefore(e){const{renderer:t}=e,s=t.getRenderObjectFunction();t.setRenderObjectFunction((n,r,i,a,u,c,l,d)=>{if((u.isMeshToonMaterial||u.isMeshToonNodeMaterial)&&u.wireframe===!1){const h=this._getOutlineMaterial(u);t.renderObject(n,r,i,a,h,c,l,d)}t.renderObject(n,r,i,a,u,c,l,d)}),super.updateBefore(e),t.setRenderObjectFunction(s)}_createMaterial(){const e=new ce;e.isMeshToonOutlineMaterial=!0,e.name="Toon_Outline",e.side=Ct;const t=Xe.negate(),s=He.mul(Kt),n=g(1),r=s.mul(P(me,1)),i=s.mul(P(me.add(t),1)),a=qt(r.sub(i));return e.vertexNode=r.add(a.mul(this.thicknessNode).mul(r.w).mul(n)),e.colorNode=P(this.colorNode,this.alphaNode),e}_getOutlineMaterial(e){let t=this._materialCache.get(e);return t===void 0&&(t=this._createMaterial(),this._materialCache.set(e,t)),t}}const nN=(o,e,t=new mt(0,0,0),s=.003,n=1)=>E(new sN(o,e,E(t),E(s),E(n))),Ef=b(([o,e])=>o.mul(e).clamp()).setLayout({name:"linearToneMapping",type:"vec3",inputs:[{name:"color",type:"vec3"},{name:"exposure",type:"float"}]}),wf=b(([o,e])=>(o=o.mul(e),o.div(o.add(1)).clamp())).setLayout({name:"reinhardToneMapping",type:"vec3",inputs:[{name:"color",type:"vec3"},{name:"exposure",type:"float"}]}),Mf=b(([o,e])=>{o=o.mul(e),o=o.sub(.004).max(0);const t=o.mul(o.mul(6.2).add(.5)),s=o.mul(o.mul(6.2).add(1.7)).add(.06);return t.div(s).pow(2.2)}).setLayout({name:"cineonToneMapping",type:"vec3",inputs:[{name:"color",type:"vec3"},{name:"exposure",type:"float"}]}),rN=b(([o])=>{const e=o.mul(o.add(.0245786)).sub(90537e-9),t=o.mul(o.add(.432951).mul(.983729)).add(.238081);return e.div(t)}),Bf=b(([o,e])=>{const t=Oe(.59719,.35458,.04823,.076,.90834,.01566,.0284,.13383,.83777),s=Oe(1.60475,-.53108,-.07367,-.10208,1.10813,-.00605,-.00327,-.07276,1.07602);return o=o.mul(e).div(.6),o=t.mul(o),o=rN(o),o=s.mul(o),o.clamp()}).setLayout({name:"acesFilmicToneMapping",type:"vec3",inputs:[{name:"color",type:"vec3"},{name:"exposure",type:"float"}]}),iN=Oe(T(1.6605,-.1246,-.0182),T(-.5876,1.1329,-.1006),T(-.0728,-.0083,1.1187)),oN=Oe(T(.6274,.0691,.0164),T(.3293,.9195,.088),T(.0433,.0113,.8956)),aN=b(([o])=>{const e=T(o).toVar(),t=T(e.mul(e)).toVar(),s=T(t.mul(t)).toVar();return g(15.5).mul(s.mul(t)).sub($(40.14,s.mul(e))).add($(31.96,s).sub($(6.868,t.mul(e))).add($(.4298,t).add($(.1191,e).sub(.00232))))}),Ff=b(([o,e])=>{const t=T(o).toVar(),s=Oe(T(.856627153315983,.137318972929847,.11189821299995),T(.0951212405381588,.761241990602591,.0767994186031903),T(.0482516061458583,.101439036467562,.811302368396859)),n=Oe(T(1.1271005818144368,-.1413297634984383,-.14132976349843826),T(-.11060664309660323,1.157823702216272,-.11060664309660294),T(-.016493938717834573,-.016493938717834257,1.2519364065950405)),r=g(-12.47393),i=g(4.026069);return t.mulAssign(e),t.assign(oN.mul(t)),t.assign(s.mul(t)),t.assign(ue(t,1e-10)),t.assign(vt(t)),t.assign(t.sub(r).div(i.sub(r))),t.assign(Et(t,0,1)),t.assign(aN(t)),t.assign(n.mul(t)),t.assign(ht(ue(T(0),t),T(2.2))),t.assign(iN.mul(t)),t.assign(Et(t,0,1)),t}).setLayout({name:"agxToneMapping",type:"vec3",inputs:[{name:"color",type:"vec3"},{name:"exposure",type:"float"}]}),Uf=b(([o,e])=>{const t=g(.76),s=g(.15);o=o.mul(e);const n=Re(o.r,Re(o.g,o.b)),r=Ue(n.lessThan(.08),n.sub($(6.25,n.mul(n))),.04);o.subAssign(r);const i=ue(o.r,ue(o.g,o.b));z(i.lessThan(t),()=>o);const a=Y(1,t),u=Y(1,a.mul(a).div(i.add(a.sub(t))));o.mulAssign(u.div(i));const c=Y(1,gt(1,s.mul(i.sub(u)).add(1)));return Q(o,T(u),c)}).setLayout({name:"neutralToneMapping",type:"vec3",inputs:[{name:"color",type:"vec3"},{name:"exposure",type:"float"}]});class Ne extends O{static get type(){return"CodeNode"}constructor(e="",t=[],s=""){super("code"),this.isCodeNode=!0,this.code=e,this.includes=t,this.language=s}isGlobal(){return!0}setIncludes(e){return this.includes=e,this}getIncludes(){return this.includes}generate(e){const t=this.getIncludes(e);for(const n of t)n.build(e);const s=e.getCodeFromNode(this,this.getNodeType(e));return s.code=this.code,s.code}serialize(e){super.serialize(e),e.code=this.code,e.language=this.language}deserialize(e){super.deserialize(e),this.code=e.code,this.language=e.language}}const Fi=C(Ne),uN=(o,e)=>Fi(o,e,"js"),cN=(o,e)=>Fi(o,e,"wgsl"),lN=(o,e)=>Fi(o,e,"glsl");class Pf extends Ne{static get type(){return"FunctionNode"}constructor(e="",t=[],s=""){super(e,t,s)}getNodeType(e){return this.getNodeFunction(e).type}getInputs(e){return this.getNodeFunction(e).inputs}getNodeFunction(e){const t=e.getDataFromNode(this);let s=t.nodeFunction;return s===void 0&&(s=e.parser.parseFunction(this.code),t.nodeFunction=s),s}generate(e,t){super.generate(e);const s=this.getNodeFunction(e),n=s.name,r=s.type,i=e.getCodeFromNode(this,r);n!==""&&(i.name=n);const a=e.getPropertyName(i),u=this.getNodeFunction(e).getCode(a);return i.code=u+` -`,t==="property"?a:e.format(`${a}()`,r,t)}}const Df=(o,e=[],t="")=>{for(let r=0;rs.call(...r);return n.functionNode=s,n},dN=(o,e)=>Df(o,e,"glsl"),hN=(o,e)=>Df(o,e,"wgsl");class pN extends O{static get type(){return"ScriptableValueNode"}constructor(e=null){super(),this._value=e,this._cache=null,this.inputType=null,this.outputType=null,this.events=new gl,this.isScriptableValueNode=!0}get isScriptableOutputNode(){return this.outputType!==null}set value(e){this._value!==e&&(this._cache&&this.inputType==="URL"&&this.value.value instanceof ArrayBuffer&&(URL.revokeObjectURL(this._cache),this._cache=null),this._value=e,this.events.dispatchEvent({type:"change"}),this.refresh())}get value(){return this._value}refresh(){this.events.dispatchEvent({type:"refresh"})}getValue(){const e=this.value;if(e&&this._cache===null&&this.inputType==="URL"&&e.value instanceof ArrayBuffer)this._cache=URL.createObjectURL(new Blob([e.value]));else if(e&&e.value!==null&&e.value!==void 0&&((this.inputType==="URL"||this.inputType==="String")&&typeof e.value=="string"||this.inputType==="Number"&&typeof e.value=="number"||this.inputType==="Vector2"&&e.value.isVector2||this.inputType==="Vector3"&&e.value.isVector3||this.inputType==="Vector4"&&e.value.isVector4||this.inputType==="Color"&&e.value.isColor||this.inputType==="Matrix3"&&e.value.isMatrix3||this.inputType==="Matrix4"&&e.value.isMatrix4))return e.value;return this._cache||e}getNodeType(e){return this.value&&this.value.isNode?this.value.getNodeType(e):"float"}setup(){return this.value&&this.value.isNode?this.value:g()}serialize(e){super.serialize(e),this.value!==null?this.inputType==="ArrayBuffer"?e.value=Na(this.value):e.value=this.value?this.value.toJSON(e.meta).uuid:null:e.value=null,e.inputType=this.inputType,e.outputType=this.outputType}deserialize(e){super.deserialize(e);let t=null;e.value!==null&&(e.inputType==="ArrayBuffer"?t=Sa(e.value):e.inputType==="Texture"?t=e.meta.textures[e.value]:t=e.meta.nodes[e.value]||null),this.value=t,this.inputType=e.inputType,this.outputType=e.outputType}}const zr=C(pN);class Lf extends Map{get(e,t=null,...s){if(this.has(e))return super.get(e);if(t!==null){const n=t(...s);return this.set(e,n),n}}}class fN{constructor(e){this.scriptableNode=e}get parameters(){return this.scriptableNode.parameters}get layout(){return this.scriptableNode.getLayout()}getInputLayout(e){return this.scriptableNode.getInputLayout(e)}get(e){const t=this.parameters[e];return t?t.getValue():null}}const Wr=new Lf;class gN extends O{static get type(){return"ScriptableNode"}constructor(e=null,t={}){super(),this.codeNode=e,this.parameters=t,this._local=new Lf,this._output=zr(),this._outputs={},this._source=this.source,this._method=null,this._object=null,this._value=null,this._needsOutputUpdate=!0,this.onRefresh=this.onRefresh.bind(this),this.isScriptableNode=!0}get source(){return this.codeNode?this.codeNode.code:""}setLocal(e,t){return this._local.set(e,t)}getLocal(e){return this._local.get(e)}onRefresh(){this._refresh()}getInputLayout(e){for(const t of this.getLayout())if(t.inputType&&(t.id===e||t.name===e))return t}getOutputLayout(e){for(const t of this.getLayout())if(t.outputType&&(t.id===e||t.name===e))return t}setOutput(e,t){const s=this._outputs;return s[e]===void 0?s[e]=zr(t):s[e].value=t,this}getOutput(e){return this._outputs[e]}getParameter(e){return this.parameters[e]}setParameter(e,t){const s=this.parameters;return t&&t.isScriptableNode?(this.deleteParameter(e),s[e]=t,s[e].getDefaultOutput().events.addEventListener("refresh",this.onRefresh)):t&&t.isScriptableValueNode?(this.deleteParameter(e),s[e]=t,s[e].events.addEventListener("refresh",this.onRefresh)):s[e]===void 0?(s[e]=zr(t),s[e].events.addEventListener("refresh",this.onRefresh)):s[e].value=t,this}getValue(){return this.getDefaultOutput().getValue()}deleteParameter(e){let t=this.parameters[e];return t&&(t.isScriptableNode&&(t=t.getDefaultOutput()),t.events.removeEventListener("refresh",this.onRefresh)),this}clearParameters(){for(const e of Object.keys(this.parameters))this.deleteParameter(e);return this.needsUpdate=!0,this}call(e,...t){const n=this.getObject()[e];if(typeof n=="function")return n(...t)}async callAsync(e,...t){const n=this.getObject()[e];if(typeof n=="function")return n.constructor.name==="AsyncFunction"?await n(...t):n(...t)}getNodeType(e){return this.getDefaultOutputNode().getNodeType(e)}refresh(e=null){e!==null?this.getOutput(e).refresh():this._refresh()}getObject(){if(this.needsUpdate&&this.dispose(),this._object!==null)return this._object;const e=()=>this.refresh(),t=(c,l)=>this.setOutput(c,l),s=new fN(this),n=Wr.get("THREE"),r=Wr.get("TSL"),i=this.getMethod(),a=[s,this._local,Wr,e,t,n,r];this._object=i(...a);const u=this._object.layout;if(u&&(u.cache===!1&&this._local.clear(),this._output.outputType=u.outputType||null,Array.isArray(u.elements)))for(const c of u.elements){const l=c.id||c.name;c.inputType&&(this.getParameter(l)===void 0&&this.setParameter(l,null),this.getParameter(l).inputType=c.inputType),c.outputType&&(this.getOutput(l)===void 0&&this.setOutput(l,null),this.getOutput(l).outputType=c.outputType)}return this._object}deserialize(e){super.deserialize(e);for(const t in this.parameters){let s=this.parameters[t];s.isScriptableNode&&(s=s.getDefaultOutput()),s.events.addEventListener("refresh",this.onRefresh)}}getLayout(){return this.getObject().layout}getDefaultOutputNode(){const e=this.getDefaultOutput().value;return e&&e.isNode?e:g()}getDefaultOutput(){return this._exec()._output}getMethod(){if(this.needsUpdate&&this.dispose(),this._method!==null)return this._method;const e=["parameters","local","global","refresh","setOutput","THREE","TSL"],s=["layout","init","main","dispose"].join(", "),n="var "+s+`; var output = {}; -`,r=` -return { ...output, `+s+" };",i=n+this.codeNode.code+r;return this._method=new Function(...e,i),this._method}dispose(){this._method!==null&&(this._object&&typeof this._object.dispose=="function"&&this._object.dispose(),this._method=null,this._object=null,this._source=null,this._value=null,this._needsOutputUpdate=!0,this._output.value=null,this._outputs={})}setup(){return this.getDefaultOutputNode()}getCacheKey(e){const t=[ga(this.source),this.getDefaultOutputNode().getCacheKey(e)];for(const s in this.parameters)t.push(this.parameters[s].getCacheKey(e));return Zn(t)}set needsUpdate(e){e===!0&&this.dispose()}get needsUpdate(){return this.source!==this._source}_exec(){return this.codeNode===null?this:(this._needsOutputUpdate===!0&&(this._value=this.call("main"),this._needsOutputUpdate=!1),this._output.value=this._value,this)}_refresh(){this.needsUpdate=!0,this._exec(),this._output.refresh()}}const mN=C(gN);function If(o){let e;const t=o.context.getViewZ;return t!==void 0&&(e=t(this)),(e||xe.z).negate()}const wu=b(([o,e],t)=>{const s=If(t);return ct(o,e,s)}),Mu=b(([o],e)=>{const t=If(e);return o.mul(o,t,t).negate().exp().oneMinus()}),Xn=b(([o,e])=>P(e.toFloat().mix($n.rgb,o.toVec3()),$n.a));function yN(o,e,t){return console.warn('THREE.TSL: "rangeFog( color, near, far )" is deprecated. Use "fog( color, rangeFogFactor( near, far ) )" instead.'),Xn(o,wu(e,t))}function xN(o,e){return console.warn('THREE.TSL: "densityFog( color, density )" is deprecated. Use "fog( color, densityFogFactor( density ) )" instead.'),Xn(o,Mu(e))}let us=null,cs=null;class TN extends O{static get type(){return"RangeNode"}constructor(e=g(),t=g()){super(),this.minNode=e,this.maxNode=t}getVectorLength(e){const t=e.getTypeLength(Ot(this.minNode.value)),s=e.getTypeLength(Ot(this.maxNode.value));return t>s?t:s}getNodeType(e){return e.object.count>1?e.getTypeFromLength(this.getVectorLength(e)):"float"}setup(e){const t=e.object;let s=null;if(t.count>1){const n=this.minNode.value,r=this.maxNode.value,i=e.getTypeLength(Ot(n)),a=e.getTypeLength(Ot(r));us=us||new Me,cs=cs||new Me,us.setScalar(0),cs.setScalar(0),i===1?us.setScalar(n):n.isColor?us.set(n.r,n.g,n.b,1):us.set(n.x,n.y,n.z||0,n.w||0),a===1?cs.setScalar(r):r.isColor?cs.set(r.r,r.g,r.b,1):cs.set(r.x,r.y,r.z||0,r.w||0);const u=4,c=u*t.count,l=new Float32Array(c);for(let h=0;hE(new bN(o,e)),NN=Ui("numWorkgroups","uvec3"),SN=Ui("workgroupId","uvec3"),vN=Ui("localId","uvec3"),AN=Ui("subgroupSize","uint");class RN extends O{constructor(e){super(),this.scope=e}generate(e){const{scope:t}=this,{renderer:s}=e;s.backend.isWebGLBackend===!0?e.addFlowCode(` // ${t}Barrier -`):e.addLineFlowCode(`${t}Barrier()`,this)}}const Bu=C(RN),CN=()=>Bu("workgroup").append(),EN=()=>Bu("storage").append(),wN=()=>Bu("texture").append();class MN extends Rs{constructor(e,t){super(e,t),this.isWorkgroupInfoElementNode=!0}generate(e,t){let s;const n=e.context.assign;if(s=super.generate(e),n!==!0){const r=this.getNodeType(e);s=e.format(s,r,t)}return s}}class BN extends O{constructor(e,t,s=0){super(t),this.bufferType=t,this.bufferCount=s,this.isWorkgroupInfoNode=!0,this.elementType=t,this.scope=e}label(e){return this.name=e,this}setScope(e){return this.scope=e,this}getElementType(){return this.elementType}getInputType(){return`${this.scope}Array`}element(e){return E(new MN(this,e))}generate(e){return e.getScopedArray(this.name||`${this.scope}Array_${this.id}`,this.scope.toLowerCase(),this.bufferType,this.bufferCount)}}const FN=(o,e)=>E(new BN("Workgroup",o,e));class Be extends be{static get type(){return"AtomicFunctionNode"}constructor(e,t,s,n=null){super("uint"),this.method=e,this.pointerNode=t,this.valueNode=s,this.storeNode=n}getInputType(e){return this.pointerNode.getNodeType(e)}getNodeType(e){return this.getInputType(e)}generate(e){const t=this.method,s=this.getNodeType(e),n=this.getInputType(e),r=this.pointerNode,i=this.valueNode,a=[];a.push(`&${r.build(e,n)}`),a.push(i.build(e,n));const u=`${e.getMethod(t,s)}( ${a.join(", ")} )`;if(this.storeNode!==null){const c=this.storeNode.build(e,n);e.addLineFlowCode(`${c} = ${u}`,this)}else e.addLineFlowCode(u,this)}}Be.ATOMIC_LOAD="atomicLoad";Be.ATOMIC_STORE="atomicStore";Be.ATOMIC_ADD="atomicAdd";Be.ATOMIC_SUB="atomicSub";Be.ATOMIC_MAX="atomicMax";Be.ATOMIC_MIN="atomicMin";Be.ATOMIC_AND="atomicAnd";Be.ATOMIC_OR="atomicOr";Be.ATOMIC_XOR="atomicXor";const UN=C(Be),Yt=(o,e,t,s=null)=>{const n=UN(o,e,t,s);return n.append(),n},PN=(o,e,t=null)=>Yt(Be.ATOMIC_STORE,o,e,t),DN=(o,e,t=null)=>Yt(Be.ATOMIC_ADD,o,e,t),LN=(o,e,t=null)=>Yt(Be.ATOMIC_SUB,o,e,t),IN=(o,e,t=null)=>Yt(Be.ATOMIC_MAX,o,e,t),VN=(o,e,t=null)=>Yt(Be.ATOMIC_MIN,o,e,t),GN=(o,e,t=null)=>Yt(Be.ATOMIC_AND,o,e,t),ON=(o,e,t=null)=>Yt(Be.ATOMIC_OR,o,e,t),kN=(o,e,t=null)=>Yt(Be.ATOMIC_XOR,o,e,t);let Nr;function hr(o){Nr=Nr||new WeakMap;let e=Nr.get(o);return e===void 0&&Nr.set(o,e={}),e}function Fu(o){const e=hr(o);return e.shadowMatrix||(e.shadowMatrix=V("mat4").setGroup(k).onRenderUpdate(()=>(o.castShadow!==!0&&o.shadow.updateMatrices(o),o.shadow.matrix)))}function Vf(o){const e=hr(o);if(e.projectionUV===void 0){const t=Fu(o).mul(Wt);e.projectionUV=t.xyz.div(t.w)}return e.projectionUV}function Uu(o){const e=hr(o);return e.position||(e.position=V(new j).setGroup(k).onRenderUpdate((t,s)=>s.value.setFromMatrixPosition(o.matrixWorld)))}function Gf(o){const e=hr(o);return e.targetPosition||(e.targetPosition=V(new j).setGroup(k).onRenderUpdate((t,s)=>s.value.setFromMatrixPosition(o.target.matrixWorld)))}function Pi(o){const e=hr(o);return e.viewPosition||(e.viewPosition=V(new j).setGroup(k).onRenderUpdate(({camera:t},s)=>{s.value=s.value||new j,s.value.setFromMatrixPosition(o.matrixWorld),s.value.applyMatrix4(t.matrixWorldInverse)}))}const Pu=o=>je.transformDirection(Uu(o).sub(Gf(o))),zN=o=>o.sort((e,t)=>e.id-t.id),WN=(o,e)=>{for(const t of e)if(t.isAnalyticLightNode&&t.light.id===o)return t;return null},Ji=new WeakMap;class Du extends O{static get type(){return"LightsNode"}constructor(){super("vec3"),this.totalDiffuseNode=T().toVar("totalDiffuse"),this.totalSpecularNode=T().toVar("totalSpecular"),this.outgoingLightNode=T().toVar("outgoingLight"),this._lights=[],this._lightNodes=null,this._lightNodesHash=null,this.global=!0}customCacheKey(){const e=[],t=this._lights;for(let s=0;s0}}const $N=(o=[])=>E(new Du).setLights(o);class HN extends O{static get type(){return"ShadowBaseNode"}constructor(e){super(),this.light=e,this.updateBeforeType=W.RENDER,this.isShadowBaseNode=!0}setupShadowPosition({material:e}){Lu.assign(e.shadowPositionNode||Wt)}dispose(){this.updateBeforeType=W.NONE}}const Lu=T().toVar("shadowPositionWorld");function Iu(o,e={}){return e.toneMapping=o.toneMapping,e.toneMappingExposure=o.toneMappingExposure,e.outputColorSpace=o.outputColorSpace,e.renderTarget=o.getRenderTarget(),e.activeCubeFace=o.getActiveCubeFace(),e.activeMipmapLevel=o.getActiveMipmapLevel(),e.renderObjectFunction=o.getRenderObjectFunction(),e.pixelRatio=o.getPixelRatio(),e.mrt=o.getMRT(),e.clearColor=o.getClearColor(e.clearColor||new mt),e.clearAlpha=o.getClearAlpha(),e.autoClear=o.autoClear,e.scissorTest=o.getScissorTest(),e}function Of(o,e){return e=Iu(o,e),o.setMRT(null),o.setRenderObjectFunction(null),o.setClearColor(0,1),o.autoClear=!0,e}function kf(o,e){o.toneMapping=e.toneMapping,o.toneMappingExposure=e.toneMappingExposure,o.outputColorSpace=e.outputColorSpace,o.setRenderTarget(e.renderTarget,e.activeCubeFace,e.activeMipmapLevel),o.setRenderObjectFunction(e.renderObjectFunction),o.setPixelRatio(e.pixelRatio),o.setMRT(e.mrt),o.setClearColor(e.clearColor,e.clearAlpha),o.autoClear=e.autoClear,o.setScissorTest(e.scissorTest)}function Vu(o,e={}){return e.background=o.background,e.backgroundNode=o.backgroundNode,e.overrideMaterial=o.overrideMaterial,e}function zf(o,e){return e=Vu(o,e),o.background=null,o.backgroundNode=null,o.overrideMaterial=null,e}function Wf(o,e){o.background=e.background,o.backgroundNode=e.backgroundNode,o.overrideMaterial=e.overrideMaterial}function qN(o,e,t={}){return t=Iu(o,t),t=Vu(e,t),t}function $f(o,e,t){return t=Of(o,t),t=zf(e,t),t}function Hf(o,e,t){kf(o,t),Wf(e,t)}var gR=Object.freeze({__proto__:null,resetRendererAndSceneState:$f,resetRendererState:Of,resetSceneState:zf,restoreRendererAndSceneState:Hf,restoreRendererState:kf,restoreSceneState:Wf,saveRendererAndSceneState:qN,saveRendererState:Iu,saveSceneState:Vu});const Ic=new WeakMap,KN=b(([o,e,t])=>{let s=Wt.sub(o).length();return s=s.sub(e).div(t.sub(e)),s=s.saturate(),s}),XN=o=>{const e=o.shadow.camera,t=ne("near","float",e).setGroup(k),s=ne("far","float",e).setGroup(k),n=Dh(o);return KN(n,t,s)},YN=o=>{let e=Ic.get(o);if(e===void 0){const t=o.isPointLight?XN(o):null;e=new ce,e.colorNode=P(0,0,0,1),e.depthNode=t,e.isShadowNodeMaterial=!0,e.name="ShadowMaterial",e.fog=!1,Ic.set(o,e)}return e},qf=b(({depthTexture:o,shadowCoord:e})=>K(o,e.xy).compare(e.z)),Kf=b(({depthTexture:o,shadowCoord:e,shadow:t})=>{const s=(m,x)=>K(o,m).compare(x),n=ne("mapSize","vec2",t).setGroup(k),r=ne("radius","float",t).setGroup(k),i=M(1).div(n),a=i.x.negate().mul(r),u=i.y.negate().mul(r),c=i.x.mul(r),l=i.y.mul(r),d=a.div(2),h=u.div(2),p=c.div(2),f=l.div(2);return _e(s(e.xy.add(M(a,u)),e.z),s(e.xy.add(M(0,u)),e.z),s(e.xy.add(M(c,u)),e.z),s(e.xy.add(M(d,h)),e.z),s(e.xy.add(M(0,h)),e.z),s(e.xy.add(M(p,h)),e.z),s(e.xy.add(M(a,0)),e.z),s(e.xy.add(M(d,0)),e.z),s(e.xy,e.z),s(e.xy.add(M(p,0)),e.z),s(e.xy.add(M(c,0)),e.z),s(e.xy.add(M(d,f)),e.z),s(e.xy.add(M(0,f)),e.z),s(e.xy.add(M(p,f)),e.z),s(e.xy.add(M(a,l)),e.z),s(e.xy.add(M(0,l)),e.z),s(e.xy.add(M(c,l)),e.z)).mul(1/17)}),Xf=b(({depthTexture:o,shadowCoord:e,shadow:t})=>{const s=(l,d)=>K(o,l).compare(d),n=ne("mapSize","vec2",t).setGroup(k),r=M(1).div(n),i=r.x,a=r.y,u=e.xy,c=Xt(u.mul(n).add(.5));return u.subAssign(c.mul(r)),_e(s(u,e.z),s(u.add(M(i,0)),e.z),s(u.add(M(0,a)),e.z),s(u.add(r),e.z),Q(s(u.add(M(i.negate(),0)),e.z),s(u.add(M(i.mul(2),0)),e.z),c.x),Q(s(u.add(M(i.negate(),a)),e.z),s(u.add(M(i.mul(2),a)),e.z),c.x),Q(s(u.add(M(0,a.negate())),e.z),s(u.add(M(0,a.mul(2))),e.z),c.y),Q(s(u.add(M(i,a.negate())),e.z),s(u.add(M(i,a.mul(2))),e.z),c.y),Q(Q(s(u.add(M(i.negate(),a.negate())),e.z),s(u.add(M(i.mul(2),a.negate())),e.z),c.x),Q(s(u.add(M(i.negate(),a.mul(2))),e.z),s(u.add(M(i.mul(2),a.mul(2))),e.z),c.x),c.y)).mul(1/9)}),Yf=b(({depthTexture:o,shadowCoord:e})=>{const t=g(1).toVar(),s=K(o).sample(e.xy).rg,n=Ti(e.z,s.x);return z(n.notEqual(g(1)),()=>{const r=e.z.sub(s.x),i=ue(0,s.y.mul(s.y));let a=i.div(i.add(r.mul(r)));a=Et(Y(a,.3).div(.95-.3)),t.assign(Et(ue(n,a)))}),t}),jN=b(({samples:o,radius:e,size:t,shadowPass:s})=>{const n=g(0).toVar(),r=g(0).toVar(),i=o.lessThanEqual(g(1)).select(g(0),g(2).div(o.sub(1))),a=o.lessThanEqual(g(1)).select(g(0),g(-1));te({start:y(0),end:y(o),type:"int",condition:"<"},({i:c})=>{const l=a.add(g(c).mul(i)),d=s.sample(_e(dr.xy,M(0,l).mul(e)).div(t)).x;n.addAssign(d),r.addAssign(d.mul(d))}),n.divAssign(o),r.divAssign(o);const u=Pt(r.sub(n.mul(n)));return M(n,u)}),QN=b(({samples:o,radius:e,size:t,shadowPass:s})=>{const n=g(0).toVar(),r=g(0).toVar(),i=o.lessThanEqual(g(1)).select(g(0),g(2).div(o.sub(1))),a=o.lessThanEqual(g(1)).select(g(0),g(-1));te({start:y(0),end:y(o),type:"int",condition:"<"},({i:c})=>{const l=a.add(g(c).mul(i)),d=s.sample(_e(dr.xy,M(l,0).mul(e)).div(t));n.addAssign(d.x),r.addAssign(_e(d.y.mul(d.y),d.x.mul(d.x)))}),n.divAssign(o),r.divAssign(o);const u=Pt(r.sub(n.mul(n)));return M(n,u)}),ZN=[qf,Kf,Xf,Yf];let eo;const Sr=new Mi;class jf extends HN{static get type(){return"ShadowNode"}constructor(e,t=null){super(e),this.shadow=t||e.shadow,this.shadowMap=null,this.vsmShadowMapVertical=null,this.vsmShadowMapHorizontal=null,this.vsmMaterialVertical=null,this.vsmMaterialHorizontal=null,this._node=null,this.isShadowNode=!0}setupShadowFilter(e,{filterFn:t,depthTexture:s,shadowCoord:n,shadow:r}){const i=n.x.greaterThanEqual(0).and(n.x.lessThanEqual(1)).and(n.y.greaterThanEqual(0)).and(n.y.lessThanEqual(1)).and(n.z.lessThanEqual(1)),a=t({depthTexture:s,shadowCoord:n,shadow:r});return i.select(a,g(1))}setupShadowCoord(e,t){const{shadow:s}=this,{renderer:n}=e,r=ne("bias","float",s).setGroup(k);let i=t,a;if(s.camera.isOrthographicCamera||n.logarithmicDepthBuffer!==!0)i=i.xyz.div(i.w),a=i.z,n.coordinateSystem===ln&&(a=a.mul(2).sub(1));else{const u=i.w;i=i.xy.div(u);const c=ne("near","float",s.camera).setGroup(k),l=ne("far","float",s.camera).setGroup(k);a=yu(u.negate(),c,l)}return i=T(i.x,i.y.oneMinus(),a.add(r)),i}getShadowFilterFn(e){return ZN[e]}setupShadow(e){const{renderer:t}=e,{light:s,shadow:n}=this,r=t.shadowMap.type,i=new vs(n.mapSize.width,n.mapSize.height);i.compareFunction=ua;const a=e.createRenderTarget(n.mapSize.width,n.mapSize.height);if(a.depthTexture=i,n.camera.updateProjectionMatrix(),r===fr){i.compareFunction=null,this.vsmShadowMapVertical=e.createRenderTarget(n.mapSize.width,n.mapSize.height,{format:Ln,type:pt}),this.vsmShadowMapHorizontal=e.createRenderTarget(n.mapSize.width,n.mapSize.height,{format:Ln,type:pt});const N=K(i),v=K(this.vsmShadowMapVertical.texture),w=ne("blurSamples","float",n).setGroup(k),B=ne("radius","float",n).setGroup(k),F=ne("mapSize","vec2",n).setGroup(k);let L=this.vsmMaterialVertical||(this.vsmMaterialVertical=new ce);L.fragmentNode=jN({samples:w,radius:B,size:F,shadowPass:N}).context(e.getSharedContext()),L.name="VSMVertical",L=this.vsmMaterialHorizontal||(this.vsmMaterialHorizontal=new ce),L.fragmentNode=QN({samples:w,radius:B,size:F,shadowPass:v}).context(e.getSharedContext()),L.name="VSMHorizontal"}const u=ne("intensity","float",n).setGroup(k),c=ne("normalBias","float",n).setGroup(k),l=Fu(s).mul(Lu.add(vi.mul(c))),d=this.setupShadowCoord(e,l),h=n.filterNode||this.getShadowFilterFn(t.shadowMap.type)||null;if(h===null)throw new Error("THREE.WebGPURenderer: Shadow map type not supported yet.");const p=r===fr?this.vsmShadowMapHorizontal.texture:i,f=this.setupShadowFilter(e,{filterFn:h,shadowTexture:a.texture,depthTexture:p,shadowCoord:d,shadow:n}),m=K(a.texture,d),x=Q(1,f.rgb.mix(m,1),u.mul(m.a)).toVar();return this.shadowMap=a,this.shadow.map=a,x}setup(e){if(e.renderer.shadowMap.enabled!==!1)return b(()=>{let t=this._node;return this.setupShadowPosition(e),t===null&&(this._node=t=this.setupShadow(e)),e.material.shadowNode&&console.warn('THREE.NodeMaterial: ".shadowNode" is deprecated. Use ".castShadowNode" instead.'),e.material.receivedShadowNode&&(t=e.material.receivedShadowNode(t)),t})()}renderShadow(e){const{shadow:t,shadowMap:s,light:n}=this,{renderer:r,scene:i}=e;t.updateMatrices(n),s.setSize(t.mapSize.width,t.mapSize.height),r.render(i,t.camera)}updateShadow(e){const{shadowMap:t,light:s,shadow:n}=this,{renderer:r,scene:i,camera:a}=e,u=r.shadowMap.type,c=t.depthTexture.version;this._depthVersionCached=c,n.camera.layers.mask=a.layers.mask;const l=r.getRenderObjectFunction(),d=r.getMRT(),h=d?d.has("velocity"):!1;eo=$f(r,i,eo),i.overrideMaterial=YN(s),r.setRenderObjectFunction((p,f,m,x,N,v,...w)=>{(p.castShadow===!0||p.receiveShadow&&u===fr)&&(h&&(ba(p).useVelocity=!0),p.onBeforeShadow(r,p,a,n.camera,x,f.overrideMaterial,v),r.renderObject(p,f,m,x,N,v,...w),p.onAfterShadow(r,p,a,n.camera,x,f.overrideMaterial,v))}),r.setRenderTarget(t),this.renderShadow(e),r.setRenderObjectFunction(l),s.isPointLight!==!0&&u===fr&&this.vsmPass(r),Hf(r,i,eo)}vsmPass(e){const{shadow:t}=this;this.vsmShadowMapVertical.setSize(t.mapSize.width,t.mapSize.height),this.vsmShadowMapHorizontal.setSize(t.mapSize.width,t.mapSize.height),e.setRenderTarget(this.vsmShadowMapVertical),Sr.material=this.vsmMaterialVertical,Sr.render(e),e.setRenderTarget(this.vsmShadowMapHorizontal),Sr.material=this.vsmMaterialHorizontal,Sr.render(e)}dispose(){this.shadowMap.dispose(),this.shadowMap=null,this.vsmShadowMapVertical!==null&&(this.vsmShadowMapVertical.dispose(),this.vsmShadowMapVertical=null,this.vsmMaterialVertical.dispose(),this.vsmMaterialVertical=null),this.vsmShadowMapHorizontal!==null&&(this.vsmShadowMapHorizontal.dispose(),this.vsmShadowMapHorizontal=null,this.vsmMaterialHorizontal.dispose(),this.vsmMaterialHorizontal=null),super.dispose()}updateBefore(e){const{shadow:t}=this;(t.needsUpdate||t.autoUpdate)&&(this.updateShadow(e),this.shadowMap.depthTexture.version===this._depthVersionCached&&(t.needsUpdate=!1))}}const Qf=(o,e)=>E(new jf(o,e));class Es extends hn{static get type(){return"AnalyticLightNode"}constructor(e=null){super(),this.light=e,this.color=new mt,this.colorNode=e&&e.colorNode||V(this.color).setGroup(k),this.baseColorNode=null,this.shadowNode=null,this.shadowColorNode=null,this.isAnalyticLightNode=!0,this.updateType=W.FRAME}customCacheKey(){return di(this.light.id,this.light.castShadow?1:0)}getHash(){return this.light.uuid}setupShadowNode(){return Qf(this.light)}setupShadow(e){const{renderer:t}=e;if(t.shadowMap.enabled===!1)return;let s=this.shadowColorNode;if(s===null){const n=this.light.shadow.shadowNode;let r;n!==void 0?r=E(n):r=this.setupShadowNode(e),this.shadowNode=r,this.shadowColorNode=s=this.colorNode.mul(r),this.baseColorNode=this.colorNode}this.colorNode=s}setup(e){this.colorNode=this.baseColorNode||this.colorNode,this.light.castShadow?e.object.receiveShadow&&this.setupShadow(e):this.shadowNode!==null&&(this.shadowNode.dispose(),this.shadowNode=null,this.shadowColorNode=null)}update(){const{light:e}=this;this.color.copy(e.color).multiplyScalar(e.intensity)}}const Gu=b(o=>{const{lightDistance:e,cutoffDistance:t,decayExponent:s}=o,n=e.pow(s).max(.01).reciprocal();return t.greaterThan(0).select(n.mul(e.div(t).pow4().oneMinus().clamp().pow2()),n)}),JN=new mt,Tt=b(([o,e])=>{const t=o.toVar(),s=oe(t),n=gt(1,ue(s.x,ue(s.y,s.z)));s.mulAssign(n),t.mulAssign(n.mul(e.mul(2).oneMinus()));const r=M(t.xy).toVar(),a=e.mul(1.5).oneMinus();return z(s.z.greaterThanEqual(a),()=>{z(t.z.greaterThan(0),()=>{r.x.assign(Y(4,t.x))})}).ElseIf(s.x.greaterThanEqual(a),()=>{const u=qn(t.x);r.x.assign(t.z.mul(u).add(u.mul(2)))}).ElseIf(s.y.greaterThanEqual(a),()=>{const u=qn(t.y);r.x.assign(t.x.add(u.mul(2)).add(2)),r.y.assign(t.z.mul(u).sub(2))}),M(.125,.25).mul(r).add(M(.375,.75)).flipY()}).setLayout({name:"cubeToUV",type:"vec2",inputs:[{name:"pos",type:"vec3"},{name:"texelSizeY",type:"float"}]}),eS=b(({depthTexture:o,bd3D:e,dp:t,texelSize:s})=>K(o,Tt(e,s.y)).compare(t)),tS=b(({depthTexture:o,bd3D:e,dp:t,texelSize:s,shadow:n})=>{const r=ne("radius","float",n).setGroup(k),i=M(-1,1).mul(r).mul(s.y);return K(o,Tt(e.add(i.xyy),s.y)).compare(t).add(K(o,Tt(e.add(i.yyy),s.y)).compare(t)).add(K(o,Tt(e.add(i.xyx),s.y)).compare(t)).add(K(o,Tt(e.add(i.yyx),s.y)).compare(t)).add(K(o,Tt(e,s.y)).compare(t)).add(K(o,Tt(e.add(i.xxy),s.y)).compare(t)).add(K(o,Tt(e.add(i.yxy),s.y)).compare(t)).add(K(o,Tt(e.add(i.xxx),s.y)).compare(t)).add(K(o,Tt(e.add(i.yxx),s.y)).compare(t)).mul(1/9)}),sS=b(({filterFn:o,depthTexture:e,shadowCoord:t,shadow:s})=>{const n=t.xyz.toVar(),r=n.length(),i=V("float").setGroup(k).onRenderUpdate(()=>s.camera.near),a=V("float").setGroup(k).onRenderUpdate(()=>s.camera.far),u=ne("bias","float",s).setGroup(k),c=V(s.mapSize).setGroup(k),l=g(1).toVar();return z(r.sub(a).lessThanEqual(0).and(r.sub(i).greaterThanEqual(0)),()=>{const d=r.sub(i).div(a.sub(i)).toVar();d.addAssign(u);const h=n.normalize(),p=M(1).div(c.mul(M(4,2)));l.assign(o({depthTexture:e,bd3D:h,dp:d,texelSize:p,shadow:s}))}),l}),Vc=new Me,Ps=new Ye,bn=new Ye;class nS extends jf{static get type(){return"PointShadowNode"}constructor(e,t=null){super(e,t)}getShadowFilterFn(e){return e===gm?eS:tS}setupShadowCoord(e,t){return t}setupShadowFilter(e,{filterFn:t,shadowTexture:s,depthTexture:n,shadowCoord:r,shadow:i}){return sS({filterFn:t,shadowTexture:s,depthTexture:n,shadowCoord:r,shadow:i})}renderShadow(e){const{shadow:t,shadowMap:s,light:n}=this,{renderer:r,scene:i}=e,a=t.getFrameExtents();bn.copy(t.mapSize),bn.multiply(a),s.setSize(bn.width,bn.height),Ps.copy(t.mapSize);const u=r.autoClear,c=r.getClearColor(JN),l=r.getClearAlpha();r.autoClear=!1,r.setClearColor(t.clearColor,t.clearAlpha),r.clear();const d=t.getViewportCount();for(let h=0;hE(new nS(o,e)),Zf=b(({color:o,lightViewPosition:e,cutoffDistance:t,decayExponent:s},n)=>{const r=n.context.lightingModel,i=e.sub(xe),a=i.normalize(),u=i.length(),c=Gu({lightDistance:u,cutoffDistance:t,decayExponent:s}),l=o.mul(c),d=n.context.reflectedLight;r.direct({lightDirection:a,lightColor:l,reflectedLight:d},n.stack,n)});class iS extends Es{static get type(){return"PointLightNode"}constructor(e=null){super(e),this.cutoffDistanceNode=V(0).setGroup(k),this.decayExponentNode=V(2).setGroup(k)}update(e){const{light:t}=this;super.update(e),this.cutoffDistanceNode.value=t.distance,this.decayExponentNode.value=t.decay}setupShadowNode(){return rS(this.light)}setup(e){super.setup(e),Zf({color:this.colorNode,lightViewPosition:Pi(this.light),cutoffDistance:this.cutoffDistanceNode,decayExponent:this.decayExponentNode}).append()}}const oS=b(([o=le()])=>{const e=o.mul(2),t=e.x.floor(),s=e.y.floor();return t.add(s).mod(2).sign()}),Un=b(([o,e,t])=>{const s=g(t).toVar(),n=g(e).toVar(),r=Ht(o).toVar();return Ue(r,n,s)}).setLayout({name:"mx_select",type:"float",inputs:[{name:"b",type:"bool"},{name:"t",type:"float"},{name:"f",type:"float"}]}),ni=b(([o,e])=>{const t=Ht(e).toVar(),s=g(o).toVar();return Ue(t,s.negate(),s)}).setLayout({name:"mx_negate_if",type:"float",inputs:[{name:"val",type:"float"},{name:"b",type:"bool"}]}),Te=b(([o])=>{const e=g(o).toVar();return y(At(e))}).setLayout({name:"mx_floor",type:"int",inputs:[{name:"x",type:"float"}]}),he=b(([o,e])=>{const t=g(o).toVar();return e.assign(Te(t)),t.sub(g(e))}),aS=b(([o,e,t,s,n,r])=>{const i=g(r).toVar(),a=g(n).toVar(),u=g(s).toVar(),c=g(t).toVar(),l=g(e).toVar(),d=g(o).toVar(),h=g(Y(1,a)).toVar();return Y(1,i).mul(d.mul(h).add(l.mul(a))).add(i.mul(c.mul(h).add(u.mul(a))))}).setLayout({name:"mx_bilerp_0",type:"float",inputs:[{name:"v0",type:"float"},{name:"v1",type:"float"},{name:"v2",type:"float"},{name:"v3",type:"float"},{name:"s",type:"float"},{name:"t",type:"float"}]}),uS=b(([o,e,t,s,n,r])=>{const i=g(r).toVar(),a=g(n).toVar(),u=T(s).toVar(),c=T(t).toVar(),l=T(e).toVar(),d=T(o).toVar(),h=g(Y(1,a)).toVar();return Y(1,i).mul(d.mul(h).add(l.mul(a))).add(i.mul(c.mul(h).add(u.mul(a))))}).setLayout({name:"mx_bilerp_1",type:"vec3",inputs:[{name:"v0",type:"vec3"},{name:"v1",type:"vec3"},{name:"v2",type:"vec3"},{name:"v3",type:"vec3"},{name:"s",type:"float"},{name:"t",type:"float"}]}),Jf=Pe([aS,uS]),cS=b(([o,e,t,s,n,r,i,a,u,c,l])=>{const d=g(l).toVar(),h=g(c).toVar(),p=g(u).toVar(),f=g(a).toVar(),m=g(i).toVar(),x=g(r).toVar(),N=g(n).toVar(),v=g(s).toVar(),w=g(t).toVar(),B=g(e).toVar(),F=g(o).toVar(),L=g(Y(1,p)).toVar(),I=g(Y(1,h)).toVar();return g(Y(1,d)).toVar().mul(I.mul(F.mul(L).add(B.mul(p))).add(h.mul(w.mul(L).add(v.mul(p))))).add(d.mul(I.mul(N.mul(L).add(x.mul(p))).add(h.mul(m.mul(L).add(f.mul(p))))))}).setLayout({name:"mx_trilerp_0",type:"float",inputs:[{name:"v0",type:"float"},{name:"v1",type:"float"},{name:"v2",type:"float"},{name:"v3",type:"float"},{name:"v4",type:"float"},{name:"v5",type:"float"},{name:"v6",type:"float"},{name:"v7",type:"float"},{name:"s",type:"float"},{name:"t",type:"float"},{name:"r",type:"float"}]}),lS=b(([o,e,t,s,n,r,i,a,u,c,l])=>{const d=g(l).toVar(),h=g(c).toVar(),p=g(u).toVar(),f=T(a).toVar(),m=T(i).toVar(),x=T(r).toVar(),N=T(n).toVar(),v=T(s).toVar(),w=T(t).toVar(),B=T(e).toVar(),F=T(o).toVar(),L=g(Y(1,p)).toVar(),I=g(Y(1,h)).toVar();return g(Y(1,d)).toVar().mul(I.mul(F.mul(L).add(B.mul(p))).add(h.mul(w.mul(L).add(v.mul(p))))).add(d.mul(I.mul(N.mul(L).add(x.mul(p))).add(h.mul(m.mul(L).add(f.mul(p))))))}).setLayout({name:"mx_trilerp_1",type:"vec3",inputs:[{name:"v0",type:"vec3"},{name:"v1",type:"vec3"},{name:"v2",type:"vec3"},{name:"v3",type:"vec3"},{name:"v4",type:"vec3"},{name:"v5",type:"vec3"},{name:"v6",type:"vec3"},{name:"v7",type:"vec3"},{name:"s",type:"float"},{name:"t",type:"float"},{name:"r",type:"float"}]}),eg=Pe([cS,lS]),dS=b(([o,e,t])=>{const s=g(t).toVar(),n=g(e).toVar(),r=D(o).toVar(),i=D(r.bitAnd(D(7))).toVar(),a=g(Un(i.lessThan(D(4)),n,s)).toVar(),u=g($(2,Un(i.lessThan(D(4)),s,n))).toVar();return ni(a,Ht(i.bitAnd(D(1)))).add(ni(u,Ht(i.bitAnd(D(2)))))}).setLayout({name:"mx_gradient_float_0",type:"float",inputs:[{name:"hash",type:"uint"},{name:"x",type:"float"},{name:"y",type:"float"}]}),hS=b(([o,e,t,s])=>{const n=g(s).toVar(),r=g(t).toVar(),i=g(e).toVar(),a=D(o).toVar(),u=D(a.bitAnd(D(15))).toVar(),c=g(Un(u.lessThan(D(8)),i,r)).toVar(),l=g(Un(u.lessThan(D(4)),r,Un(u.equal(D(12)).or(u.equal(D(14))),i,n))).toVar();return ni(c,Ht(u.bitAnd(D(1)))).add(ni(l,Ht(u.bitAnd(D(2)))))}).setLayout({name:"mx_gradient_float_1",type:"float",inputs:[{name:"hash",type:"uint"},{name:"x",type:"float"},{name:"y",type:"float"},{name:"z",type:"float"}]}),Ee=Pe([dS,hS]),pS=b(([o,e,t])=>{const s=g(t).toVar(),n=g(e).toVar(),r=dn(o).toVar();return T(Ee(r.x,n,s),Ee(r.y,n,s),Ee(r.z,n,s))}).setLayout({name:"mx_gradient_vec3_0",type:"vec3",inputs:[{name:"hash",type:"uvec3"},{name:"x",type:"float"},{name:"y",type:"float"}]}),fS=b(([o,e,t,s])=>{const n=g(s).toVar(),r=g(t).toVar(),i=g(e).toVar(),a=dn(o).toVar();return T(Ee(a.x,i,r,n),Ee(a.y,i,r,n),Ee(a.z,i,r,n))}).setLayout({name:"mx_gradient_vec3_1",type:"vec3",inputs:[{name:"hash",type:"uvec3"},{name:"x",type:"float"},{name:"y",type:"float"},{name:"z",type:"float"}]}),nt=Pe([pS,fS]),gS=b(([o])=>{const e=g(o).toVar();return $(.6616,e)}).setLayout({name:"mx_gradient_scale2d_0",type:"float",inputs:[{name:"v",type:"float"}]}),mS=b(([o])=>{const e=g(o).toVar();return $(.982,e)}).setLayout({name:"mx_gradient_scale3d_0",type:"float",inputs:[{name:"v",type:"float"}]}),yS=b(([o])=>{const e=T(o).toVar();return $(.6616,e)}).setLayout({name:"mx_gradient_scale2d_1",type:"vec3",inputs:[{name:"v",type:"vec3"}]}),tg=Pe([gS,yS]),xS=b(([o])=>{const e=T(o).toVar();return $(.982,e)}).setLayout({name:"mx_gradient_scale3d_1",type:"vec3",inputs:[{name:"v",type:"vec3"}]}),sg=Pe([mS,xS]),tt=b(([o,e])=>{const t=y(e).toVar(),s=D(o).toVar();return s.shiftLeft(t).bitOr(s.shiftRight(y(32).sub(t)))}).setLayout({name:"mx_rotl32",type:"uint",inputs:[{name:"x",type:"uint"},{name:"k",type:"int"}]}),ng=b(([o,e,t])=>{o.subAssign(t),o.bitXorAssign(tt(t,y(4))),t.addAssign(e),e.subAssign(o),e.bitXorAssign(tt(o,y(6))),o.addAssign(t),t.subAssign(e),t.bitXorAssign(tt(e,y(8))),e.addAssign(o),o.subAssign(t),o.bitXorAssign(tt(t,y(16))),t.addAssign(e),e.subAssign(o),e.bitXorAssign(tt(o,y(19))),o.addAssign(t),t.subAssign(e),t.bitXorAssign(tt(e,y(4))),e.addAssign(o)}),pr=b(([o,e,t])=>{const s=D(t).toVar(),n=D(e).toVar(),r=D(o).toVar();return s.bitXorAssign(n),s.subAssign(tt(n,y(14))),r.bitXorAssign(s),r.subAssign(tt(s,y(11))),n.bitXorAssign(r),n.subAssign(tt(r,y(25))),s.bitXorAssign(n),s.subAssign(tt(n,y(16))),r.bitXorAssign(s),r.subAssign(tt(s,y(4))),n.bitXorAssign(r),n.subAssign(tt(r,y(14))),s.bitXorAssign(n),s.subAssign(tt(n,y(24))),s}).setLayout({name:"mx_bjfinal",type:"uint",inputs:[{name:"a",type:"uint"},{name:"b",type:"uint"},{name:"c",type:"uint"}]}),Ge=b(([o])=>{const e=D(o).toVar();return g(e).div(g(D(y(4294967295))))}).setLayout({name:"mx_bits_to_01",type:"float",inputs:[{name:"bits",type:"uint"}]}),Rt=b(([o])=>{const e=g(o).toVar();return e.mul(e).mul(e).mul(e.mul(e.mul(6).sub(15)).add(10))}).setLayout({name:"mx_fade",type:"float",inputs:[{name:"t",type:"float"}]}),TS=b(([o])=>{const e=y(o).toVar(),t=D(D(1)).toVar(),s=D(D(y(3735928559)).add(t.shiftLeft(D(2))).add(D(13))).toVar();return pr(s.add(D(e)),s,s)}).setLayout({name:"mx_hash_int_0",type:"uint",inputs:[{name:"x",type:"int"}]}),_S=b(([o,e])=>{const t=y(e).toVar(),s=y(o).toVar(),n=D(D(2)).toVar(),r=D().toVar(),i=D().toVar(),a=D().toVar();return r.assign(i.assign(a.assign(D(y(3735928559)).add(n.shiftLeft(D(2))).add(D(13))))),r.addAssign(D(s)),i.addAssign(D(t)),pr(r,i,a)}).setLayout({name:"mx_hash_int_1",type:"uint",inputs:[{name:"x",type:"int"},{name:"y",type:"int"}]}),bS=b(([o,e,t])=>{const s=y(t).toVar(),n=y(e).toVar(),r=y(o).toVar(),i=D(D(3)).toVar(),a=D().toVar(),u=D().toVar(),c=D().toVar();return a.assign(u.assign(c.assign(D(y(3735928559)).add(i.shiftLeft(D(2))).add(D(13))))),a.addAssign(D(r)),u.addAssign(D(n)),c.addAssign(D(s)),pr(a,u,c)}).setLayout({name:"mx_hash_int_2",type:"uint",inputs:[{name:"x",type:"int"},{name:"y",type:"int"},{name:"z",type:"int"}]}),NS=b(([o,e,t,s])=>{const n=y(s).toVar(),r=y(t).toVar(),i=y(e).toVar(),a=y(o).toVar(),u=D(D(4)).toVar(),c=D().toVar(),l=D().toVar(),d=D().toVar();return c.assign(l.assign(d.assign(D(y(3735928559)).add(u.shiftLeft(D(2))).add(D(13))))),c.addAssign(D(a)),l.addAssign(D(i)),d.addAssign(D(r)),ng(c,l,d),c.addAssign(D(n)),pr(c,l,d)}).setLayout({name:"mx_hash_int_3",type:"uint",inputs:[{name:"x",type:"int"},{name:"y",type:"int"},{name:"z",type:"int"},{name:"xx",type:"int"}]}),SS=b(([o,e,t,s,n])=>{const r=y(n).toVar(),i=y(s).toVar(),a=y(t).toVar(),u=y(e).toVar(),c=y(o).toVar(),l=D(D(5)).toVar(),d=D().toVar(),h=D().toVar(),p=D().toVar();return d.assign(h.assign(p.assign(D(y(3735928559)).add(l.shiftLeft(D(2))).add(D(13))))),d.addAssign(D(c)),h.addAssign(D(u)),p.addAssign(D(a)),ng(d,h,p),d.addAssign(D(i)),h.addAssign(D(r)),pr(d,h,p)}).setLayout({name:"mx_hash_int_4",type:"uint",inputs:[{name:"x",type:"int"},{name:"y",type:"int"},{name:"z",type:"int"},{name:"xx",type:"int"},{name:"yy",type:"int"}]}),re=Pe([TS,_S,bS,NS,SS]),vS=b(([o,e])=>{const t=y(e).toVar(),s=y(o).toVar(),n=D(re(s,t)).toVar(),r=dn().toVar();return r.x.assign(n.bitAnd(y(255))),r.y.assign(n.shiftRight(y(8)).bitAnd(y(255))),r.z.assign(n.shiftRight(y(16)).bitAnd(y(255))),r}).setLayout({name:"mx_hash_vec3_0",type:"uvec3",inputs:[{name:"x",type:"int"},{name:"y",type:"int"}]}),AS=b(([o,e,t])=>{const s=y(t).toVar(),n=y(e).toVar(),r=y(o).toVar(),i=D(re(r,n,s)).toVar(),a=dn().toVar();return a.x.assign(i.bitAnd(y(255))),a.y.assign(i.shiftRight(y(8)).bitAnd(y(255))),a.z.assign(i.shiftRight(y(16)).bitAnd(y(255))),a}).setLayout({name:"mx_hash_vec3_1",type:"uvec3",inputs:[{name:"x",type:"int"},{name:"y",type:"int"},{name:"z",type:"int"}]}),rt=Pe([vS,AS]),RS=b(([o])=>{const e=M(o).toVar(),t=y().toVar(),s=y().toVar(),n=g(he(e.x,t)).toVar(),r=g(he(e.y,s)).toVar(),i=g(Rt(n)).toVar(),a=g(Rt(r)).toVar(),u=g(Jf(Ee(re(t,s),n,r),Ee(re(t.add(y(1)),s),n.sub(1),r),Ee(re(t,s.add(y(1))),n,r.sub(1)),Ee(re(t.add(y(1)),s.add(y(1))),n.sub(1),r.sub(1)),i,a)).toVar();return tg(u)}).setLayout({name:"mx_perlin_noise_float_0",type:"float",inputs:[{name:"p",type:"vec2"}]}),CS=b(([o])=>{const e=T(o).toVar(),t=y().toVar(),s=y().toVar(),n=y().toVar(),r=g(he(e.x,t)).toVar(),i=g(he(e.y,s)).toVar(),a=g(he(e.z,n)).toVar(),u=g(Rt(r)).toVar(),c=g(Rt(i)).toVar(),l=g(Rt(a)).toVar(),d=g(eg(Ee(re(t,s,n),r,i,a),Ee(re(t.add(y(1)),s,n),r.sub(1),i,a),Ee(re(t,s.add(y(1)),n),r,i.sub(1),a),Ee(re(t.add(y(1)),s.add(y(1)),n),r.sub(1),i.sub(1),a),Ee(re(t,s,n.add(y(1))),r,i,a.sub(1)),Ee(re(t.add(y(1)),s,n.add(y(1))),r.sub(1),i,a.sub(1)),Ee(re(t,s.add(y(1)),n.add(y(1))),r,i.sub(1),a.sub(1)),Ee(re(t.add(y(1)),s.add(y(1)),n.add(y(1))),r.sub(1),i.sub(1),a.sub(1)),u,c,l)).toVar();return sg(d)}).setLayout({name:"mx_perlin_noise_float_1",type:"float",inputs:[{name:"p",type:"vec3"}]}),Ou=Pe([RS,CS]),ES=b(([o])=>{const e=M(o).toVar(),t=y().toVar(),s=y().toVar(),n=g(he(e.x,t)).toVar(),r=g(he(e.y,s)).toVar(),i=g(Rt(n)).toVar(),a=g(Rt(r)).toVar(),u=T(Jf(nt(rt(t,s),n,r),nt(rt(t.add(y(1)),s),n.sub(1),r),nt(rt(t,s.add(y(1))),n,r.sub(1)),nt(rt(t.add(y(1)),s.add(y(1))),n.sub(1),r.sub(1)),i,a)).toVar();return tg(u)}).setLayout({name:"mx_perlin_noise_vec3_0",type:"vec3",inputs:[{name:"p",type:"vec2"}]}),wS=b(([o])=>{const e=T(o).toVar(),t=y().toVar(),s=y().toVar(),n=y().toVar(),r=g(he(e.x,t)).toVar(),i=g(he(e.y,s)).toVar(),a=g(he(e.z,n)).toVar(),u=g(Rt(r)).toVar(),c=g(Rt(i)).toVar(),l=g(Rt(a)).toVar(),d=T(eg(nt(rt(t,s,n),r,i,a),nt(rt(t.add(y(1)),s,n),r.sub(1),i,a),nt(rt(t,s.add(y(1)),n),r,i.sub(1),a),nt(rt(t.add(y(1)),s.add(y(1)),n),r.sub(1),i.sub(1),a),nt(rt(t,s,n.add(y(1))),r,i,a.sub(1)),nt(rt(t.add(y(1)),s,n.add(y(1))),r.sub(1),i,a.sub(1)),nt(rt(t,s.add(y(1)),n.add(y(1))),r,i.sub(1),a.sub(1)),nt(rt(t.add(y(1)),s.add(y(1)),n.add(y(1))),r.sub(1),i.sub(1),a.sub(1)),u,c,l)).toVar();return sg(d)}).setLayout({name:"mx_perlin_noise_vec3_1",type:"vec3",inputs:[{name:"p",type:"vec3"}]}),ku=Pe([ES,wS]),MS=b(([o])=>{const e=g(o).toVar(),t=y(Te(e)).toVar();return Ge(re(t))}).setLayout({name:"mx_cell_noise_float_0",type:"float",inputs:[{name:"p",type:"float"}]}),BS=b(([o])=>{const e=M(o).toVar(),t=y(Te(e.x)).toVar(),s=y(Te(e.y)).toVar();return Ge(re(t,s))}).setLayout({name:"mx_cell_noise_float_1",type:"float",inputs:[{name:"p",type:"vec2"}]}),FS=b(([o])=>{const e=T(o).toVar(),t=y(Te(e.x)).toVar(),s=y(Te(e.y)).toVar(),n=y(Te(e.z)).toVar();return Ge(re(t,s,n))}).setLayout({name:"mx_cell_noise_float_2",type:"float",inputs:[{name:"p",type:"vec3"}]}),US=b(([o])=>{const e=P(o).toVar(),t=y(Te(e.x)).toVar(),s=y(Te(e.y)).toVar(),n=y(Te(e.z)).toVar(),r=y(Te(e.w)).toVar();return Ge(re(t,s,n,r))}).setLayout({name:"mx_cell_noise_float_3",type:"float",inputs:[{name:"p",type:"vec4"}]}),PS=Pe([MS,BS,FS,US]),DS=b(([o])=>{const e=g(o).toVar(),t=y(Te(e)).toVar();return T(Ge(re(t,y(0))),Ge(re(t,y(1))),Ge(re(t,y(2))))}).setLayout({name:"mx_cell_noise_vec3_0",type:"vec3",inputs:[{name:"p",type:"float"}]}),LS=b(([o])=>{const e=M(o).toVar(),t=y(Te(e.x)).toVar(),s=y(Te(e.y)).toVar();return T(Ge(re(t,s,y(0))),Ge(re(t,s,y(1))),Ge(re(t,s,y(2))))}).setLayout({name:"mx_cell_noise_vec3_1",type:"vec3",inputs:[{name:"p",type:"vec2"}]}),IS=b(([o])=>{const e=T(o).toVar(),t=y(Te(e.x)).toVar(),s=y(Te(e.y)).toVar(),n=y(Te(e.z)).toVar();return T(Ge(re(t,s,n,y(0))),Ge(re(t,s,n,y(1))),Ge(re(t,s,n,y(2))))}).setLayout({name:"mx_cell_noise_vec3_2",type:"vec3",inputs:[{name:"p",type:"vec3"}]}),VS=b(([o])=>{const e=P(o).toVar(),t=y(Te(e.x)).toVar(),s=y(Te(e.y)).toVar(),n=y(Te(e.z)).toVar(),r=y(Te(e.w)).toVar();return T(Ge(re(t,s,n,r,y(0))),Ge(re(t,s,n,r,y(1))),Ge(re(t,s,n,r,y(2))))}).setLayout({name:"mx_cell_noise_vec3_3",type:"vec3",inputs:[{name:"p",type:"vec4"}]}),rg=Pe([DS,LS,IS,VS]),ri=b(([o,e,t,s])=>{const n=g(s).toVar(),r=g(t).toVar(),i=y(e).toVar(),a=T(o).toVar(),u=g(0).toVar(),c=g(1).toVar();return te(i,()=>{u.addAssign(c.mul(Ou(a))),c.mulAssign(n),a.mulAssign(r)}),u}).setLayout({name:"mx_fractal_noise_float",type:"float",inputs:[{name:"p",type:"vec3"},{name:"octaves",type:"int"},{name:"lacunarity",type:"float"},{name:"diminish",type:"float"}]}),ig=b(([o,e,t,s])=>{const n=g(s).toVar(),r=g(t).toVar(),i=y(e).toVar(),a=T(o).toVar(),u=T(0).toVar(),c=g(1).toVar();return te(i,()=>{u.addAssign(c.mul(ku(a))),c.mulAssign(n),a.mulAssign(r)}),u}).setLayout({name:"mx_fractal_noise_vec3",type:"vec3",inputs:[{name:"p",type:"vec3"},{name:"octaves",type:"int"},{name:"lacunarity",type:"float"},{name:"diminish",type:"float"}]}),GS=b(([o,e,t,s])=>{const n=g(s).toVar(),r=g(t).toVar(),i=y(e).toVar(),a=T(o).toVar();return M(ri(a,i,r,n),ri(a.add(T(y(19),y(193),y(17))),i,r,n))}).setLayout({name:"mx_fractal_noise_vec2",type:"vec2",inputs:[{name:"p",type:"vec3"},{name:"octaves",type:"int"},{name:"lacunarity",type:"float"},{name:"diminish",type:"float"}]}),OS=b(([o,e,t,s])=>{const n=g(s).toVar(),r=g(t).toVar(),i=y(e).toVar(),a=T(o).toVar(),u=T(ig(a,i,r,n)).toVar(),c=g(ri(a.add(T(y(19),y(193),y(17))),i,r,n)).toVar();return P(u,c)}).setLayout({name:"mx_fractal_noise_vec4",type:"vec4",inputs:[{name:"p",type:"vec3"},{name:"octaves",type:"int"},{name:"lacunarity",type:"float"},{name:"diminish",type:"float"}]}),kS=b(([o,e,t,s,n,r,i])=>{const a=y(i).toVar(),u=g(r).toVar(),c=y(n).toVar(),l=y(s).toVar(),d=y(t).toVar(),h=y(e).toVar(),p=M(o).toVar(),f=T(rg(M(h.add(l),d.add(c)))).toVar(),m=M(f.x,f.y).toVar();m.subAssign(.5),m.mulAssign(u),m.addAssign(.5);const x=M(M(g(h),g(d)).add(m)).toVar(),N=M(x.sub(p)).toVar();return z(a.equal(y(2)),()=>oe(N.x).add(oe(N.y))),z(a.equal(y(3)),()=>ue(oe(N.x),oe(N.y))),is(N,N)}).setLayout({name:"mx_worley_distance_0",type:"float",inputs:[{name:"p",type:"vec2"},{name:"x",type:"int"},{name:"y",type:"int"},{name:"xoff",type:"int"},{name:"yoff",type:"int"},{name:"jitter",type:"float"},{name:"metric",type:"int"}]}),zS=b(([o,e,t,s,n,r,i,a,u])=>{const c=y(u).toVar(),l=g(a).toVar(),d=y(i).toVar(),h=y(r).toVar(),p=y(n).toVar(),f=y(s).toVar(),m=y(t).toVar(),x=y(e).toVar(),N=T(o).toVar(),v=T(rg(T(x.add(p),m.add(h),f.add(d)))).toVar();v.subAssign(.5),v.mulAssign(l),v.addAssign(.5);const w=T(T(g(x),g(m),g(f)).add(v)).toVar(),B=T(w.sub(N)).toVar();return z(c.equal(y(2)),()=>oe(B.x).add(oe(B.y)).add(oe(B.z))),z(c.equal(y(3)),()=>ue(ue(oe(B.x),oe(B.y)),oe(B.z))),is(B,B)}).setLayout({name:"mx_worley_distance_1",type:"float",inputs:[{name:"p",type:"vec3"},{name:"x",type:"int"},{name:"y",type:"int"},{name:"z",type:"int"},{name:"xoff",type:"int"},{name:"yoff",type:"int"},{name:"zoff",type:"int"},{name:"jitter",type:"float"},{name:"metric",type:"int"}]}),pn=Pe([kS,zS]),WS=b(([o,e,t])=>{const s=y(t).toVar(),n=g(e).toVar(),r=M(o).toVar(),i=y().toVar(),a=y().toVar(),u=M(he(r.x,i),he(r.y,a)).toVar(),c=g(1e6).toVar();return te({start:-1,end:y(1),name:"x",condition:"<="},({x:l})=>{te({start:-1,end:y(1),name:"y",condition:"<="},({y:d})=>{const h=g(pn(u,l,d,i,a,n,s)).toVar();c.assign(Re(c,h))})}),z(s.equal(y(0)),()=>{c.assign(Pt(c))}),c}).setLayout({name:"mx_worley_noise_float_0",type:"float",inputs:[{name:"p",type:"vec2"},{name:"jitter",type:"float"},{name:"metric",type:"int"}]}),$S=b(([o,e,t])=>{const s=y(t).toVar(),n=g(e).toVar(),r=M(o).toVar(),i=y().toVar(),a=y().toVar(),u=M(he(r.x,i),he(r.y,a)).toVar(),c=M(1e6,1e6).toVar();return te({start:-1,end:y(1),name:"x",condition:"<="},({x:l})=>{te({start:-1,end:y(1),name:"y",condition:"<="},({y:d})=>{const h=g(pn(u,l,d,i,a,n,s)).toVar();z(h.lessThan(c.x),()=>{c.y.assign(c.x),c.x.assign(h)}).ElseIf(h.lessThan(c.y),()=>{c.y.assign(h)})})}),z(s.equal(y(0)),()=>{c.assign(Pt(c))}),c}).setLayout({name:"mx_worley_noise_vec2_0",type:"vec2",inputs:[{name:"p",type:"vec2"},{name:"jitter",type:"float"},{name:"metric",type:"int"}]}),HS=b(([o,e,t])=>{const s=y(t).toVar(),n=g(e).toVar(),r=M(o).toVar(),i=y().toVar(),a=y().toVar(),u=M(he(r.x,i),he(r.y,a)).toVar(),c=T(1e6,1e6,1e6).toVar();return te({start:-1,end:y(1),name:"x",condition:"<="},({x:l})=>{te({start:-1,end:y(1),name:"y",condition:"<="},({y:d})=>{const h=g(pn(u,l,d,i,a,n,s)).toVar();z(h.lessThan(c.x),()=>{c.z.assign(c.y),c.y.assign(c.x),c.x.assign(h)}).ElseIf(h.lessThan(c.y),()=>{c.z.assign(c.y),c.y.assign(h)}).ElseIf(h.lessThan(c.z),()=>{c.z.assign(h)})})}),z(s.equal(y(0)),()=>{c.assign(Pt(c))}),c}).setLayout({name:"mx_worley_noise_vec3_0",type:"vec3",inputs:[{name:"p",type:"vec2"},{name:"jitter",type:"float"},{name:"metric",type:"int"}]}),qS=b(([o,e,t])=>{const s=y(t).toVar(),n=g(e).toVar(),r=T(o).toVar(),i=y().toVar(),a=y().toVar(),u=y().toVar(),c=T(he(r.x,i),he(r.y,a),he(r.z,u)).toVar(),l=g(1e6).toVar();return te({start:-1,end:y(1),name:"x",condition:"<="},({x:d})=>{te({start:-1,end:y(1),name:"y",condition:"<="},({y:h})=>{te({start:-1,end:y(1),name:"z",condition:"<="},({z:p})=>{const f=g(pn(c,d,h,p,i,a,u,n,s)).toVar();l.assign(Re(l,f))})})}),z(s.equal(y(0)),()=>{l.assign(Pt(l))}),l}).setLayout({name:"mx_worley_noise_float_1",type:"float",inputs:[{name:"p",type:"vec3"},{name:"jitter",type:"float"},{name:"metric",type:"int"}]}),KS=Pe([WS,qS]),XS=b(([o,e,t])=>{const s=y(t).toVar(),n=g(e).toVar(),r=T(o).toVar(),i=y().toVar(),a=y().toVar(),u=y().toVar(),c=T(he(r.x,i),he(r.y,a),he(r.z,u)).toVar(),l=M(1e6,1e6).toVar();return te({start:-1,end:y(1),name:"x",condition:"<="},({x:d})=>{te({start:-1,end:y(1),name:"y",condition:"<="},({y:h})=>{te({start:-1,end:y(1),name:"z",condition:"<="},({z:p})=>{const f=g(pn(c,d,h,p,i,a,u,n,s)).toVar();z(f.lessThan(l.x),()=>{l.y.assign(l.x),l.x.assign(f)}).ElseIf(f.lessThan(l.y),()=>{l.y.assign(f)})})})}),z(s.equal(y(0)),()=>{l.assign(Pt(l))}),l}).setLayout({name:"mx_worley_noise_vec2_1",type:"vec2",inputs:[{name:"p",type:"vec3"},{name:"jitter",type:"float"},{name:"metric",type:"int"}]}),YS=Pe([$S,XS]),jS=b(([o,e,t])=>{const s=y(t).toVar(),n=g(e).toVar(),r=T(o).toVar(),i=y().toVar(),a=y().toVar(),u=y().toVar(),c=T(he(r.x,i),he(r.y,a),he(r.z,u)).toVar(),l=T(1e6,1e6,1e6).toVar();return te({start:-1,end:y(1),name:"x",condition:"<="},({x:d})=>{te({start:-1,end:y(1),name:"y",condition:"<="},({y:h})=>{te({start:-1,end:y(1),name:"z",condition:"<="},({z:p})=>{const f=g(pn(c,d,h,p,i,a,u,n,s)).toVar();z(f.lessThan(l.x),()=>{l.z.assign(l.y),l.y.assign(l.x),l.x.assign(f)}).ElseIf(f.lessThan(l.y),()=>{l.z.assign(l.y),l.y.assign(f)}).ElseIf(f.lessThan(l.z),()=>{l.z.assign(f)})})})}),z(s.equal(y(0)),()=>{l.assign(Pt(l))}),l}).setLayout({name:"mx_worley_noise_vec3_1",type:"vec3",inputs:[{name:"p",type:"vec3"},{name:"jitter",type:"float"},{name:"metric",type:"int"}]}),QS=Pe([HS,jS]),ZS=b(([o])=>{const e=o.y,t=o.z,s=T().toVar();return z(e.lessThan(1e-4),()=>{s.assign(T(t,t,t))}).Else(()=>{let n=o.x;n=n.sub(At(n)).mul(6).toVar();const r=y(Ka(n)),i=n.sub(g(r)),a=t.mul(e.oneMinus()),u=t.mul(e.mul(i).oneMinus()),c=t.mul(e.mul(i.oneMinus()).oneMinus());z(r.equal(y(0)),()=>{s.assign(T(t,c,a))}).ElseIf(r.equal(y(1)),()=>{s.assign(T(u,t,a))}).ElseIf(r.equal(y(2)),()=>{s.assign(T(a,t,c))}).ElseIf(r.equal(y(3)),()=>{s.assign(T(a,u,t))}).ElseIf(r.equal(y(4)),()=>{s.assign(T(c,a,t))}).Else(()=>{s.assign(T(t,a,u))})}),s}).setLayout({name:"mx_hsvtorgb",type:"vec3",inputs:[{name:"hsv",type:"vec3"}]}),JS=b(([o])=>{const e=T(o).toVar(),t=g(e.x).toVar(),s=g(e.y).toVar(),n=g(e.z).toVar(),r=g(Re(t,Re(s,n))).toVar(),i=g(ue(t,ue(s,n))).toVar(),a=g(i.sub(r)).toVar(),u=g().toVar(),c=g().toVar(),l=g().toVar();return l.assign(i),z(i.greaterThan(0),()=>{c.assign(a.div(i))}).Else(()=>{c.assign(0)}),z(c.lessThanEqual(0),()=>{u.assign(0)}).Else(()=>{z(t.greaterThanEqual(i),()=>{u.assign(s.sub(n).div(a))}).ElseIf(s.greaterThanEqual(i),()=>{u.assign(_e(2,n.sub(t).div(a)))}).Else(()=>{u.assign(_e(4,t.sub(s).div(a)))}),u.mulAssign(1/6),z(u.lessThan(0),()=>{u.addAssign(1)})}),T(u,c,l)}).setLayout({name:"mx_rgbtohsv",type:"vec3",inputs:[{name:"c",type:"vec3"}]}),ev=b(([o])=>{const e=T(o).toVar(),t=wa(Oa(e,T(.04045))).toVar(),s=T(e.div(12.92)).toVar(),n=T(ht(ue(e.add(T(.055)),T(0)).div(1.055),T(2.4))).toVar();return Q(s,n,t)}).setLayout({name:"mx_srgb_texture_to_lin_rec709",type:"vec3",inputs:[{name:"color",type:"vec3"}]}),og=(o,e)=>{o=g(o),e=g(e);const t=M(e.dFdx(),e.dFdy()).length().mul(.7071067811865476);return ct(o.sub(t),o.add(t),e)},ag=(o,e,t,s)=>Q(o,e,t[s].clamp()),tv=(o,e,t=le())=>ag(o,e,t,"x"),sv=(o,e,t=le())=>ag(o,e,t,"y"),ug=(o,e,t,s,n)=>Q(o,e,og(t,s[n])),nv=(o,e,t,s=le())=>ug(o,e,t,s,"x"),rv=(o,e,t,s=le())=>ug(o,e,t,s,"y"),iv=(o=1,e=0,t=le())=>t.mul(o).add(e),ov=(o,e=1)=>(o=g(o),o.abs().pow(e).mul(o.sign())),av=(o,e=1,t=.5)=>g(o).sub(t).mul(e).add(t),uv=(o=le(),e=1,t=0)=>Ou(o.convert("vec2|vec3")).mul(e).add(t),cv=(o=le(),e=1,t=0)=>ku(o.convert("vec2|vec3")).mul(e).add(t),lv=(o=le(),e=1,t=0)=>(o=o.convert("vec2|vec3"),P(ku(o),Ou(o.add(M(19,73)))).mul(e).add(t)),dv=(o=le(),e=1)=>KS(o.convert("vec2|vec3"),e,y(1)),hv=(o=le(),e=1)=>YS(o.convert("vec2|vec3"),e,y(1)),pv=(o=le(),e=1)=>QS(o.convert("vec2|vec3"),e,y(1)),fv=(o=le())=>PS(o.convert("vec2|vec3")),gv=(o=le(),e=3,t=2,s=.5,n=1)=>ri(o,y(e),t,s).mul(n),mv=(o=le(),e=3,t=2,s=.5,n=1)=>GS(o,y(e),t,s).mul(n),yv=(o=le(),e=3,t=2,s=.5,n=1)=>ig(o,y(e),t,s).mul(n),xv=(o=le(),e=3,t=2,s=.5,n=1)=>OS(o,y(e),t,s).mul(n),Tv=b(([o,e,t])=>{const s=qt(o).toVar("nDir"),n=Y(g(.5).mul(e.sub(t)),Wt).div(s).toVar("rbmax"),r=Y(g(-.5).mul(e.sub(t)),Wt).div(s).toVar("rbmin"),i=T().toVar("rbminmax");i.x=s.x.greaterThan(g(0)).select(n.x,r.x),i.y=s.y.greaterThan(g(0)).select(n.y,r.y),i.z=s.z.greaterThan(g(0)).select(n.z,r.z);const a=Re(Re(i.x,i.y),i.z).toVar("correction");return Wt.add(s.mul(a)).toVar("boxIntersection").sub(t)}),cg=b(([o,e])=>{const t=o.x,s=o.y,n=o.z;let r=e.element(0).mul(.886227);return r=r.add(e.element(1).mul(2*.511664).mul(s)),r=r.add(e.element(2).mul(2*.511664).mul(n)),r=r.add(e.element(3).mul(2*.511664).mul(t)),r=r.add(e.element(4).mul(2*.429043).mul(t).mul(s)),r=r.add(e.element(5).mul(2*.429043).mul(s).mul(n)),r=r.add(e.element(6).mul(n.mul(n).mul(.743125).sub(.247708))),r=r.add(e.element(7).mul(2*.429043).mul(t).mul(n)),r=r.add(e.element(8).mul(.429043).mul($(t,t).sub($(s,s)))),r});var mR=Object.freeze({__proto__:null,BRDF_GGX:jo,BRDF_Lambert:Ns,BasicShadowFilter:qf,Break:pu,Continue:Ex,DFGApprox:Nu,D_GGX:Xp,Discard:Fh,EPSILON:Gd,F_Schlick:un,Fn:b,INFINITY:Ny,If:z,Loop:te,NodeAccess:Ve,NodeShaderStage:Oo,NodeType:Zm,NodeUpdateType:W,PCFShadowFilter:Kf,PCFSoftShadowFilter:Xf,PI:Qr,PI2:Sy,Return:Vy,Schlick_to_F0:jp,ScriptableNodeResources:Wr,ShaderNode:Cn,TBNViewMatrix:gs,VSMShadowFilter:Yf,V_GGX_SmithCorrelated:Kp,abs:oe,acesFilmicToneMapping:Bf,acos:Hd,add:_e,addMethodChaining:R,addNodeElement:Oy,agxToneMapping:Ff,all:ka,alphaT:Xr,and:Ed,anisotropy:Zt,anisotropyB:_s,anisotropyT:En,any:Od,append:ud,arrayBuffer:yy,asin:$d,assign:bd,atan:$a,atan2:lh,atomicAdd:DN,atomicAnd:GN,atomicFunc:Yt,atomicMax:IN,atomicMin:VN,atomicOr:ON,atomicStore:PN,atomicSub:LN,atomicXor:kN,attenuationColor:Ia,attenuationDistance:La,attribute:we,attributeArray:Eb,backgroundBlurriness:Tf,backgroundIntensity:sa,backgroundRotation:_f,batch:Mp,billboarding:ab,bitAnd:Fd,bitNot:Ud,bitOr:Pd,bitXor:Dd,bitangentGeometry:lx,bitangentLocal:dx,bitangentView:Yh,bitangentWorld:hx,bitcast:vy,blendBurn:Sf,blendColor:Ob,blendDodge:vf,blendOverlay:Rf,blendScreen:Af,blur:sf,bool:Ht,buffer:ir,bufferAttribute:nr,bumpMap:Jh,burn:kb,bvec2:dd,bvec3:wa,bvec4:gd,bypass:Eh,cache:Mn,call:Nd,cameraFar:es,cameraNear:Jt,cameraNormalMatrix:qy,cameraPosition:su,cameraProjectionMatrix:He,cameraProjectionMatrixInverse:$y,cameraViewMatrix:je,cameraWorldMatrix:Hy,cbrt:ih,cdl:Yb,ceil:xi,checker:oS,cineonToneMapping:Mf,clamp:Et,clearcoat:Kr,clearcoatRoughness:zn,code:Fi,color:cd,colorSpaceToWorking:eu,colorToDirection:rT,compute:Ch,cond:dh,context:bi,convert:yd,convertColorSpace:wy,convertToTexture:_b,cos:It,cross:_i,cubeTexture:on,dFdx:Ha,dFdy:qa,dashSize:bs,defaultBuildStages:ko,defaultShaderStages:sd,defined:Gn,degrees:zd,deltaTime:gf,densityFog:xN,densityFogFactor:Mu,depth:xu,depthPass:tN,difference:th,diffuseColor:ee,directPointLight:Zf,directionToColor:Op,dispersion:Va,distance:eh,div:gt,dodge:zb,dot:is,drawIndex:Cp,dynamicBufferAttribute:Rh,element:md,emissive:Ho,equal:Sd,equals:Zd,equirectUV:Tu,exp:za,exp2:rn,expression:rs,faceDirection:rr,faceForward:Za,faceforward:Ay,float:g,floor:At,fog:Xn,fract:Xt,frameGroup:_d,frameId:Q_,frontFacing:Gh,fwidth:jd,gain:$_,gapSize:Hn,getConstNodeType:ad,getCurrentStack:Ea,getDirection:ef,getDistanceAttenuation:Gu,getGeometryRoughness:qp,getNormalFromDepth:Nb,getParallaxCorrectNormal:Tv,getRoughness:bu,getScreenPosition:bb,getShIrradianceAt:cg,getTextureIndex:pf,getViewPosition:ks,glsl:lN,glslFn:dN,grayscale:Hb,greaterThan:Oa,greaterThanEqual:Cd,hash:W_,highpModelNormalViewMatrix:sx,highpModelViewMatrix:tx,hue:Xb,instance:Sx,instanceIndex:lr,instancedArray:wb,instancedBufferAttribute:Zr,instancedDynamicBufferAttribute:qo,instancedMesh:wp,int:y,inverseSqrt:Wa,inversesqrt:Ry,invocationLocalIndex:Nx,invocationSubgroupIndex:bx,ior:wn,iridescence:mi,iridescenceIOR:Ua,iridescenceThickness:Pa,ivec2:Ae,ivec3:hd,ivec4:pd,js:uN,label:ph,length:zt,lengthSq:ja,lessThan:Ad,lessThanEqual:Rd,lightPosition:Uu,lightProjectionUV:Vf,lightShadowMatrix:Fu,lightTargetDirection:Pu,lightTargetPosition:Gf,lightViewPosition:Pi,lightingContext:Pp,lights:$N,linearDepth:ei,linearToneMapping:Ef,localId:vN,log:yi,log2:vt,logarithmicDepthToViewZ:zx,loop:wx,luminance:Eu,mat2:fi,mat3:Oe,mat4:Ts,matcapUV:of,materialAO:Ap,materialAlphaTest:ep,materialAnisotropy:fp,materialAnisotropyVector:Os,materialAttenuationColor:Np,materialAttenuationDistance:bp,materialClearcoat:up,materialClearcoatNormal:lp,materialClearcoatRoughness:cp,materialColor:an,materialDispersion:vp,materialEmissive:sp,materialIOR:_p,materialIridescence:gp,materialIridescenceIOR:mp,materialIridescenceThickness:yp,materialLightMap:du,materialLineDashOffset:lu,materialLineDashSize:uu,materialLineGapSize:cu,materialLineScale:au,materialLineWidth:Or,materialMetalness:op,materialNormal:ap,materialOpacity:cr,materialPointWidth:Sp,materialReference:dt,materialReflectivity:Gr,materialRefractionRatio:zh,materialRotation:dp,materialRoughness:ip,materialSheen:hp,materialSheenRoughness:pp,materialShininess:tp,materialSpecular:np,materialSpecularColor:rp,materialSpecularIntensity:Yo,materialSpecularStrength:Bn,materialThickness:Tp,materialTransmission:xp,max:ue,maxMipLevel:Ph,mediumpModelViewMatrix:Vh,metalness:kn,min:Re,mix:Q,mixElement:uh,mod:Xa,modInt:Ga,modelDirection:Qy,modelNormalMatrix:Lh,modelPosition:Zy,modelScale:Jy,modelViewMatrix:Kt,modelViewPosition:ex,modelViewProjection:hu,modelWorldMatrix:at,modelWorldMatrixInverse:Ih,morphReference:Up,mrt:ff,mul:$,mx_aastep:og,mx_cell_noise_float:fv,mx_contrast:av,mx_fractal_noise_float:gv,mx_fractal_noise_vec2:mv,mx_fractal_noise_vec3:yv,mx_fractal_noise_vec4:xv,mx_hsvtorgb:ZS,mx_noise_float:uv,mx_noise_vec3:cv,mx_noise_vec4:lv,mx_ramplr:tv,mx_ramptb:sv,mx_rgbtohsv:JS,mx_safepower:ov,mx_splitlr:nv,mx_splittb:rv,mx_srgb_texture_to_lin_rec709:ev,mx_transform_uv:iv,mx_worley_noise_float:dv,mx_worley_noise_vec2:hv,mx_worley_noise_vec3:pv,negate:qd,neutralToneMapping:Uf,nodeArray:xs,nodeImmutable:U,nodeObject:E,nodeObjects:Jn,nodeProxy:C,normalFlat:Oh,normalGeometry:Ni,normalLocal:Xe,normalMap:Xo,normalView:lt,normalWorld:Si,normalize:qt,not:Md,notEqual:vd,numWorkgroups:NN,objectDirection:Ky,objectGroup:Ba,objectPosition:Dh,objectScale:Yy,objectViewPosition:jy,objectWorldMatrix:Xy,oneMinus:Kd,or:wd,orthographicDepthToViewZ:kx,oscSawtooth:rb,oscSine:tb,oscSquare:sb,oscTriangle:nb,output:$n,outputStruct:k_,overlay:$b,overloadingFn:Pe,parabola:ta,parallaxDirection:Qh,parallaxUV:fx,parameter:G_,pass:Jb,passTexture:eN,pcurve:H_,perspectiveDepthToViewZ:mu,pmremTexture:vu,pointUV:Ub,pointWidth:Ty,positionGeometry:Ce,positionLocal:me,positionPrevious:Jr,positionView:xe,positionViewDirection:ae,positionWorld:Wt,positionWorldDirection:nu,posterize:Qb,pow:ht,pow2:Ya,pow3:sh,pow4:nh,property:Fa,radians:kd,rand:ah,range:_N,rangeFog:yN,rangeFogFactor:wu,reciprocal:Yd,reference:ne,referenceBuffer:Ko,reflect:Jd,reflectVector:Hh,reflectView:Wh,reflector:gb,refract:Qa,refractVector:qh,refractView:$h,reinhardToneMapping:wf,remainder:Vd,remap:Mh,remapClamp:Bh,renderGroup:k,renderOutput:tu,rendererReference:Sh,rotate:Au,rotateUV:ib,roughness:Nt,round:Xd,rtt:yf,sRGBTransferEOTF:yh,sRGBTransferOETF:xh,sampler:Wy,saturate:oh,saturation:qb,screen:Wb,screenCoordinate:dr,screenSize:Kn,screenUV:Bt,scriptable:mN,scriptableValue:zr,select:Ue,setCurrentStack:On,shaderStages:zo,shadow:Qf,shadowPositionWorld:Lu,sharedUniformGroup:Ma,sheen:fs,sheenRoughness:gi,shiftLeft:Ld,shiftRight:Id,shininess:Yr,sign:qn,sin:st,sinc:q_,skinning:Rx,skinningReference:Fp,smoothstep:ct,smoothstepElement:ch,specularColor:We,specularF90:Wn,spherizeUV:ob,split:xy,spritesheetUV:lb,sqrt:Pt,stack:kr,step:Ti,storage:Bi,storageBarrier:EN,storageObject:Cb,storageTexture:bf,string:my,sub:Y,subgroupIndex:_x,subgroupSize:AN,tan:Wd,tangentGeometry:Ri,tangentLocal:or,tangentView:ar,tangentWorld:Xh,temp:gh,texture:K,texture3D:af,textureBarrier:wN,textureBicubic:Jp,textureCubeUV:tf,textureLoad:ge,textureSize:ns,textureStore:Db,thickness:Da,time:Cs,timerDelta:eb,timerGlobal:J_,timerLocal:Z_,toOutputColorSpace:Th,toWorkingColorSpace:_h,toneMapping:vh,toneMappingExposure:Ah,toonOutlinePass:nN,transformDirection:rh,transformNormal:kh,transformNormalToView:ru,transformedBentNormalView:Zh,transformedBitangentView:jh,transformedBitangentWorld:px,transformedClearcoatNormalView:Hs,transformedNormalView:fe,transformedNormalWorld:vi,transformedTangentView:ou,transformedTangentWorld:cx,transmission:jr,transpose:Qd,triNoise3D:X_,triplanarTexture:hb,triplanarTextures:mf,trunc:Ka,tslFn:gy,uint:D,uniform:V,uniformArray:Gt,uniformGroup:Td,uniforms:ox,userData:Ib,uv:le,uvec2:ld,uvec3:dn,uvec4:fd,varying:ke,varyingProperty:et,vec2:M,vec3:T,vec4:P,vectorComponents:As,velocity:Gb,vertexColor:Bb,vertexIndex:Rp,vertexStage:mh,vibrance:Kb,viewZToLogarithmicDepth:yu,viewZToOrthographicDepth:Qs,viewZToPerspectiveDepth:Ip,viewport:$t,viewportBottomLeft:Vx,viewportCoordinate:Lp,viewportDepthTexture:gu,viewportLinearDepth:Wx,viewportMipTexture:fu,viewportResolution:Lx,viewportSafeUV:ub,viewportSharedTexture:Gp,viewportSize:Dp,viewportTexture:Gx,viewportTopLeft:Ix,viewportUV:Dx,wgsl:cN,wgslFn:hN,workgroupArray:FN,workgroupBarrier:CN,workgroupId:SN,workingToColorSpace:bh,xor:Bd});const Lt=new Ru;class _v extends os{constructor(e,t){super(),this.renderer=e,this.nodes=t}update(e,t,s){const n=this.renderer,r=this.nodes.getBackgroundNode(e)||e.background;let i=!1;if(r===null)n._clearColor.getRGB(Lt,kt),Lt.a=n._clearColor.a;else if(r.isColor===!0)r.getRGB(Lt,kt),Lt.a=1,i=!0;else if(r.isNode===!0){const a=this.get(e),u=r;Lt.copy(n._clearColor);let c=a.backgroundMesh;if(c===void 0){const d=bi(P(u).mul(sa),{getUV:()=>_f.mul(Si),getTextureLevel:()=>Tf});let h=hu;h=h.setZ(h.w);const p=new ce;p.name="Background.material",p.side=Ct,p.depthTest=!1,p.depthWrite=!1,p.fog=!1,p.lights=!1,p.vertexNode=h,p.colorNode=d,a.backgroundMeshNode=d,a.backgroundMesh=c=new tn(new mm(1,32,32),p),c.frustumCulled=!1,c.name="Background.mesh",c.onBeforeRender=function(f,m,x){this.matrixWorld.copyPosition(x.matrixWorld)}}const l=u.getCacheKey();a.backgroundCacheKey!==l&&(a.backgroundMeshNode.node=P(u).mul(sa),a.backgroundMeshNode.needsUpdate=!0,c.material.needsUpdate=!0,a.backgroundCacheKey=l),t.unshift(c,c.geometry,c.material,0,0,null,null)}else console.error("THREE.Renderer: Unsupported background configuration.",r);if(n.autoClear===!0||i===!0){const a=s.clearColorValue;a.r=Lt.r,a.g=Lt.g,a.b=Lt.b,a.a=Lt.a,(n.backend.isWebGLBackend===!0||n.alpha===!0)&&(a.r*=a.a,a.g*=a.a,a.b*=a.a),s.depthClearValue=n._clearDepth,s.stencilClearValue=n._clearStencil,s.clearColor=n.autoClearColor===!0,s.clearDepth=n.autoClearDepth===!0,s.clearStencil=n.autoClearStencil===!0}else s.clearColor=!1,s.clearDepth=!1,s.clearStencil=!1}}let bv=0;class na{constructor(e="",t=[],s=0,n=[]){this.name=e,this.bindings=t,this.index=s,this.bindingsReference=n,this.id=bv++}}class Nv{constructor(e,t,s,n,r,i,a,u,c,l=[]){this.vertexShader=e,this.fragmentShader=t,this.computeShader=s,this.transforms=l,this.nodeAttributes=n,this.bindings=r,this.updateNodes=i,this.updateBeforeNodes=a,this.updateAfterNodes=u,this.monitor=c,this.usedTimes=0}createBindings(){const e=[];for(const t of this.bindings)if(t.bindings[0].groupNode.shared!==!0){const n=new na(t.name,[],t.index,t);e.push(n);for(const r of t.bindings)n.bindings.push(r.clone())}else e.push(t);return e}}class Gc{constructor(e,t,s=null){this.isNodeAttribute=!0,this.name=e,this.type=t,this.node=s}}class Sv{constructor(e,t,s){this.isNodeUniform=!0,this.name=e,this.type=t,this.node=s.getSelf()}get value(){return this.node.value}set value(e){this.node.value=e}get id(){return this.node.id}get groupNode(){return this.node.groupNode}}class lg{constructor(e,t){this.isNodeVar=!0,this.name=e,this.type=t}}class vv extends lg{constructor(e,t){super(e,t),this.needsInterpolation=!1,this.isNodeVarying=!0}}class Av{constructor(e,t,s=""){this.name=e,this.type=t,this.code=s,Object.defineProperty(this,"isNodeCode",{value:!0})}}let Rv=0;class to{constructor(e=null){this.id=Rv++,this.nodesData=new WeakMap,this.parent=e}getData(e){let t=this.nodesData.get(e);return t===void 0&&this.parent!==null&&(t=this.parent.getData(e)),t}setData(e,t){this.nodesData.set(e,t)}}class Cv extends O{static get type(){return"StructTypeNode"}constructor(e,t){super(),this.name=e,this.types=t,this.isStructTypeNode=!0}getMemberTypes(){return this.types}}class ws{constructor(e,t){this.name=e,this.value=t,this.boundary=0,this.itemSize=0,this.offset=0}setValue(e){this.value=e}getValue(){return this.value}}class Ev extends ws{constructor(e,t=0){super(e,t),this.isNumberUniform=!0,this.boundary=4,this.itemSize=1}}class wv extends ws{constructor(e,t=new Ye){super(e,t),this.isVector2Uniform=!0,this.boundary=8,this.itemSize=2}}class Mv extends ws{constructor(e,t=new j){super(e,t),this.isVector3Uniform=!0,this.boundary=16,this.itemSize=3}}class Bv extends ws{constructor(e,t=new Me){super(e,t),this.isVector4Uniform=!0,this.boundary=16,this.itemSize=4}}class Fv extends ws{constructor(e,t=new mt){super(e,t),this.isColorUniform=!0,this.boundary=16,this.itemSize=3}}class Uv extends ws{constructor(e,t=new Yn){super(e,t),this.isMatrix3Uniform=!0,this.boundary=48,this.itemSize=12}}class Pv extends ws{constructor(e,t=new Ie){super(e,t),this.isMatrix4Uniform=!0,this.boundary=64,this.itemSize=16}}class Dv extends Ev{constructor(e){super(e.name,e.value),this.nodeUniform=e}getValue(){return this.nodeUniform.value}getType(){return this.nodeUniform.type}}class Lv extends wv{constructor(e){super(e.name,e.value),this.nodeUniform=e}getValue(){return this.nodeUniform.value}getType(){return this.nodeUniform.type}}class Iv extends Mv{constructor(e){super(e.name,e.value),this.nodeUniform=e}getValue(){return this.nodeUniform.value}getType(){return this.nodeUniform.type}}class Vv extends Bv{constructor(e){super(e.name,e.value),this.nodeUniform=e}getValue(){return this.nodeUniform.value}getType(){return this.nodeUniform.type}}class Gv extends Fv{constructor(e){super(e.name,e.value),this.nodeUniform=e}getValue(){return this.nodeUniform.value}getType(){return this.nodeUniform.type}}class Ov extends Uv{constructor(e){super(e.name,e.value),this.nodeUniform=e}getValue(){return this.nodeUniform.value}getType(){return this.nodeUniform.type}}class kv extends Pv{constructor(e){super(e.name,e.value),this.nodeUniform=e}getValue(){return this.nodeUniform.value}getType(){return this.nodeUniform.type}}const qs=4,Oc=[.125,.215,.35,.446,.526,.582],hs=20,so=new Tl(-1,1,1,-1,0,1),zv=new kg(90,1),kc=new mt;let no=null,ro=0,io=0;const ds=(1+Math.sqrt(5))/2,Ds=1/ds,zc=[new j(-ds,Ds,0),new j(ds,Ds,0),new j(-Ds,0,ds),new j(Ds,0,ds),new j(0,ds,-Ds),new j(0,ds,Ds),new j(-1,1,-1),new j(1,1,-1),new j(-1,1,1),new j(1,1,1)],Wv=[3,1,5,0,4,2],oo=ef(le(),we("faceIndex")).normalize(),zu=T(oo.x,oo.y,oo.z);class $v{constructor(e){this._renderer=e,this._pingPongRenderTarget=null,this._lodMax=0,this._cubeSize=0,this._lodPlanes=[],this._sizeLods=[],this._sigmas=[],this._lodMeshes=[],this._blurMaterial=null,this._cubemapMaterial=null,this._equirectMaterial=null,this._backgroundBox=null}get _hasInitialized(){return this._renderer.hasInitialized()}fromScene(e,t=0,s=.1,n=100,r=null){if(this._setSize(256),this._hasInitialized===!1){console.warn("THREE.PMREMGenerator: .fromScene() called before the backend is initialized. Try using .fromSceneAsync() instead.");const a=r||this._allocateTargets();return this.fromSceneAsync(e,t,s,n,a),a}no=this._renderer.getRenderTarget(),ro=this._renderer.getActiveCubeFace(),io=this._renderer.getActiveMipmapLevel();const i=r||this._allocateTargets();return i.depthBuffer=!0,this._sceneToCubeUV(e,s,n,i),t>0&&this._blur(i,0,0,t),this._applyPMREM(i),this._cleanup(i),i}async fromSceneAsync(e,t=0,s=.1,n=100,r=null){return this._hasInitialized===!1&&await this._renderer.init(),this.fromScene(e,t,s,n,r)}fromEquirectangular(e,t=null){if(this._hasInitialized===!1){console.warn("THREE.PMREMGenerator: .fromEquirectangular() called before the backend is initialized. Try using .fromEquirectangularAsync() instead."),this._setSizeFromTexture(e);const s=t||this._allocateTargets();return this.fromEquirectangularAsync(e,s),s}return this._fromTexture(e,t)}async fromEquirectangularAsync(e,t=null){return this._hasInitialized===!1&&await this._renderer.init(),this._fromTexture(e,t)}fromCubemap(e,t=null){if(this._hasInitialized===!1){console.warn("THREE.PMREMGenerator: .fromCubemap() called before the backend is initialized. Try using .fromCubemapAsync() instead."),this._setSizeFromTexture(e);const s=t||this._allocateTargets();return this.fromCubemapAsync(e,t),s}return this._fromTexture(e,t)}async fromCubemapAsync(e,t=null){return this._hasInitialized===!1&&await this._renderer.init(),this._fromTexture(e,t)}async compileCubemapShader(){this._cubemapMaterial===null&&(this._cubemapMaterial=$c(),await this._compileMaterial(this._cubemapMaterial))}async compileEquirectangularShader(){this._equirectMaterial===null&&(this._equirectMaterial=Hc(),await this._compileMaterial(this._equirectMaterial))}dispose(){this._dispose(),this._cubemapMaterial!==null&&this._cubemapMaterial.dispose(),this._equirectMaterial!==null&&this._equirectMaterial.dispose(),this._backgroundBox!==null&&(this._backgroundBox.geometry.dispose(),this._backgroundBox.material.dispose())}_setSizeFromTexture(e){e.mapping===Zs||e.mapping===Js?this._setSize(e.image.length===0?16:e.image[0].width||e.image[0].image.width):this._setSize(e.image.width/4)}_setSize(e){this._lodMax=Math.floor(Math.log2(e)),this._cubeSize=Math.pow(2,this._lodMax)}_dispose(){this._blurMaterial!==null&&this._blurMaterial.dispose(),this._pingPongRenderTarget!==null&&this._pingPongRenderTarget.dispose();for(let e=0;e2?m:0,m,m),u.render(e,r)}u.autoClear=c,e.background=h}_textureToCubeUV(e,t){const s=this._renderer,n=e.mapping===Zs||e.mapping===Js;n?this._cubemapMaterial===null&&(this._cubemapMaterial=$c(e)):this._equirectMaterial===null&&(this._equirectMaterial=Hc(e));const r=n?this._cubemapMaterial:this._equirectMaterial;r.fragmentNode.value=e;const i=this._lodMeshes[0];i.material=r;const a=this._cubeSize;vr(t,0,0,3*a,2*a),s.setRenderTarget(t),s.render(i,so)}_applyPMREM(e){const t=this._renderer,s=t.autoClear;t.autoClear=!1;const n=this._lodPlanes.length;for(let r=1;rhs&&console.warn(`sigmaRadians, ${r}, is too large and will clip, as it requested ${x} samples when the maximum is set to ${hs}`);const N=[];let v=0;for(let I=0;Iw-qs?n-w+qs:0),L=4*(this._cubeSize-B);vr(t,F,L,3*B,2*B),u.setRenderTarget(t),u.render(d,so)}}function Hv(o){const e=[],t=[],s=[],n=[];let r=o;const i=o-qs+1+Oc.length;for(let a=0;ao-qs?c=Oc[a-o+qs-1]:a===0&&(c=0),s.push(c);const l=1/(u-2),d=-l,h=1+l,p=[d,d,h,d,h,h,d,d,h,h,d,h],f=6,m=6,x=3,N=2,v=1,w=new Float32Array(x*m*f),B=new Float32Array(N*m*f),F=new Float32Array(v*m*f);for(let I=0;I2?0:-1,ie=[G,X,0,G+2/3,X,0,G+2/3,X+1,0,G,X,0,G+2/3,X+1,0,G,X+1,0],Z=Wv[I];w.set(ie,x*m*Z),B.set(p,N*m*Z);const Qe=[Z,Z,Z,Z,Z,Z];F.set(Qe,v*m*Z)}const L=new Sl;L.setAttribute("position",new Ur(w,x)),L.setAttribute("uv",new Ur(B,N)),L.setAttribute("faceIndex",new Ur(F,v)),e.push(L),n.push(new tn(L,null)),r>qs&&r--}return{lodPlanes:e,sizeLods:t,sigmas:s,lodMeshes:n}}function Wc(o,e,t){const s=new Ss(o,e,t);return s.texture.mapping=bo,s.texture.name="PMREM.cubeUv",s.texture.isPMREMTexture=!0,s.scissorTest=!0,s}function vr(o,e,t,s,n){o.viewport.set(e,t,s,n),o.scissor.set(e,t,s,n)}function Wu(o){const e=new ce;return e.depthTest=!1,e.depthWrite=!1,e.blending=en,e.name=`PMREM_${o}`,e}function qv(o,e,t){const s=Gt(new Array(hs).fill(0)),n=V(new j(0,1,0)),r=V(0),i=g(hs),a=V(0),u=V(1),c=K(null),l=V(0),d=g(1/e),h=g(1/t),p=g(o),f={n:i,latitudinal:a,weights:s,poleAxis:n,outputDirection:zu,dTheta:r,samples:u,envMap:c,mipInt:l,CUBEUV_TEXEL_WIDTH:d,CUBEUV_TEXEL_HEIGHT:h,CUBEUV_MAX_MIP:p},m=Wu("blur");return m.uniforms=f,m.fragmentNode=sf({...f,latitudinal:a.equal(1)}),m}function $c(o){const e=Wu("cubemap");return e.fragmentNode=on(o,zu),e}function Hc(o){const e=Wu("equirect");return e.fragmentNode=K(o,Tu(zu),0),e}const qc=new WeakMap,Kv=new Map([[Int8Array,"int"],[Int16Array,"int"],[Int32Array,"int"],[Uint8Array,"uint"],[Uint16Array,"uint"],[Uint32Array,"uint"],[Float32Array,"float"]]),Ar=o=>/e/g.test(o)?String(o).replace(/\+/g,""):(o=Number(o),o+(o%1?"":".0"));class dg{constructor(e,t,s){this.object=e,this.material=e&&e.material||null,this.geometry=e&&e.geometry||null,this.renderer=t,this.parser=s,this.scene=null,this.camera=null,this.nodes=[],this.sequentialNodes=[],this.updateNodes=[],this.updateBeforeNodes=[],this.updateAfterNodes=[],this.hashNodes={},this.monitor=null,this.lightsNode=null,this.environmentNode=null,this.fogNode=null,this.clippingContext=null,this.vertexShader=null,this.fragmentShader=null,this.computeShader=null,this.flowNodes={vertex:[],fragment:[],compute:[]},this.flowCode={vertex:"",fragment:"",compute:""},this.uniforms={vertex:[],fragment:[],compute:[],index:0},this.structs={vertex:[],fragment:[],compute:[],index:0},this.bindings={vertex:{},fragment:{},compute:{}},this.bindingsIndexes={},this.bindGroups=null,this.attributes=[],this.bufferAttributes=[],this.varyings=[],this.codes={},this.vars={},this.flow={code:""},this.chaining=[],this.stack=kr(),this.stacks=[],this.tab=" ",this.currentFunctionNode=null,this.context={material:this.material},this.cache=new to,this.globalCache=this.cache,this.flowsData=new WeakMap,this.shaderStage=null,this.buildStage=null,this.useComparisonMethod=!1}getBindGroupsCache(){let e=qc.get(this.renderer);return e===void 0&&(e=new Ft,qc.set(this.renderer,e)),e}createRenderTarget(e,t,s){return new Ss(e,t,s)}createCubeRenderTarget(e,t){return new kp(e,t)}createPMREMGenerator(){return new $v(this.renderer)}includes(e){return this.nodes.includes(e)}_getBindGroup(e,t){const s=this.getBindGroupsCache(),n=[];let r=!0;for(const a of t)n.push(a),r=r&&a.groupNode.shared!==!0;let i;return r?(i=s.get(n),i===void 0&&(i=new na(e,n,this.bindingsIndexes[e].group,n),s.set(n,i))):i=new na(e,n,this.bindingsIndexes[e].group,n),i}getBindGroupArray(e,t){const s=this.bindings[t];let n=s[e];return n===void 0&&(this.bindingsIndexes[e]===void 0&&(this.bindingsIndexes[e]={binding:0,group:Object.keys(this.bindingsIndexes).length}),s[e]=n=[]),n}getBindings(){let e=this.bindGroups;if(e===null){const t={},s=this.bindings;for(const n of zo)for(const r in s[n]){const i=s[n][r];(t[r]||(t[r]=[])).push(...i)}e=[];for(const n in t){const r=t[n],i=this._getBindGroup(n,r);e.push(i)}this.bindGroups=e}return e}sortBindingGroups(){const e=this.getBindings();e.sort((t,s)=>t.bindings[0].groupNode.order-s.bindings[0].groupNode.order);for(let t=0;t=0?`${Math.round(t)}u`:"0u";if(e==="bool")return t?"true":"false";if(e==="color")return`${this.getType("vec3")}( ${Ar(t.r)}, ${Ar(t.g)}, ${Ar(t.b)} )`;const s=this.getTypeLength(e),n=this.getComponentType(e),r=i=>this.generateConst(n,i);if(s===2)return`${this.getType(e)}( ${r(t.x)}, ${r(t.y)} )`;if(s===3)return`${this.getType(e)}( ${r(t.x)}, ${r(t.y)}, ${r(t.z)} )`;if(s===4)return`${this.getType(e)}( ${r(t.x)}, ${r(t.y)}, ${r(t.z)}, ${r(t.w)} )`;if(s>4&&t&&(t.isMatrix3||t.isMatrix4))return`${this.getType(e)}( ${t.elements.map(r).join(", ")} )`;if(s>4)return`${this.getType(e)}()`;throw new Error(`NodeBuilder: Type '${e}' not found in generate constant attempt.`)}getType(e){return e==="color"?"vec3":e}hasGeometryAttribute(e){return this.geometry&&this.geometry.getAttribute(e)!==void 0}getAttribute(e,t){const s=this.attributes;for(const r of s)if(r.name===e)return r;const n=new Gc(e,t);return s.push(n),n}getPropertyName(e){return e.name}isVector(e){return/vec\d/.test(e)}isMatrix(e){return/mat\d/.test(e)}isReference(e){return e==="void"||e==="property"||e==="sampler"||e==="texture"||e==="cubeTexture"||e==="storageTexture"||e==="depthTexture"||e==="texture3D"}needsToWorkingColorSpace(){return!1}getComponentTypeFromTexture(e){const t=e.type;if(e.isDataTexture){if(t===ze)return"int";if(t===Le)return"uint"}return"float"}getElementType(e){return e==="mat2"?"vec2":e==="mat3"?"vec3":e==="mat4"?"vec4":this.getComponentType(e)}getComponentType(e){if(e=this.getVectorType(e),e==="float"||e==="bool"||e==="int"||e==="uint")return e;const t=/(b|i|u|)(vec|mat)([2-4])/.exec(e);return t===null?null:t[1]==="b"?"bool":t[1]==="i"?"int":t[1]==="u"?"uint":"float"}getVectorType(e){return e==="color"?"vec3":e==="texture"||e==="cubeTexture"||e==="storageTexture"||e==="texture3D"?"vec4":e}getTypeFromLength(e,t="float"){if(e===1)return t;const s=ya(e);return(t==="float"?"":t[0])+s}getTypeFromArray(e){return Kv.get(e.constructor)}getTypeFromAttribute(e){let t=e;e.isInterleavedBufferAttribute&&(t=e.data);const s=t.array,n=e.itemSize,r=e.normalized;let i;return!(e instanceof yl)&&r!==!0&&(i=this.getTypeFromArray(s)),this.getTypeFromLength(n,i)}getTypeLength(e){const t=this.getVectorType(e),s=/vec([2-4])/.exec(t);return s!==null?Number(s[1]):t==="float"||t==="bool"||t==="int"||t==="uint"?1:/mat2/.test(e)===!0?4:/mat3/.test(e)===!0?9:/mat4/.test(e)===!0?16:0}getVectorFromMatrix(e){return e.replace("mat","vec")}changeComponentType(e,t){return this.getTypeFromLength(this.getTypeLength(e),t)}getIntegerType(e){const t=this.getComponentType(e);return t==="int"||t==="uint"?e:this.changeComponentType(e,"int")}addStack(){return this.stack=kr(this.stack),this.stacks.push(Ea()||this.stack),On(this.stack),this.stack}removeStack(){const e=this.stack;return this.stack=e.parent,On(this.stacks.pop()),e}getDataFromNode(e,t=this.shaderStage,s=null){s=s===null?e.isGlobal(this)?this.globalCache:this.cache:s;let n=s.getData(e);return n===void 0&&(n={},s.setData(e,n)),n[t]===void 0&&(n[t]={}),n[t]}getNodeProperties(e,t="any"){const s=this.getDataFromNode(e,t);return s.properties||(s.properties={outputNode:null})}getBufferAttributeFromNode(e,t){const s=this.getDataFromNode(e);let n=s.bufferAttribute;if(n===void 0){const r=this.uniforms.index++;n=new Gc("nodeAttribute"+r,t,e),this.bufferAttributes.push(n),s.bufferAttribute=n}return n}getStructTypeFromNode(e,t,s=this.shaderStage){const n=this.getDataFromNode(e,s);let r=n.structType;if(r===void 0){const i=this.structs.index++;r=new Cv("StructType"+i,t),this.structs[s].push(r),n.structType=r}return r}getUniformFromNode(e,t,s=this.shaderStage,n=null){const r=this.getDataFromNode(e,s,this.globalCache);let i=r.uniform;if(i===void 0){const a=this.uniforms.index++;i=new Sv(n||"nodeUniform"+a,t,e),this.uniforms[s].push(i),r.uniform=i}return i}getVarFromNode(e,t=null,s=e.getNodeType(this),n=this.shaderStage){const r=this.getDataFromNode(e,n);let i=r.variable;if(i===void 0){const a=this.vars[n]||(this.vars[n]=[]);t===null&&(t="nodeVar"+a.length),i=new lg(t,s),a.push(i),r.variable=i}return i}getVaryingFromNode(e,t=null,s=e.getNodeType(this)){const n=this.getDataFromNode(e,"any");let r=n.varying;if(r===void 0){const i=this.varyings,a=i.length;t===null&&(t="nodeVarying"+a),r=new vv(t,s),i.push(r),n.varying=r}return r}getCodeFromNode(e,t,s=this.shaderStage){const n=this.getDataFromNode(e);let r=n.code;if(r===void 0){const i=this.codes[s]||(this.codes[s]=[]),a=i.length;r=new Av("nodeCode"+a,t),i.push(r),n.code=r}return r}addFlowCodeHierarchy(e,t){const{flowCodes:s,flowCodeBlock:n}=this.getDataFromNode(e);let r=!0,i=t;for(;i;){if(n.get(i)===!0){r=!1;break}i=this.getDataFromNode(i).parentNodeBlock}if(r)for(const a of s)this.addLineFlowCode(a)}addLineFlowCodeBlock(e,t,s){const n=this.getDataFromNode(e),r=n.flowCodes||(n.flowCodes=[]),i=n.flowCodeBlock||(n.flowCodeBlock=new WeakMap);r.push(t),i.set(s,!0)}addLineFlowCode(e,t=null){return e===""?this:(t!==null&&this.context.nodeBlock&&this.addLineFlowCodeBlock(t,e,this.context.nodeBlock),e=this.tab+e,/;\s*$/.test(e)||(e=e+`; -`),this.flow.code+=e,this)}addFlowCode(e){return this.flow.code+=e,this}addFlowTab(){return this.tab+=" ",this}removeFlowTab(){return this.tab=this.tab.slice(0,-1),this}getFlowData(e){return this.flowsData.get(e)}flowNode(e){const t=e.getNodeType(this),s=this.flowChildNode(e,t);return this.flowsData.set(e,s),s}buildFunctionNode(e){const t=new Pf,s=this.currentFunctionNode;return this.currentFunctionNode=t,t.code=this.buildFunctionCode(e),this.currentFunctionNode=s,t}flowShaderNode(e){const t=e.layout,s={[Symbol.iterator](){let i=0;const a=Object.values(this);return{next:()=>({value:a[i],done:i++>=a.length})}}};for(const i of t.inputs)s[i.name]=new df(i.type,i.name);e.layout=null;const n=e.call(s),r=this.flowStagesNode(n,t.type);return e.layout=t,r}flowStagesNode(e,t=null){const s=this.flow,n=this.vars,r=this.cache,i=this.buildStage,a=this.stack,u={code:""};this.flow=u,this.vars={},this.cache=new to,this.stack=kr();for(const c of ko)this.setBuildStage(c),u.result=e.build(this,t);return u.vars=this.getVars(this.shaderStage),this.flow=s,this.vars=n,this.cache=r,this.stack=a,this.setBuildStage(i),u}getFunctionOperator(){return null}flowChildNode(e,t=null){const s=this.flow,n={code:""};return this.flow=n,n.result=e.build(this,t),this.flow=s,n}flowNodeFromShaderStage(e,t,s=null,n=null){const r=this.shaderStage;this.setShaderStage(e);const i=this.flowChildNode(t,s);return n!==null&&(i.code+=`${this.tab+n} = ${i.result}; -`),this.flowCode[e]=this.flowCode[e]+i.code,this.setShaderStage(r),i}getAttributesArray(){return this.attributes.concat(this.bufferAttributes)}getAttributes(){console.warn("Abstract function.")}getVaryings(){console.warn("Abstract function.")}getVar(e,t){return`${this.getType(e)} ${t}`}getVars(e){let t="";const s=this.vars[e];if(s!==void 0)for(const n of s)t+=`${this.getVar(n.type,n.name)}; `;return t}getUniforms(){console.warn("Abstract function.")}getCodes(e){const t=this.codes[e];let s="";if(t!==void 0)for(const n of t)s+=n.code+` -`;return s}getHash(){return this.vertexShader+this.fragmentShader+this.computeShader}setShaderStage(e){this.shaderStage=e}getShaderStage(){return this.shaderStage}setBuildStage(e){this.buildStage=e}getBuildStage(){return this.buildStage}buildCode(){console.warn("Abstract function.")}build(){const{object:e,material:t,renderer:s}=this;if(t!==null){let n=s.library.fromMaterial(t);n===null&&(console.error(`NodeMaterial: Material "${t.type}" is not compatible.`),n=new ce),n.build(this)}else this.addFlow("compute",e);for(const n of ko){this.setBuildStage(n),this.context.vertex&&this.context.vertex.isNode&&this.flowNodeFromShaderStage("vertex",this.context.vertex);for(const r of zo){this.setShaderStage(r);const i=this.flowNodes[r];for(const a of i)n==="generate"?this.flowNode(a):a.build(this)}}return this.setBuildStage(null),this.setShaderStage(null),this.buildCode(),this.buildUpdateNodes(),this}getNodeUniform(e,t){if(t==="float"||t==="int"||t==="uint")return new Dv(e);if(t==="vec2"||t==="ivec2"||t==="uvec2")return new Lv(e);if(t==="vec3"||t==="ivec3"||t==="uvec3")return new Iv(e);if(t==="vec4"||t==="ivec4"||t==="uvec4")return new Vv(e);if(t==="color")return new Gv(e);if(t==="mat3")return new Ov(e);if(t==="mat4")return new kv(e);throw new Error(`Uniform "${t}" not declared.`)}format(e,t,s){if(t=this.getVectorType(t),s=this.getVectorType(s),t===s||s===null||this.isReference(s))return e;const n=this.getTypeLength(t),r=this.getTypeLength(s);return n===16&&r===9?`${this.getType(s)}(${e}[0].xyz, ${e}[1].xyz, ${e}[2].xyz)`:n===9&&r===4?`${this.getType(s)}(${e}[0].xy, ${e}[1].xy)`:n>4||r>4||r===0?e:n===r?`${this.getType(s)}( ${e} )`:n>r?this.format(`${e}.${"xyz".slice(0,r)}`,this.getTypeFromLength(r,this.getComponentType(t)),s):r===4&&n>1?`${this.getType(s)}( ${this.format(e,t,"vec3")}, 1.0 )`:n===2?`${this.getType(s)}( ${this.format(e,t,"vec2")}, 0.0 )`:(n===1&&r>1&&t!==this.getComponentType(s)&&(e=`${this.getType(this.getComponentType(s))}( ${e} )`),`${this.getType(s)}( ${e} )`)}getSignature(){return`// Three.js r${xl} - Node System -`}createNodeMaterial(e="NodeMaterial"){throw new Error(`THREE.NodeBuilder: createNodeMaterial() was deprecated. Use new ${e}() instead.`)}}class Kc{constructor(){this.time=0,this.deltaTime=0,this.frameId=0,this.renderId=0,this.updateMap=new WeakMap,this.updateBeforeMap=new WeakMap,this.updateAfterMap=new WeakMap,this.renderer=null,this.material=null,this.camera=null,this.object=null,this.scene=null}_getMaps(e,t){let s=e.get(t);return s===void 0&&(s={renderMap:new WeakMap,frameMap:new WeakMap},e.set(t,s)),s}updateBeforeNode(e){const t=e.getUpdateBeforeType(),s=e.updateReference(this);if(t===W.FRAME){const{frameMap:n}=this._getMaps(this.updateBeforeMap,s);n.get(s)!==this.frameId&&e.updateBefore(this)!==!1&&n.set(s,this.frameId)}else if(t===W.RENDER){const{renderMap:n}=this._getMaps(this.updateBeforeMap,s);n.get(s)!==this.renderId&&e.updateBefore(this)!==!1&&n.set(s,this.renderId)}else t===W.OBJECT&&e.updateBefore(this)}updateAfterNode(e){const t=e.getUpdateAfterType(),s=e.updateReference(this);if(t===W.FRAME){const{frameMap:n}=this._getMaps(this.updateAfterMap,s);n.get(s)!==this.frameId&&e.updateAfter(this)!==!1&&n.set(s,this.frameId)}else if(t===W.RENDER){const{renderMap:n}=this._getMaps(this.updateAfterMap,s);n.get(s)!==this.renderId&&e.updateAfter(this)!==!1&&n.set(s,this.renderId)}else t===W.OBJECT&&e.updateAfter(this)}updateNode(e){const t=e.getUpdateType(),s=e.updateReference(this);if(t===W.FRAME){const{frameMap:n}=this._getMaps(this.updateMap,s);n.get(s)!==this.frameId&&e.update(this)!==!1&&n.set(s,this.frameId)}else if(t===W.RENDER){const{renderMap:n}=this._getMaps(this.updateMap,s);n.get(s)!==this.renderId&&e.update(this)!==!1&&n.set(s,this.renderId)}else t===W.OBJECT&&e.update(this)}update(){this.frameId++,this.lastTime===void 0&&(this.lastTime=performance.now()),this.deltaTime=(performance.now()-this.lastTime)/1e3,this.lastTime=performance.now(),this.time+=this.deltaTime}}class $u{constructor(e,t,s=null,n="",r=!1){this.type=e,this.name=t,this.count=s,this.qualifier=n,this.isConst=r}}$u.isNodeFunctionInput=!0;class Xv extends Es{static get type(){return"DirectionalLightNode"}constructor(e=null){super(e)}setup(e){super.setup(e);const t=e.context.lightingModel,s=this.colorNode,n=Pu(this.light),r=e.context.reflectedLight;t.direct({lightDirection:n,lightColor:s,reflectedLight:r},e.stack,e)}}const ao=new Ie,Rr=new Ie;let Nn=null;class Yv extends Es{static get type(){return"RectAreaLightNode"}constructor(e=null){super(e),this.halfHeight=V(new j).setGroup(k),this.halfWidth=V(new j).setGroup(k),this.updateType=W.RENDER}update(e){super.update(e);const{light:t}=this,s=e.camera.matrixWorldInverse;Rr.identity(),ao.copy(t.matrixWorld),ao.premultiply(s),Rr.extractRotation(ao),this.halfWidth.value.set(t.width*.5,0,0),this.halfHeight.value.set(0,t.height*.5,0),this.halfWidth.value.applyMatrix4(Rr),this.halfHeight.value.applyMatrix4(Rr)}setup(e){super.setup(e);let t,s;e.isAvailable("float32Filterable")?(t=K(Nn.LTC_FLOAT_1),s=K(Nn.LTC_FLOAT_2)):(t=K(Nn.LTC_HALF_1),s=K(Nn.LTC_HALF_2));const{colorNode:n,light:r}=this,i=e.context.lightingModel,a=Pi(r),u=e.context.reflectedLight;i.directRectArea({lightColor:n,lightPosition:a,halfWidth:this.halfWidth,halfHeight:this.halfHeight,reflectedLight:u,ltc_1:t,ltc_2:s},e.stack,e)}static setLTC(e){Nn=e}}class hg extends Es{static get type(){return"SpotLightNode"}constructor(e=null){super(e),this.coneCosNode=V(0).setGroup(k),this.penumbraCosNode=V(0).setGroup(k),this.cutoffDistanceNode=V(0).setGroup(k),this.decayExponentNode=V(0).setGroup(k)}update(e){super.update(e);const{light:t}=this;this.coneCosNode.value=Math.cos(t.angle),this.penumbraCosNode.value=Math.cos(t.angle*(1-t.penumbra)),this.cutoffDistanceNode.value=t.distance,this.decayExponentNode.value=t.decay}getSpotAttenuation(e){const{coneCosNode:t,penumbraCosNode:s}=this;return ct(t,s,e)}setup(e){super.setup(e);const t=e.context.lightingModel,{colorNode:s,cutoffDistanceNode:n,decayExponentNode:r,light:i}=this,a=Pi(i).sub(xe),u=a.normalize(),c=u.dot(Pu(i)),l=this.getSpotAttenuation(c),d=a.length(),h=Gu({lightDistance:d,cutoffDistance:n,decayExponent:r});let p=s.mul(l).mul(h);if(i.map){const m=Vf(i),x=K(i.map,m.xy).onRenderUpdate(()=>i.map);p=m.mul(2).sub(1).abs().lessThan(1).all().select(p.mul(x),p)}const f=e.context.reflectedLight;t.direct({lightDirection:u,lightColor:p,reflectedLight:f},e.stack,e)}}class jv extends hg{static get type(){return"IESSpotLightNode"}getSpotAttenuation(e){const t=this.light.iesMap;let s=null;if(t&&t.isTexture===!0){const n=e.acos().mul(1/Math.PI);s=K(t,M(n,0),0).r}else s=super.getSpotAttenuation(e);return s}}class Qv extends Es{static get type(){return"AmbientLightNode"}constructor(e=null){super(e)}setup({context:e}){e.irradiance.addAssign(this.colorNode)}}class Zv extends Es{static get type(){return"HemisphereLightNode"}constructor(e=null){super(e),this.lightPositionNode=Uu(e),this.lightDirectionNode=this.lightPositionNode.normalize(),this.groundColorNode=V(new mt).setGroup(k)}update(e){const{light:t}=this;super.update(e),this.lightPositionNode.object3d=t,this.groundColorNode.value.copy(t.groundColor).multiplyScalar(t.intensity)}setup(e){const{colorNode:t,groundColorNode:s,lightDirectionNode:n}=this,i=lt.dot(n).mul(.5).add(.5),a=Q(s,t,i);e.context.irradiance.addAssign(a)}}class Jv extends Es{static get type(){return"LightProbeNode"}constructor(e=null){super(e);const t=[];for(let s=0;s<9;s++)t.push(new j);this.lightProbe=Gt(t)}update(e){const{light:t}=this;super.update(e);for(let s=0;s<9;s++)this.lightProbe.array[s].copy(t.sh.coefficients[s]).multiplyScalar(t.intensity)}setup(e){const t=cg(Si,this.lightProbe);e.context.irradiance.addAssign(t)}}class pg{parseFunction(){console.warn("Abstract function.")}}class Hu{constructor(e,t,s="",n=""){this.type=e,this.inputs=t,this.name=s,this.precision=n}getCode(){console.warn("Abstract function.")}}Hu.isNodeFunction=!0;const eA=/^\s*(highp|mediump|lowp)?\s*([a-z_0-9]+)\s*([a-z_0-9]+)?\s*\(([\s\S]*?)\)/i,tA=/[a-z_0-9]+/ig,Xc="#pragma main",sA=o=>{o=o.trim();const e=o.indexOf(Xc),t=e!==-1?o.slice(e+Xc.length):o,s=t.match(eA);if(s!==null&&s.length===5){const n=s[4],r=[];let i=null;for(;(i=tA.exec(n))!==null;)r.push(i);const a=[];let u=0;for(;u0||e.backgroundBlurriness>0&&t.backgroundBlurriness===0;if(t.background!==s||n){const r=this.getCacheNode("background",s,()=>{if(s.isCubeTexture===!0||s.mapping===jn||s.mapping===Qn||s.mapping===bo){if(e.backgroundBlurriness>0||s.mapping===bo)return vu(s);{let i;return s.isCubeTexture===!0?i=on(s):i=K(s),Wp(i)}}else{if(s.isTexture===!0)return K(s,Bt.flipY()).setUpdateMatrix(!0);s.isColor!==!0&&console.error("WebGPUNodes: Unsupported background configuration.",s)}},n);t.backgroundNode=r,t.background=s,t.backgroundBlurriness=e.backgroundBlurriness}}else t.backgroundNode&&(delete t.backgroundNode,delete t.background)}getCacheNode(e,t,s,n=!1){const r=this.cacheLib[e]||(this.cacheLib[e]=new WeakMap);let i=r.get(t);return(i===void 0||n)&&(i=s(),r.set(t,i)),i}updateFog(e){const t=this.get(e),s=e.fog;if(s){if(t.fog!==s){const n=this.getCacheNode("fog",s,()=>{if(s.isFogExp2){const r=ne("color","color",s).setGroup(k),i=ne("density","float",s).setGroup(k);return Xn(r,Mu(i))}else if(s.isFog){const r=ne("color","color",s).setGroup(k),i=ne("near","float",s).setGroup(k),a=ne("far","float",s).setGroup(k);return Xn(r,wu(i,a))}else console.error("THREE.Renderer: Unsupported fog configuration.",s)});t.fogNode=n,t.fog=s}}else delete t.fogNode,delete t.fog}updateEnvironment(e){const t=this.get(e),s=e.environment;if(s){if(t.environment!==s){const n=this.getCacheNode("environment",s,()=>{if(s.isCubeTexture===!0)return on(s);if(s.isTexture===!0)return K(s);console.error("Nodes: Unsupported environment configuration.",s)});t.environmentNode=n,t.environment=s}}else t.environmentNode&&(delete t.environmentNode,delete t.environment)}getNodeFrame(e=this.renderer,t=null,s=null,n=null,r=null){const i=this.nodeFrame;return i.renderer=e,i.scene=t,i.object=s,i.camera=n,i.material=r,i}getNodeFrameForRender(e){return this.getNodeFrame(e.renderer,e.scene,e.object,e.camera,e.material)}getOutputCacheKey(){const e=this.renderer;return e.toneMapping+","+e.currentColorSpace}hasOutputChange(e){return Yc.get(e)!==this.getOutputCacheKey()}getOutputNode(e){const t=this.renderer,s=this.getOutputCacheKey(),n=K(e,Bt).renderOutput(t.toneMapping,t.currentColorSpace);return Yc.set(e,s),n}updateBefore(e){const t=e.getNodeBuilderState();for(const s of t.updateBeforeNodes)this.getNodeFrameForRender(e).updateBeforeNode(s)}updateAfter(e){const t=e.getNodeBuilderState();for(const s of t.updateAfterNodes)this.getNodeFrameForRender(e).updateAfterNode(s)}updateForCompute(e){const t=this.getNodeFrame(),s=this.getForCompute(e);for(const n of s.updateNodes)t.updateNode(n)}updateForRender(e){const t=this.getNodeFrameForRender(e),s=e.getNodeBuilderState();for(const n of s.updateNodes)t.updateNode(n)}needsRefresh(e){const t=this.getNodeFrameForRender(e);return e.getMonitor().needsRefresh(e,t)}dispose(){super.dispose(),this.nodeFrame=new Kc,this.nodeBuilderCache=new Map,this.cacheLib={}}}const uo=new vl;class ii{constructor(e=null){this.version=0,this.clipIntersection=null,this.cacheKey="",this.shadowPass=!1,this.viewNormalMatrix=new Yn,this.clippingGroupContexts=new WeakMap,this.intersectionPlanes=[],this.unionPlanes=[],this.parentVersion=null,e!==null&&(this.viewNormalMatrix=e.viewNormalMatrix,this.clippingGroupContexts=e.clippingGroupContexts,this.shadowPass=e.shadowPass,this.viewMatrix=e.viewMatrix)}projectPlanes(e,t,s){const n=e.length;for(let r=0;r{await this.compileAsync(d,h);const f=this._renderLists.get(d,h),m=this._renderContexts.get(d,h,this._renderTarget),x=d.overrideMaterial||p.material,N=this._objects.get(p,x,d,h,f.lightsNode,m,m.clippingContext),{fragmentShader:v,vertexShader:w}=N.getNodeBuilderState();return{fragmentShader:v,vertexShader:w}}}}async init(){if(this._initialized)throw new Error("Renderer: Backend has already been initialized.");return this._initPromise!==null?this._initPromise:(this._initPromise=new Promise(async(e,t)=>{let s=this.backend;try{await s.init(this)}catch(n){if(this._getFallback!==null)try{this.backend=s=this._getFallback(n),await s.init(this)}catch(r){t(r);return}else{t(n);return}}this._nodes=new iA(this,s),this._animation=new g_(this._nodes,this.info),this._attributes=new N_(s),this._background=new _v(this,this._nodes),this._geometries=new v_(this._attributes,this.info),this._textures=new V_(this,s,this.info),this._pipelines=new w_(s,this._nodes),this._bindings=new M_(s,this._nodes,this._textures,this._attributes,this._pipelines,this.info),this._objects=new T_(this,this._nodes,this._geometries,this._pipelines,this._bindings,this.info),this._renderLists=new U_(this.lighting),this._bundles=new aA,this._renderContexts=new L_,this._animation.start(),this._initialized=!0,e()}),this._initPromise)}get coordinateSystem(){return this.backend.coordinateSystem}async compileAsync(e,t,s=null){if(this._isDeviceLost===!0)return;this._initialized===!1&&await this.init();const n=this._nodes.nodeFrame,r=n.renderId,i=this._currentRenderContext,a=this._currentRenderObjectFunction,u=this._compilationPromises,c=e.isScene===!0?e:jc;s===null&&(s=e);const l=this._renderTarget,d=this._renderContexts.get(s,t,l),h=this._activeMipmapLevel,p=[];this._currentRenderContext=d,this._currentRenderObjectFunction=this.renderObject,this._handleObjectFunction=this._createObjectPipeline,this._compilationPromises=p,n.renderId++,n.update(),d.depth=this.depth,d.stencil=this.stencil,d.clippingContext||(d.clippingContext=new ii),d.clippingContext.updateGlobal(c,t),c.onBeforeRender(this,e,t,l);const f=this._renderLists.get(e,t);if(f.begin(),this._projectObject(e,t,0,f,d.clippingContext),s!==e&&s.traverseVisible(function(w){w.isLight&&w.layers.test(t.layers)&&f.pushLight(w)}),f.finish(),l!==null){this._textures.updateRenderTarget(l,h);const w=this._textures.get(l);d.textures=w.textures,d.depthTexture=w.depthTexture}else d.textures=null,d.depthTexture=null;this._background.update(c,f,d);const m=f.opaque,x=f.transparent,N=f.transparentDoublePass,v=f.lightsNode;this.opaque===!0&&m.length>0&&this._renderObjects(m,t,c,v),this.transparent===!0&&x.length>0&&this._renderTransparents(x,N,t,c,v),n.renderId=r,this._currentRenderContext=i,this._currentRenderObjectFunction=a,this._compilationPromises=u,this._handleObjectFunction=this._renderObjectDirect,await Promise.all(p)}async renderAsync(e,t){this._initialized===!1&&await this.init();const s=this._renderScene(e,t);await this.backend.resolveTimestampAsync(s,"render")}async waitForGPU(){await this.backend.waitForGPU()}setMRT(e){return this._mrt=e,this}getMRT(){return this._mrt}_onDeviceLost(e){let t=`THREE.WebGPURenderer: ${e.api} Device Lost: - -Message: ${e.message}`;e.reason&&(t+=` -Reason: ${e.reason}`),console.error(t),this._isDeviceLost=!0}_renderBundle(e,t,s){const{bundleGroup:n,camera:r,renderList:i}=e,a=this._currentRenderContext,u=this._bundles.get(n,r),c=this.backend.get(u);c.renderContexts===void 0&&(c.renderContexts=new Set);const l=n.version!==c.version,d=c.renderContexts.has(a)===!1||l;if(c.renderContexts.add(a),d){this.backend.beginBundle(a),(c.renderObjects===void 0||l)&&(c.renderObjects=[]),this._currentRenderBundle=u;const h=i.opaque;this.opaque===!0&&h.length>0&&this._renderObjects(h,r,t,s),this._currentRenderBundle=null,this.backend.finishBundle(a,u),c.version=n.version}else{const{renderObjects:h}=c;for(let p=0,f=h.length;p>=h,f.viewportValue.height>>=h,f.viewportValue.minDepth=w,f.viewportValue.maxDepth=B,f.viewport=f.viewportValue.equals(co)===!1,f.scissorValue.copy(N).multiplyScalar(v).floor(),f.scissor=this._scissorTest&&f.scissorValue.equals(co)===!1,f.scissorValue.width>>=h,f.scissorValue.height>>=h,f.clippingContext||(f.clippingContext=new ii),f.clippingContext.updateGlobal(c,t),c.onBeforeRender(this,e,t,p),Er.multiplyMatrices(t.projectionMatrix,t.matrixWorldInverse),lo.setFromProjectionMatrix(Er,m);const F=this._renderLists.get(e,t);if(F.begin(),this._projectObject(e,t,0,F,f.clippingContext),F.finish(),this.sortObjects===!0&&F.sort(this._opaqueSort,this._transparentSort),p!==null){this._textures.updateRenderTarget(p,h);const Z=this._textures.get(p);f.textures=Z.textures,f.depthTexture=Z.depthTexture,f.width=Z.width,f.height=Z.height,f.renderTarget=p,f.depth=p.depthBuffer,f.stencil=p.stencilBuffer}else f.textures=null,f.depthTexture=null,f.width=this.domElement.width,f.height=this.domElement.height,f.depth=this.depth,f.stencil=this.stencil;f.width>>=h,f.height>>=h,f.activeCubeFace=d,f.activeMipmapLevel=h,f.occlusionQueryCount=F.occlusionQueryCount,this._background.update(c,F,f),this.backend.beginRender(f);const{bundles:L,lightsNode:I,transparentDoublePass:G,transparent:X,opaque:ie}=F;if(L.length>0&&this._renderBundles(L,c,I),this.opaque===!0&&ie.length>0&&this._renderObjects(ie,t,c,I),this.transparent===!0&&X.length>0&&this._renderTransparents(X,G,t,c,I),this.backend.finishRender(f),r.renderId=i,this._currentRenderContext=a,this._currentRenderObjectFunction=u,n!==null){this.setRenderTarget(l,d,h);const Z=this._quad;this._nodes.hasOutputChange(p.texture)&&(Z.material.fragmentNode=this._nodes.getOutputNode(p.texture),Z.material.needsUpdate=!0),this._renderScene(Z,Z.camera,!1)}return c.onAfterRender(this,e,t,p),f}getMaxAnisotropy(){return this.backend.getMaxAnisotropy()}getActiveCubeFace(){return this._activeCubeFace}getActiveMipmapLevel(){return this._activeMipmapLevel}async setAnimationLoop(e){this._initialized===!1&&await this.init(),this._animation.setAnimationLoop(e)}async getArrayBufferAsync(e){return await this.backend.getArrayBufferAsync(e)}getContext(){return this.backend.getContext()}getPixelRatio(){return this._pixelRatio}getDrawingBufferSize(e){return e.set(this._width*this._pixelRatio,this._height*this._pixelRatio).floor()}getSize(e){return e.set(this._width,this._height)}setPixelRatio(e=1){this._pixelRatio!==e&&(this._pixelRatio=e,this.setSize(this._width,this._height,!1))}setDrawingBufferSize(e,t,s){this._width=e,this._height=t,this._pixelRatio=s,this.domElement.width=Math.floor(e*s),this.domElement.height=Math.floor(t*s),this.setViewport(0,0,e,t),this._initialized&&this.backend.updateSize()}setSize(e,t,s=!0){this._width=e,this._height=t,this.domElement.width=Math.floor(e*this._pixelRatio),this.domElement.height=Math.floor(t*this._pixelRatio),s===!0&&(this.domElement.style.width=e+"px",this.domElement.style.height=t+"px"),this.setViewport(0,0,e,t),this._initialized&&this.backend.updateSize()}setOpaqueSort(e){this._opaqueSort=e}setTransparentSort(e){this._transparentSort=e}getScissor(e){const t=this._scissor;return e.x=t.x,e.y=t.y,e.width=t.width,e.height=t.height,e}setScissor(e,t,s,n){const r=this._scissor;e.isVector4?r.copy(e):r.set(e,t,s,n)}getScissorTest(){return this._scissorTest}setScissorTest(e){this._scissorTest=e,this.backend.setScissorTest(e)}getViewport(e){return e.copy(this._viewport)}setViewport(e,t,s,n,r=0,i=1){const a=this._viewport;e.isVector4?a.copy(e):a.set(e,t,s,n),a.minDepth=r,a.maxDepth=i}getClearColor(e){return e.copy(this._clearColor)}setClearColor(e,t=1){this._clearColor.set(e),this._clearColor.a=t}getClearAlpha(){return this._clearColor.a}setClearAlpha(e){this._clearColor.a=e}getClearDepth(){return this._clearDepth}setClearDepth(e){this._clearDepth=e}getClearStencil(){return this._clearStencil}setClearStencil(e){this._clearStencil=e}isOccluded(e){const t=this._currentRenderContext;return t&&this.backend.isOccluded(t,e)}clear(e=!0,t=!0,s=!0){if(this._initialized===!1)return console.warn("THREE.Renderer: .clear() called before the backend is initialized. Try using .clearAsync() instead."),this.clearAsync(e,t,s);const n=this._renderTarget||this._getFrameBufferTarget();let r=null;if(n!==null){this._textures.updateRenderTarget(n);const i=this._textures.get(n);r=this._renderContexts.get(null,null,n),r.textures=i.textures,r.depthTexture=i.depthTexture,r.width=i.width,r.height=i.height,r.renderTarget=n,r.depth=n.depthBuffer,r.stencil=n.stencilBuffer}if(this.backend.clear(e,t,s,r),n!==null&&this._renderTarget===null){const i=this._quad;this._nodes.hasOutputChange(n.texture)&&(i.material.fragmentNode=this._nodes.getOutputNode(n.texture),i.material.needsUpdate=!0),this._renderScene(i,i.camera,!1)}}clearColor(){return this.clear(!0,!1,!1)}clearDepth(){return this.clear(!1,!0,!1)}clearStencil(){return this.clear(!1,!1,!0)}async clearAsync(e=!0,t=!0,s=!0){this._initialized===!1&&await this.init(),this.clear(e,t,s)}async clearColorAsync(){this.clearAsync(!0,!1,!1)}async clearDepthAsync(){this.clearAsync(!1,!0,!1)}async clearStencilAsync(){this.clearAsync(!1,!1,!0)}get currentToneMapping(){return this._renderTarget!==null?ss:this.toneMapping}get currentColorSpace(){return this._renderTarget!==null?kt:this.outputColorSpace}dispose(){this.info.dispose(),this.backend.dispose(),this._animation.dispose(),this._objects.dispose(),this._pipelines.dispose(),this._nodes.dispose(),this._bindings.dispose(),this._renderLists.dispose(),this._renderContexts.dispose(),this._textures.dispose(),this.setRenderTarget(null),this.setAnimationLoop(null)}setRenderTarget(e,t=0,s=0){this._renderTarget=e,this._activeCubeFace=t,this._activeMipmapLevel=s}getRenderTarget(){return this._renderTarget}setRenderObjectFunction(e){this._renderObjectFunction=e}getRenderObjectFunction(){return this._renderObjectFunction}compute(e){if(this.isDeviceLost===!0)return;if(this._initialized===!1)return console.warn("THREE.Renderer: .compute() called before the backend is initialized. Try using .computeAsync() instead."),this.computeAsync(e);const t=this._nodes.nodeFrame,s=t.renderId;this.info.calls++,this.info.compute.calls++,this.info.compute.frameCalls++,t.renderId=this.info.calls;const n=this.backend,r=this._pipelines,i=this._bindings,a=this._nodes,u=Array.isArray(e)?e:[e];if(u[0]===void 0||u[0].isComputeNode!==!0)throw new Error("THREE.Renderer: .compute() expects a ComputeNode.");n.beginCompute(e);for(const c of u){if(r.has(c)===!1){const h=()=>{c.removeEventListener("dispose",h),r.delete(c),i.delete(c),a.delete(c)};c.addEventListener("dispose",h);const p=c.onInitFunction;p!==null&&p.call(c,{renderer:this})}a.updateForCompute(c),i.updateForCompute(c);const l=i.getForCompute(c),d=r.getForCompute(c,l);n.compute(e,c,l,d)}n.finishCompute(e),t.renderId=s}async computeAsync(e){this._initialized===!1&&await this.init(),this.compute(e),await this.backend.resolveTimestampAsync(e,"compute")}async hasFeatureAsync(e){return this._initialized===!1&&await this.init(),this.backend.hasFeature(e)}hasFeature(e){return this._initialized===!1?(console.warn("THREE.Renderer: .hasFeature() called before the backend is initialized. Try using .hasFeatureAsync() instead."),!1):this.backend.hasFeature(e)}hasInitialized(){return this._initialized}async initTextureAsync(e){this._initialized===!1&&await this.init(),this._textures.updateTexture(e)}initTexture(e){this._initialized===!1&&console.warn("THREE.Renderer: .initTexture() called before the backend is initialized. Try using .initTextureAsync() instead."),this._textures.updateTexture(e)}copyFramebufferToTexture(e,t=null){if(t!==null)if(t.isVector2)t=jt.set(t.x,t.y,e.image.width,e.image.height).floor();else if(t.isVector4)t=jt.copy(t).floor();else{console.error("THREE.Renderer.copyFramebufferToTexture: Invalid rectangle.");return}else t=jt.set(0,0,e.image.width,e.image.height);let s=this._currentRenderContext,n;s!==null?n=s.renderTarget:(n=this._renderTarget||this._getFrameBufferTarget(),n!==null&&(this._textures.updateRenderTarget(n),s=this._textures.get(n))),this._textures.updateTexture(e,{renderTarget:n}),this.backend.copyFramebufferToTexture(e,s,t)}copyTextureToTexture(e,t,s=null,n=null,r=0){this._textures.updateTexture(e),this._textures.updateTexture(t),this.backend.copyTextureToTexture(e,t,s,n,r)}async readRenderTargetPixelsAsync(e,t,s,n,r,i=0,a=0){return this.backend.copyTextureToBuffer(e.textures[i],t,s,n,r,a)}_projectObject(e,t,s,n,r){if(e.visible===!1)return;if(e.layers.test(t.layers)){if(e.isGroup)s=e.renderOrder,e.isClippingGroup&&e.enabled&&(r=r.getGroupContext(e));else if(e.isLOD)e.autoUpdate===!0&&e.update(t);else if(e.isLight)n.pushLight(e);else if(e.isSprite){if(!e.frustumCulled||lo.intersectsSprite(e)){this.sortObjects===!0&&jt.setFromMatrixPosition(e.matrixWorld).applyMatrix4(Er);const{geometry:u,material:c}=e;c.visible&&n.push(e,u,c,s,jt.z,null,r)}}else if(e.isLineLoop)console.error("THREE.Renderer: Objects of type THREE.LineLoop are not supported. Please use THREE.Line or THREE.LineSegments.");else if((e.isMesh||e.isLine||e.isPoints)&&(!e.frustumCulled||lo.intersectsObject(e))){const{geometry:u,material:c}=e;if(this.sortObjects===!0&&(u.boundingSphere===null&&u.computeBoundingSphere(),jt.copy(u.boundingSphere.center).applyMatrix4(e.matrixWorld).applyMatrix4(Er)),Array.isArray(c)){const l=u.groups;for(let d=0,h=l.length;d0){for(const{material:i}of t)i.side=Ct;this._renderObjects(t,s,n,r,"backSide");for(const{material:i}of t)i.side=No;this._renderObjects(e,s,n,r);for(const{material:i}of t)i.side=js}else this._renderObjects(e,s,n,r)}_renderObjects(e,t,s,n,r=null){for(let i=0,a=e.length;i0,p.isShadowNodeMaterial&&(p.side=r.shadowSide===null?r.side:r.shadowSide,r.depthNode&&r.depthNode.isNode&&(h=p.depthNode,p.depthNode=r.depthNode),r.castShadowNode&&r.castShadowNode.isNode&&(d=p.colorNode,p.colorNode=r.castShadowNode)),r=p}r.transparent===!0&&r.side===js&&r.forceSinglePass===!1?(r.side=Ct,this._handleObjectFunction(e,r,t,s,a,i,u,"backSide"),r.side=No,this._handleObjectFunction(e,r,t,s,a,i,u,c),r.side=js):this._handleObjectFunction(e,r,t,s,a,i,u,c),l!==void 0&&(t.overrideMaterial.positionNode=l),h!==void 0&&(t.overrideMaterial.depthNode=h),d!==void 0&&(t.overrideMaterial.colorNode=d),e.onAfterRender(this,t,s,n,r,i)}_renderObjectDirect(e,t,s,n,r,i,a,u){const c=this._objects.get(e,t,s,n,r,this._currentRenderContext,a,u);c.drawRange=e.geometry.drawRange,c.group=i;const l=this._nodes.needsRefresh(c);l&&(this._nodes.updateBefore(c),this._geometries.updateForRender(c),this._nodes.updateForRender(c),this._bindings.updateForRender(c)),this._pipelines.updateForRender(c),this._currentRenderBundle!==null&&(this.backend.get(this._currentRenderBundle).renderObjects.push(c),c.bundle=this._currentRenderBundle.bundleGroup),this.backend.draw(c,this.info),l&&this._nodes.updateAfter(c)}_createObjectPipeline(e,t,s,n,r,i,a,u){const c=this._objects.get(e,t,s,n,r,this._currentRenderContext,a,u);c.drawRange=e.geometry.drawRange,c.group=i,this._nodes.updateBefore(c),this._geometries.updateForRender(c),this._nodes.updateForRender(c),this._bindings.updateForRender(c),this._pipelines.getForRender(c,this._compilationPromises),this._nodes.updateAfter(c)}get compile(){return this.compileAsync}}class qu{constructor(e=""){this.name=e,this.visibility=0}setVisibility(e){this.visibility|=e}clone(){return Object.assign(new this.constructor,this)}}function dA(o){return o+(ts-o%ts)%ts}class gg extends qu{constructor(e,t=null){super(e),this.isBuffer=!0,this.bytesPerElement=Float32Array.BYTES_PER_ELEMENT,this._buffer=t}get byteLength(){return dA(this._buffer.byteLength)}get buffer(){return this._buffer}update(){return!0}}class mg extends gg{constructor(e,t=null){super(e,t),this.isUniformBuffer=!0}}let hA=0;class yg extends mg{constructor(e,t){super("UniformBuffer_"+hA++,e?e.value:null),this.nodeUniform=e,this.groupNode=t}get buffer(){return this.nodeUniform.value}}class pA extends mg{constructor(e){super(e),this.isUniformsGroup=!0,this._values=null,this.uniforms=[]}addUniform(e){return this.uniforms.push(e),this}removeUniform(e){const t=this.uniforms.indexOf(e);return t!==-1&&this.uniforms.splice(t,1),this}get values(){return this._values===null&&(this._values=Array.from(this.buffer)),this._values}get buffer(){let e=this._buffer;if(e===null){const t=this.byteLength;e=new Float32Array(new ArrayBuffer(t)),this._buffer=e}return e}get byteLength(){let e=0;for(let t=0,s=this.uniforms.length;t0?h:"";a=`${l.name} { - ${d} ${i.name}[${p}]; -}; -`}else a=`${this.getVectorType(i.type)} ${this.getPropertyName(i,e)};`,u=!0;const c=i.node.precision;if(c!==null&&(a=_A[c]+" "+a),u){a=" "+a;const l=i.groupNode.name;(n[l]||(n[l]=[])).push(a)}else a="uniform "+a,s.push(a)}let r="";for(const i in n){const a=n[i];r+=this._getGLSLUniformStruct(e+"_"+i,a.join(` -`))+` -`}return r+=s.join(` -`),r}getTypeFromAttribute(e){let t=super.getTypeFromAttribute(e);if(/^[iu]/.test(t)&&e.gpuType!==ze){let s=e;e.isInterleavedBufferAttribute&&(s=e.data);const n=s.array;n instanceof Uint32Array||n instanceof Int32Array||(t=t.slice(1))}return t}getAttributes(e){let t="";if(e==="vertex"||e==="compute"){const s=this.getAttributesArray();let n=0;for(const r of s)t+=`layout( location = ${n++} ) in ${r.type} ${r.name}; -`}return t}getStructMembers(e){const t=[],s=e.getMemberTypes();for(let n=0;ns*n,1)}u`}getDrawIndex(){return this.renderer.backend.extensions.has("WEBGL_multi_draw")?"uint( gl_DrawID )":null}getFrontFacing(){return"gl_FrontFacing"}getFragCoord(){return"gl_FragCoord.xy"}getFragDepth(){return"gl_FragDepth"}enableExtension(e,t,s=this.shaderStage){const n=this.extensions[s]||(this.extensions[s]=new Map);n.has(e)===!1&&n.set(e,{name:e,behavior:t})}getExtensions(e){const t=[];if(e==="vertex"){const n=this.renderer.backend.extensions;this.object.isBatchedMesh&&n.has("WEBGL_multi_draw")&&this.enableExtension("GL_ANGLE_multi_draw","require",e)}const s=this.extensions[e];if(s!==void 0)for(const{name:n,behavior:r}of s.values())t.push(`#extension ${n} : ${r}`);return t.join(` -`)}getClipDistance(){return"gl_ClipDistance"}isAvailable(e){let t=Qc[e];if(t===void 0){let s;switch(t=!1,e){case"float32Filterable":s="OES_texture_float_linear";break;case"clipDistance":s="WEBGL_clip_cull_distance";break}if(s!==void 0){const n=this.renderer.backend.extensions;n.has(s)&&(n.get(s),t=!0)}Qc[e]=t}return t}isFlipY(){return!0}enableHardwareClipping(e){this.enableExtension("GL_ANGLE_clip_cull_distance","require"),this.builtins.vertex.push(`out float gl_ClipDistance[ ${e} ]`)}registerTransform(e,t){this.transforms.push({varyingName:e,attributeNode:t})}getTransforms(){const e=this.transforms;let t="";for(let s=0;s0&&(s+=` -`),s+=` // flow -> ${c} - `),s+=`${u.code} - `,a===r&&t!=="compute"&&(s+=`// result - `,t==="vertex"?(s+="gl_Position = ",s+=`${u.result};`):t==="fragment"&&(a.outputNode.isOutputStructNode||(s+="fragColor = ",s+=`${u.result};`)))}const i=e[t];i.extensions=this.getExtensions(t),i.uniforms=this.getUniforms(t),i.attributes=this.getAttributes(t),i.varyings=this.getVaryings(t),i.vars=this.getVars(t),i.structs=this.getStructs(t),i.codes=this.getCodes(t),i.transforms=this.getTransforms(t),i.flow=s}this.material!==null?(this.vertexShader=this._getGLSLVertexCode(e.vertex),this.fragmentShader=this._getGLSLFragmentCode(e.fragment)):this.computeShader=this._getGLSLVertexCode(e.compute)}getUniformFromNode(e,t,s,n=null){const r=super.getUniformFromNode(e,t,s,n),i=this.getDataFromNode(e,s,this.globalCache);let a=i.uniformGPU;if(a===void 0){const u=e.groupNode,c=u.name,l=this.getBindGroupArray(c,s);if(t==="texture")a=new Di(r.name,r.node,u),l.push(a);else if(t==="cubeTexture")a=new Tg(r.name,r.node,u),l.push(a);else if(t==="texture3D")a=new _g(r.name,r.node,u),l.push(a);else if(t==="buffer"){e.name=`NodeBuffer_${e.id}`,r.name=`buffer${e.id}`;const d=new yg(e,u);d.name=e.name,l.push(d),a=d}else{const d=this.uniformGroups[s]||(this.uniformGroups[s]={});let h=d[c];h===void 0&&(h=new xg(s+"_"+c,u),d[c]=h,l.push(h)),a=this.getNodeUniform(r,t),h.addUniform(a)}i.uniformGPU=a}return r}}let ho=null,Ls=null;class bg{constructor(e={}){this.parameters=Object.assign({},e),this.data=new WeakMap,this.renderer=null,this.domElement=null}async init(e){this.renderer=e}get coordinateSystem(){}beginRender(){}finishRender(){}beginCompute(){}finishCompute(){}draw(){}compute(){}createProgram(){}destroyProgram(){}createBindings(){}updateBindings(){}updateBinding(){}createRenderPipeline(){}createComputePipeline(){}needsRenderUpdate(){}getRenderCacheKey(){}createNodeBuilder(){}createSampler(){}destroySampler(){}createDefaultTexture(){}createTexture(){}updateTexture(){}generateMipmaps(){}destroyTexture(){}copyTextureToBuffer(){}copyTextureToTexture(){}copyFramebufferToTexture(){}createAttribute(){}createIndexAttribute(){}createStorageAttribute(){}updateAttribute(){}destroyAttribute(){}getContext(){}updateSize(){}updateViewport(){}isOccluded(){}async resolveTimestampAsync(){}async waitForGPU(){}async hasFeatureAsync(){}hasFeature(){}getMaxAnisotropy(){}getDrawingBufferSize(){return ho=ho||new Ye,this.renderer.getDrawingBufferSize(ho)}setScissorTest(){}getClearColor(){const e=this.renderer;return Ls=Ls||new Ru,e.getClearColor(Ls),Ls.getRGB(Ls,this.renderer.currentColorSpace),Ls}getDomElement(){let e=this.domElement;return e===null&&(e=this.parameters.canvas!==void 0?this.parameters.canvas:ym(),"setAttribute"in e&&e.setAttribute("data-engine",`three.js r${xl} webgpu`),this.domElement=e),e}set(e,t){this.data.set(e,t)}get(e){let t=this.data.get(e);return t===void 0&&(t={},this.data.set(e,t)),t}has(e){return this.data.has(e)}delete(e){this.data.delete(e)}dispose(){}}let NA=0;class SA{constructor(e,t){this.buffers=[e.bufferGPU,t],this.type=e.type,this.bufferType=e.bufferType,this.pbo=e.pbo,this.byteLength=e.byteLength,this.bytesPerElement=e.BYTES_PER_ELEMENT,this.version=e.version,this.isInteger=e.isInteger,this.activeBufferIndex=0,this.baseId=e.id}get id(){return`${this.baseId}|${this.activeBufferIndex}`}get bufferGPU(){return this.buffers[this.activeBufferIndex]}get transformBuffer(){return this.buffers[this.activeBufferIndex^1]}switchBuffers(){this.activeBufferIndex^=1}}class vA{constructor(e){this.backend=e}createAttribute(e,t){const s=this.backend,{gl:n}=s,r=e.array,i=e.usage||n.STATIC_DRAW,a=e.isInterleavedBufferAttribute?e.data:e,u=s.get(a);let c=u.bufferGPU;c===void 0&&(c=this._createBuffer(n,t,r,i),u.bufferGPU=c,u.bufferType=t,u.version=a.version);let l;if(r instanceof Float32Array)l=n.FLOAT;else if(r instanceof Uint16Array)e.isFloat16BufferAttribute?l=n.HALF_FLOAT:l=n.UNSIGNED_SHORT;else if(r instanceof Int16Array)l=n.SHORT;else if(r instanceof Uint32Array)l=n.UNSIGNED_INT;else if(r instanceof Int32Array)l=n.INT;else if(r instanceof Int8Array)l=n.BYTE;else if(r instanceof Uint8Array)l=n.UNSIGNED_BYTE;else if(r instanceof Uint8ClampedArray)l=n.UNSIGNED_BYTE;else throw new Error("THREE.WebGLBackend: Unsupported buffer data format: "+r);let d={bufferGPU:c,bufferType:t,type:l,byteLength:r.byteLength,bytesPerElement:r.BYTES_PER_ELEMENT,version:e.version,pbo:e.pbo,isInteger:l===n.INT||l===n.UNSIGNED_INT||e.gpuType===ze,id:NA++};if(e.isStorageBufferAttribute||e.isStorageInstancedBufferAttribute){const h=this._createBuffer(n,t,r,i);d=new SA(d,h)}s.set(e,d)}updateAttribute(e){const t=this.backend,{gl:s}=t,n=e.array,r=e.isInterleavedBufferAttribute?e.data:e,i=t.get(r),a=i.bufferType,u=e.isInterleavedBufferAttribute?e.data.updateRanges:e.updateRanges;if(s.bindBuffer(a,i.bufferGPU),u.length===0)s.bufferSubData(a,0,n);else{for(let c=0,l=u.length;c1?this.enable(n.SAMPLE_ALPHA_TO_COVERAGE):this.disable(n.SAMPLE_ALPHA_TO_COVERAGE),s>0&&this.currentClippingPlanes!==s)for(let u=0;u<8;u++)u{function r(){const i=e.clientWaitSync(t,e.SYNC_FLUSH_COMMANDS_BIT,0);if(i===e.WAIT_FAILED){e.deleteSync(t),n();return}if(i===e.TIMEOUT_EXPIRED){requestAnimationFrame(r);return}e.deleteSync(t),s()}r()})}}let el=!1,wr,fo,tl;class CA{constructor(e){this.backend=e,this.gl=e.gl,this.extensions=e.extensions,this.defaultTextures={},el===!1&&(this._init(this.gl),el=!0)}_init(e){wr={[li]:e.REPEAT,[ca]:e.CLAMP_TO_EDGE,[ci]:e.MIRRORED_REPEAT},fo={[bt]:e.NEAREST,[So]:e.NEAREST_MIPMAP_NEAREST,[Xs]:e.NEAREST_MIPMAP_LINEAR,[ft]:e.LINEAR,[_o]:e.LINEAR_MIPMAP_NEAREST,[ms]:e.LINEAR_MIPMAP_LINEAR},tl={[Bl]:e.NEVER,[Ml]:e.ALWAYS,[ua]:e.LESS,[wl]:e.LEQUAL,[El]:e.EQUAL,[Cl]:e.GEQUAL,[Rl]:e.GREATER,[Al]:e.NOTEQUAL}}filterFallback(e){const{gl:t}=this;return e===bt||e===So||e===Xs?t.NEAREST:t.LINEAR}getGLTextureType(e){const{gl:t}=this;let s;return e.isCubeTexture===!0?s=t.TEXTURE_CUBE_MAP:e.isDataArrayTexture===!0||e.isCompressedArrayTexture===!0?s=t.TEXTURE_2D_ARRAY:e.isData3DTexture===!0?s=t.TEXTURE_3D:s=t.TEXTURE_2D,s}getInternalFormat(e,t,s,n,r=!1){const{gl:i,extensions:a}=this;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 u=t;return t===i.RED&&(s===i.FLOAT&&(u=i.R32F),s===i.HALF_FLOAT&&(u=i.R16F),s===i.UNSIGNED_BYTE&&(u=i.R8),s===i.UNSIGNED_SHORT&&(u=i.R16),s===i.UNSIGNED_INT&&(u=i.R32UI),s===i.BYTE&&(u=i.R8I),s===i.SHORT&&(u=i.R16I),s===i.INT&&(u=i.R32I)),t===i.RED_INTEGER&&(s===i.UNSIGNED_BYTE&&(u=i.R8UI),s===i.UNSIGNED_SHORT&&(u=i.R16UI),s===i.UNSIGNED_INT&&(u=i.R32UI),s===i.BYTE&&(u=i.R8I),s===i.SHORT&&(u=i.R16I),s===i.INT&&(u=i.R32I)),t===i.RG&&(s===i.FLOAT&&(u=i.RG32F),s===i.HALF_FLOAT&&(u=i.RG16F),s===i.UNSIGNED_BYTE&&(u=i.RG8),s===i.UNSIGNED_SHORT&&(u=i.RG16),s===i.UNSIGNED_INT&&(u=i.RG32UI),s===i.BYTE&&(u=i.RG8I),s===i.SHORT&&(u=i.RG16I),s===i.INT&&(u=i.RG32I)),t===i.RG_INTEGER&&(s===i.UNSIGNED_BYTE&&(u=i.RG8UI),s===i.UNSIGNED_SHORT&&(u=i.RG16UI),s===i.UNSIGNED_INT&&(u=i.RG32UI),s===i.BYTE&&(u=i.RG8I),s===i.SHORT&&(u=i.RG16I),s===i.INT&&(u=i.RG32I)),t===i.RGB&&(s===i.FLOAT&&(u=i.RGB32F),s===i.HALF_FLOAT&&(u=i.RGB16F),s===i.UNSIGNED_BYTE&&(u=i.RGB8),s===i.UNSIGNED_SHORT&&(u=i.RGB16),s===i.UNSIGNED_INT&&(u=i.RGB32UI),s===i.BYTE&&(u=i.RGB8I),s===i.SHORT&&(u=i.RGB16I),s===i.INT&&(u=i.RGB32I),s===i.UNSIGNED_BYTE&&(u=n===q&&r===!1?i.SRGB8:i.RGB8),s===i.UNSIGNED_SHORT_5_6_5&&(u=i.RGB565),s===i.UNSIGNED_SHORT_5_5_5_1&&(u=i.RGB5_A1),s===i.UNSIGNED_SHORT_4_4_4_4&&(u=i.RGB4),s===i.UNSIGNED_INT_5_9_9_9_REV&&(u=i.RGB9_E5)),t===i.RGB_INTEGER&&(s===i.UNSIGNED_BYTE&&(u=i.RGB8UI),s===i.UNSIGNED_SHORT&&(u=i.RGB16UI),s===i.UNSIGNED_INT&&(u=i.RGB32UI),s===i.BYTE&&(u=i.RGB8I),s===i.SHORT&&(u=i.RGB16I),s===i.INT&&(u=i.RGB32I)),t===i.RGBA&&(s===i.FLOAT&&(u=i.RGBA32F),s===i.HALF_FLOAT&&(u=i.RGBA16F),s===i.UNSIGNED_BYTE&&(u=i.RGBA8),s===i.UNSIGNED_SHORT&&(u=i.RGBA16),s===i.UNSIGNED_INT&&(u=i.RGBA32UI),s===i.BYTE&&(u=i.RGBA8I),s===i.SHORT&&(u=i.RGBA16I),s===i.INT&&(u=i.RGBA32I),s===i.UNSIGNED_BYTE&&(u=n===q&&r===!1?i.SRGB8_ALPHA8:i.RGBA8),s===i.UNSIGNED_SHORT_4_4_4_4&&(u=i.RGBA4),s===i.UNSIGNED_SHORT_5_5_5_1&&(u=i.RGB5_A1)),t===i.RGBA_INTEGER&&(s===i.UNSIGNED_BYTE&&(u=i.RGBA8UI),s===i.UNSIGNED_SHORT&&(u=i.RGBA16UI),s===i.UNSIGNED_INT&&(u=i.RGBA32UI),s===i.BYTE&&(u=i.RGBA8I),s===i.SHORT&&(u=i.RGBA16I),s===i.INT&&(u=i.RGBA32I)),t===i.DEPTH_COMPONENT&&(s===i.UNSIGNED_INT&&(u=i.DEPTH24_STENCIL8),s===i.FLOAT&&(u=i.DEPTH_COMPONENT32F)),t===i.DEPTH_STENCIL&&s===i.UNSIGNED_INT_24_8&&(u=i.DEPTH24_STENCIL8),(u===i.R16F||u===i.R32F||u===i.RG16F||u===i.RG32F||u===i.RGBA16F||u===i.RGBA32F)&&a.get("EXT_color_buffer_float"),u}setTextureParameters(e,t){const{gl:s,extensions:n,backend:r}=this;s.pixelStorei(s.UNPACK_FLIP_Y_WEBGL,t.flipY),s.pixelStorei(s.UNPACK_PREMULTIPLY_ALPHA_WEBGL,t.premultiplyAlpha),s.pixelStorei(s.UNPACK_ALIGNMENT,t.unpackAlignment),s.pixelStorei(s.UNPACK_COLORSPACE_CONVERSION_WEBGL,s.NONE),s.texParameteri(e,s.TEXTURE_WRAP_S,wr[t.wrapS]),s.texParameteri(e,s.TEXTURE_WRAP_T,wr[t.wrapT]),(e===s.TEXTURE_3D||e===s.TEXTURE_2D_ARRAY)&&s.texParameteri(e,s.TEXTURE_WRAP_R,wr[t.wrapR]),s.texParameteri(e,s.TEXTURE_MAG_FILTER,fo[t.magFilter]);const i=t.mipmaps!==void 0&&t.mipmaps.length>0,a=t.minFilter===ft&&i?ms:t.minFilter;if(s.texParameteri(e,s.TEXTURE_MIN_FILTER,fo[a]),t.compareFunction&&(s.texParameteri(e,s.TEXTURE_COMPARE_MODE,s.COMPARE_REF_TO_TEXTURE),s.texParameteri(e,s.TEXTURE_COMPARE_FUNC,tl[t.compareFunction])),n.has("EXT_texture_filter_anisotropic")===!0){if(t.magFilter===bt||t.minFilter!==Xs&&t.minFilter!==ms||t.type===ot&&n.has("OES_texture_float_linear")===!1)return;if(t.anisotropy>1){const u=n.get("EXT_texture_filter_anisotropic");s.texParameterf(e,u.TEXTURE_MAX_ANISOTROPY_EXT,Math.min(t.anisotropy,r.getMaxAnisotropy()))}}}createDefaultTexture(e){const{gl:t,backend:s,defaultTextures:n}=this,r=this.getGLTextureType(e);let i=n[r];i===void 0&&(i=t.createTexture(),s.state.bindTexture(r,i),t.texParameteri(r,t.TEXTURE_MIN_FILTER,t.NEAREST),t.texParameteri(r,t.TEXTURE_MAG_FILTER,t.NEAREST),n[r]=i),s.set(e,{textureGPU:i,glTextureType:r,isDefault:!0})}createTexture(e,t){const{gl:s,backend:n}=this,{levels:r,width:i,height:a,depth:u}=t,c=n.utils.convert(e.format,e.colorSpace),l=n.utils.convert(e.type),d=this.getInternalFormat(e.internalFormat,c,l,e.colorSpace,e.isVideoTexture),h=s.createTexture(),p=this.getGLTextureType(e);n.state.bindTexture(p,h),this.setTextureParameters(p,e),e.isDataArrayTexture||e.isCompressedArrayTexture?s.texStorage3D(s.TEXTURE_2D_ARRAY,r,d,i,a,u):e.isData3DTexture?s.texStorage3D(s.TEXTURE_3D,r,d,i,a,u):e.isVideoTexture||s.texStorage2D(p,r,d,i,a),n.set(e,{textureGPU:h,glTextureType:p,glFormat:c,glType:l,glInternalFormat:d})}copyBufferToTexture(e,t){const{gl:s,backend:n}=this,{textureGPU:r,glTextureType:i,glFormat:a,glType:u}=n.get(t),{width:c,height:l}=t.source.data;s.bindBuffer(s.PIXEL_UNPACK_BUFFER,e),n.state.bindTexture(i,r),s.pixelStorei(s.UNPACK_FLIP_Y_WEBGL,!1),s.pixelStorei(s.UNPACK_PREMULTIPLY_ALPHA_WEBGL,!1),s.texSubImage2D(i,0,0,0,c,l,a,u,0),s.bindBuffer(s.PIXEL_UNPACK_BUFFER,null),n.state.unbindTexture()}updateTexture(e,t){const{gl:s}=this,{width:n,height:r}=t,{textureGPU:i,glTextureType:a,glFormat:u,glType:c,glInternalFormat:l}=this.backend.get(e);if(e.isRenderTargetTexture||i===void 0)return;const d=h=>h.isDataTexture?h.image.data:typeof HTMLImageElement<"u"&&h instanceof HTMLImageElement||typeof HTMLCanvasElement<"u"&&h instanceof HTMLCanvasElement||typeof ImageBitmap<"u"&&h instanceof ImageBitmap||h instanceof OffscreenCanvas?h:h.data;if(this.backend.state.bindTexture(a,i),this.setTextureParameters(a,e),e.isCompressedTexture){const h=e.mipmaps,p=t.image;for(let f=0;f0,h=t.renderTarget?t.renderTarget.height:this.backend.getDrawingBufferSize().y;if(d){const p=a!==0||u!==0;let f,m;if(e.isDepthTexture===!0?(f=n.DEPTH_BUFFER_BIT,m=n.DEPTH_ATTACHMENT,t.stencil&&(f|=n.STENCIL_BUFFER_BIT)):(f=n.COLOR_BUFFER_BIT,m=n.COLOR_ATTACHMENT0),p){const x=this.backend.get(t.renderTarget),N=x.framebuffers[t.getCacheKey()],v=x.msaaFrameBuffer;r.bindFramebuffer(n.DRAW_FRAMEBUFFER,N),r.bindFramebuffer(n.READ_FRAMEBUFFER,v);const w=h-u-l;n.blitFramebuffer(a,w,a+c,w+l,a,w,a+c,w+l,f,n.NEAREST),r.bindFramebuffer(n.READ_FRAMEBUFFER,N),r.bindTexture(n.TEXTURE_2D,i),n.copyTexSubImage2D(n.TEXTURE_2D,0,0,0,a,w,c,l),r.unbindTexture()}else{const x=n.createFramebuffer();r.bindFramebuffer(n.DRAW_FRAMEBUFFER,x),n.framebufferTexture2D(n.DRAW_FRAMEBUFFER,m,n.TEXTURE_2D,i,0),n.blitFramebuffer(0,0,c,l,0,0,c,l,f,n.NEAREST),n.deleteFramebuffer(x)}}else r.bindTexture(n.TEXTURE_2D,i),n.copyTexSubImage2D(n.TEXTURE_2D,0,0,0,a,h-l-u,c,l),r.unbindTexture();e.generateMipmaps&&this.generateMipmaps(e),this.backend._setFramebuffer(t)}setupRenderBufferStorage(e,t){const{gl:s}=this,n=t.renderTarget,{samples:r,depthTexture:i,depthBuffer:a,stencilBuffer:u,width:c,height:l}=n;if(s.bindRenderbuffer(s.RENDERBUFFER,e),a&&!u){let d=s.DEPTH_COMPONENT24;r>0?(i&&i.isDepthTexture&&i.type===s.FLOAT&&(d=s.DEPTH_COMPONENT32F),s.renderbufferStorageMultisample(s.RENDERBUFFER,r,d,c,l)):s.renderbufferStorage(s.RENDERBUFFER,d,c,l),s.framebufferRenderbuffer(s.FRAMEBUFFER,s.DEPTH_ATTACHMENT,s.RENDERBUFFER,e)}else a&&u&&(r>0?s.renderbufferStorageMultisample(s.RENDERBUFFER,r,s.DEPTH24_STENCIL8,c,l):s.renderbufferStorage(s.RENDERBUFFER,s.DEPTH_STENCIL,c,l),s.framebufferRenderbuffer(s.FRAMEBUFFER,s.DEPTH_STENCIL_ATTACHMENT,s.RENDERBUFFER,e))}async copyTextureToBuffer(e,t,s,n,r,i){const{backend:a,gl:u}=this,{textureGPU:c,glFormat:l,glType:d}=this.backend.get(e),h=u.createFramebuffer();u.bindFramebuffer(u.READ_FRAMEBUFFER,h);const p=e.isCubeTexture?u.TEXTURE_CUBE_MAP_POSITIVE_X+i:u.TEXTURE_2D;u.framebufferTexture2D(u.READ_FRAMEBUFFER,u.COLOR_ATTACHMENT0,p,c,0);const f=this._getTypedArrayType(d),m=this._getBytesPerTexel(d,l),N=n*r*m,v=u.createBuffer();u.bindBuffer(u.PIXEL_PACK_BUFFER,v),u.bufferData(u.PIXEL_PACK_BUFFER,N,u.STREAM_READ),u.readPixels(t,s,n,r,l,d,0),u.bindBuffer(u.PIXEL_PACK_BUFFER,null),await a.utils._clientWaitAsync();const w=new f(N/f.BYTES_PER_ELEMENT);return u.bindBuffer(u.PIXEL_PACK_BUFFER,v),u.getBufferSubData(u.PIXEL_PACK_BUFFER,0,w),u.bindBuffer(u.PIXEL_PACK_BUFFER,null),u.deleteFramebuffer(h),w}_getTypedArrayType(e){const{gl:t}=this;if(e===t.UNSIGNED_BYTE)return Uint8Array;if(e===t.UNSIGNED_SHORT_4_4_4_4||e===t.UNSIGNED_SHORT_5_5_5_1||e===t.UNSIGNED_SHORT_5_6_5||e===t.UNSIGNED_SHORT)return Uint16Array;if(e===t.UNSIGNED_INT)return Uint32Array;if(e===t.HALF_FLOAT)return Uint16Array;if(e===t.FLOAT)return Float32Array;throw new Error(`Unsupported WebGL type: ${e}`)}_getBytesPerTexel(e,t){const{gl:s}=this;let n=0;if(e===s.UNSIGNED_BYTE&&(n=1),(e===s.UNSIGNED_SHORT_4_4_4_4||e===s.UNSIGNED_SHORT_5_5_5_1||e===s.UNSIGNED_SHORT_5_6_5||e===s.UNSIGNED_SHORT||e===s.HALF_FLOAT)&&(n=2),(e===s.UNSIGNED_INT||e===s.FLOAT)&&(n=4),t===s.RGBA)return n*4;if(t===s.RGB)return n*3;if(t===s.ALPHA)return n}}class EA{constructor(e){this.backend=e,this.gl=this.backend.gl,this.availableExtensions=this.gl.getSupportedExtensions(),this.extensions={}}get(e){let t=this.extensions[e];return t===void 0&&(t=this.gl.getExtension(e),this.extensions[e]=t),t}has(e){return this.availableExtensions.includes(e)}}class wA{constructor(e){this.backend=e,this.maxAnisotropy=null}getMaxAnisotropy(){if(this.maxAnisotropy!==null)return this.maxAnisotropy;const e=this.backend.gl,t=this.backend.extensions;if(t.has("EXT_texture_filter_anisotropic")===!0){const s=t.get("EXT_texture_filter_anisotropic");this.maxAnisotropy=e.getParameter(s.MAX_TEXTURE_MAX_ANISOTROPY_EXT)}else this.maxAnisotropy=0;return this.maxAnisotropy}}const sl={WEBGL_multi_draw:"WEBGL_multi_draw",WEBGL_compressed_texture_astc:"texture-compression-astc",WEBGL_compressed_texture_etc:"texture-compression-etc2",WEBGL_compressed_texture_etc1:"texture-compression-etc1",WEBGL_compressed_texture_pvrtc:"texture-compression-pvrtc",WEBKIT_WEBGL_compressed_texture_pvrtc:"texture-compression-pvrtc",WEBGL_compressed_texture_s3tc:"texture-compression-bc",EXT_texture_compression_bptc:"texture-compression-bptc",EXT_disjoint_timer_query_webgl2:"timestamp-query"};class MA{constructor(e){this.gl=e.gl,this.extensions=e.extensions,this.info=e.renderer.info,this.mode=null,this.index=0,this.type=null,this.object=null}render(e,t){const{gl:s,mode:n,object:r,type:i,info:a,index:u}=this;u!==0?s.drawElements(n,t,i,e):s.drawArrays(n,e,t),a.update(r,t,n,1)}renderInstances(e,t,s){const{gl:n,mode:r,type:i,index:a,object:u,info:c}=this;s!==0&&(a!==0?n.drawElementsInstanced(r,t,i,e,s):n.drawArraysInstanced(r,e,t,s),c.update(u,t,r,s))}renderMultiDraw(e,t,s){const{extensions:n,mode:r,object:i,info:a}=this;if(s===0)return;const u=n.get("WEBGL_multi_draw");if(u===null)for(let c=0;c0)){const s=t.queryQueue.shift();this.initTimestampQuery(s)}}async resolveTimestampAsync(e,t="render"){if(!this.disjoint||!this.trackTimestamp)return;const s=this.get(e);s.gpuQueries||(s.gpuQueries=[]);for(let n=0;n0&&(s.currentOcclusionQueries=s.occlusionQueries,s.currentOcclusionQueryObjects=s.occlusionQueryObjects,s.lastOcclusionObject=null,s.occlusionQueries=new Array(n),s.occlusionQueryObjects=new Array(n),s.occlusionQueryIndex=0)}finishRender(e){const{gl:t,state:s}=this,n=this.get(e),r=n.previousContext,i=e.occlusionQueryCount;i>0&&(i>n.occlusionQueryIndex&&t.endQuery(t.ANY_SAMPLES_PASSED),this.resolveOccludedAsync(e));const a=e.textures;if(a!==null)for(let u=0;u0){const l=u.framebuffers[e.getCacheKey()],d=t.COLOR_BUFFER_BIT,h=u.msaaFrameBuffer,p=e.textures;s.bindFramebuffer(t.READ_FRAMEBUFFER,h),s.bindFramebuffer(t.DRAW_FRAMEBUFFER,l);for(let f=0;f{let u=0;for(let c=0;c0&&r.add(n[c]),s[c]=null,i.deleteQuery(l),u++)}u1?N.renderInstances(B,v,w):N.render(B,v),u.bindVertexArray(null)}needsRenderUpdate(){return!1}getRenderCacheKey(){return""}createDefaultTexture(e){this.textureUtils.createDefaultTexture(e)}createTexture(e,t){this.textureUtils.createTexture(e,t)}updateTexture(e,t){this.textureUtils.updateTexture(e,t)}generateMipmaps(e){this.textureUtils.generateMipmaps(e)}destroyTexture(e){this.textureUtils.destroyTexture(e)}copyTextureToBuffer(e,t,s,n,r,i){return this.textureUtils.copyTextureToBuffer(e,t,s,n,r,i)}createSampler(){}destroySampler(){}createNodeBuilder(e,t){return new bA(e,t)}createProgram(e){const t=this.gl,{stage:s,code:n}=e,r=s==="fragment"?t.createShader(t.FRAGMENT_SHADER):t.createShader(t.VERTEX_SHADER);t.shaderSource(r,n),t.compileShader(r),this.set(e,{shaderGPU:r})}destroyProgram(e){this.delete(e)}createRenderPipeline(e,t){const s=this.gl,n=e.pipeline,{fragmentProgram:r,vertexProgram:i}=n,a=s.createProgram(),u=this.get(r).shaderGPU,c=this.get(i).shaderGPU;if(s.attachShader(a,u),s.attachShader(a,c),s.linkProgram(a),this.set(n,{programGPU:a,fragmentShader:u,vertexShader:c}),t!==null&&this.parallel){const l=new Promise(d=>{const h=this.parallel,p=()=>{s.getProgramParameter(a,h.COMPLETION_STATUS_KHR)?(this._completeCompile(e,n),d()):requestAnimationFrame(p)};p()});t.push(l);return}this._completeCompile(e,n)}_handleSource(e,t){const s=e.split(` -`),n=[],r=Math.max(t-6,0),i=Math.min(t+6,s.length);for(let a=r;a":" "} ${u}: ${s[a]}`)}return n.join(` -`)}_getShaderErrors(e,t,s){const n=e.getShaderParameter(t,e.COMPILE_STATUS),r=e.getShaderInfoLog(t).trim();if(n&&r==="")return"";const i=/ERROR: 0:(\d+)/.exec(r);if(i){const a=parseInt(i[1]);return s.toUpperCase()+` - -`+r+` - -`+this._handleSource(e.getShaderSource(t),a)}else return r}_logProgramError(e,t,s){if(this.renderer.debug.checkShaderErrors){const n=this.gl,r=n.getProgramInfoLog(e).trim();if(n.getProgramParameter(e,n.LINK_STATUS)===!1)if(typeof this.renderer.debug.onShaderError=="function")this.renderer.debug.onShaderError(n,e,s,t);else{const i=this._getShaderErrors(n,s,"vertex"),a=this._getShaderErrors(n,t,"fragment");console.error("THREE.WebGLProgram: Shader Error "+n.getError()+" - VALIDATE_STATUS "+n.getProgramParameter(e,n.VALIDATE_STATUS)+` - -Program Info Log: `+r+` -`+i+` -`+a)}else r!==""&&console.warn("THREE.WebGLProgram: Program Info Log:",r)}}_completeCompile(e,t){const{state:s,gl:n}=this,r=this.get(t),{programGPU:i,fragmentShader:a,vertexShader:u}=r;n.getProgramParameter(i,n.LINK_STATUS)===!1&&this._logProgramError(i,a,u),s.useProgram(i);const c=e.getBindings();this._setupBindings(c,i),this.set(t,{programGPU:i})}createComputePipeline(e,t){const{state:s,gl:n}=this,r={stage:"fragment",code:`#version 300 es -precision highp float; -void main() {}`};this.createProgram(r);const{computeProgram:i}=e,a=n.createProgram(),u=this.get(r).shaderGPU,c=this.get(i).shaderGPU,l=i.transforms,d=[],h=[];for(let x=0;xsl[n]===e),s=this.extensions;for(let n=0;n0){if(p===void 0){const N=[];p=t.createFramebuffer(),s.bindFramebuffer(t.FRAMEBUFFER,p);const v=[],w=e.textures;for(let B=0;B, - @location( 0 ) vTex : vec2 -}; - -@vertex -fn main( @builtin( vertex_index ) vertexIndex : u32 ) -> VarysStruct { - - var Varys : VarysStruct; - - var pos = array< vec2, 4 >( - vec2( -1.0, 1.0 ), - vec2( 1.0, 1.0 ), - vec2( -1.0, -1.0 ), - vec2( 1.0, -1.0 ) - ); - - var tex = array< vec2, 4 >( - vec2( 0.0, 0.0 ), - vec2( 1.0, 0.0 ), - vec2( 0.0, 1.0 ), - vec2( 1.0, 1.0 ) - ); - - Varys.vTex = tex[ vertexIndex ]; - Varys.Position = vec4( pos[ vertexIndex ], 0.0, 1.0 ); - - return Varys; - -} -`,s=` -@group( 0 ) @binding( 0 ) -var imgSampler : sampler; - -@group( 0 ) @binding( 1 ) -var img : texture_2d; - -@fragment -fn main( @location( 0 ) vTex : vec2 ) -> @location( 0 ) vec4 { - - return textureSample( img, imgSampler, vTex ); - -} -`,n=` -@group( 0 ) @binding( 0 ) -var imgSampler : sampler; - -@group( 0 ) @binding( 1 ) -var img : texture_2d; - -@fragment -fn main( @location( 0 ) vTex : vec2 ) -> @location( 0 ) vec4 { - - return textureSample( img, imgSampler, vec2( vTex.x, 1.0 - vTex.y ) ); - -} -`;this.mipmapSampler=e.createSampler({minFilter:ps.Linear}),this.flipYSampler=e.createSampler({minFilter:ps.Nearest}),this.transferPipelines={},this.flipYPipelines={},this.mipmapVertexShaderModule=e.createShaderModule({label:"mipmapVertex",code:t}),this.mipmapFragmentShaderModule=e.createShaderModule({label:"mipmapFragment",code:s}),this.flipYFragmentShaderModule=e.createShaderModule({label:"flipYFragment",code:n})}getTransferPipeline(e){let t=this.transferPipelines[e];return t===void 0&&(t=this.device.createRenderPipeline({label:`mipmap-${e}`,vertex:{module:this.mipmapVertexShaderModule,entryPoint:"main"},fragment:{module:this.mipmapFragmentShaderModule,entryPoint:"main",targets:[{format:e}]},primitive:{topology:Ks.TriangleStrip,stripIndexFormat:cn.Uint32},layout:"auto"}),this.transferPipelines[e]=t),t}getFlipYPipeline(e){let t=this.flipYPipelines[e];return t===void 0&&(t=this.device.createRenderPipeline({label:`flipY-${e}`,vertex:{module:this.mipmapVertexShaderModule,entryPoint:"main"},fragment:{module:this.flipYFragmentShaderModule,entryPoint:"main",targets:[{format:e}]},primitive:{topology:Ks.TriangleStrip,stripIndexFormat:cn.Uint32},layout:"auto"}),this.flipYPipelines[e]=t),t}flipY(e,t,s=0){const n=t.format,{width:r,height:i}=t.size,a=this.getTransferPipeline(n),u=this.getFlipYPipeline(n),c=this.device.createTexture({size:{width:r,height:i,depthOrArrayLayers:1},format:n,usage:GPUTextureUsage.RENDER_ATTACHMENT|GPUTextureUsage.TEXTURE_BINDING}),l=e.createView({baseMipLevel:0,mipLevelCount:1,dimension:$e.TwoD,baseArrayLayer:s}),d=c.createView({baseMipLevel:0,mipLevelCount:1,dimension:$e.TwoD,baseArrayLayer:0}),h=this.device.createCommandEncoder({}),p=(f,m,x)=>{const N=f.getBindGroupLayout(0),v=this.device.createBindGroup({layout:N,entries:[{binding:0,resource:this.flipYSampler},{binding:1,resource:m}]}),w=h.beginRenderPass({colorAttachments:[{view:x,loadOp:ye.Clear,storeOp:De.Store,clearValue:[0,0,0,0]}]});w.setPipeline(f),w.setBindGroup(0,v),w.draw(4,1,0,0),w.end()};p(a,l,d),p(u,d,l),this.device.queue.submit([h.finish()]),c.destroy()}generateMipmaps(e,t,s=0){const n=this.get(e);n.useCount===void 0&&(n.useCount=0,n.layers=[]);const r=n.layers[s]||this._mipmapCreateBundles(e,t,s),i=this.device.createCommandEncoder({});this._mipmapRunBundles(i,r),this.device.queue.submit([i.finish()]),n.useCount!==0&&(n.layers[s]=r),n.useCount++}_mipmapCreateBundles(e,t,s){const n=this.getTransferPipeline(t.format),r=n.getBindGroupLayout(0);let i=e.createView({baseMipLevel:0,mipLevelCount:1,dimension:$e.TwoD,baseArrayLayer:s});const a=[];for(let u=1;u1;for(let a=0;a]*\s*([a-z_0-9]+(?:<[\s\S]+?>)?)/i,zA=/([a-z_0-9]+)\s*:\s*([a-z_0-9]+(?:<[\s\S]+?>)?)/ig,ol={f32:"float",i32:"int",u32:"uint",bool:"bool","vec2":"vec2","vec2":"ivec2","vec2":"uvec2","vec2":"bvec2",vec2f:"vec2",vec2i:"ivec2",vec2u:"uvec2",vec2b:"bvec2","vec3":"vec3","vec3":"ivec3","vec3":"uvec3","vec3":"bvec3",vec3f:"vec3",vec3i:"ivec3",vec3u:"uvec3",vec3b:"bvec3","vec4":"vec4","vec4":"ivec4","vec4":"uvec4","vec4":"bvec4",vec4f:"vec4",vec4i:"ivec4",vec4u:"uvec4",vec4b:"bvec4","mat2x2":"mat2",mat2x2f:"mat2","mat3x3":"mat3",mat3x3f:"mat3","mat4x4":"mat4",mat4x4f:"mat4",sampler:"sampler",texture_1d:"texture",texture_2d:"texture",texture_2d_array:"texture",texture_multisampled_2d:"cubeTexture",texture_depth_2d:"depthTexture",texture_depth_multisampled_2d:"depthTexture",texture_3d:"texture3D",texture_cube:"cubeTexture",texture_cube_array:"cubeTexture",texture_storage_1d:"storageTexture",texture_storage_2d:"storageTexture",texture_storage_2d_array:"storageTexture",texture_storage_3d:"storageTexture"},WA=o=>{o=o.trim();const e=o.match(kA);if(e!==null&&e.length===4){const t=e[2],s=[];let n=null;for(;(n=zA.exec(t))!==null;)s.push({name:n[1],type:n[2]});const r=[];for(let l=0;l "+this.outputType:"";return`fn ${e} ( ${this.inputsCode.trim()} ) ${t}`+this.blockCode}}class HA extends pg{parseFunction(e){return new $A(e)}}const Vs=typeof self<"u"?self.GPUShaderStage:{VERTEX:1,FRAGMENT:2,COMPUTE:4},qA={[Ve.READ_ONLY]:"read",[Ve.WRITE_ONLY]:"write",[Ve.READ_WRITE]:"read_write"},al={[li]:"repeat",[ca]:"clamp",[ci]:"mirror"},Br={vertex:Vs?Vs.VERTEX:1,fragment:Vs?Vs.FRAGMENT:2,compute:Vs?Vs.COMPUTE:4},ul={instance:!0,swizzleAssign:!1,storageBuffer:!0},KA={"^^":"tsl_xor"},XA={float:"f32",int:"i32",uint:"u32",bool:"bool",color:"vec3",vec2:"vec2",ivec2:"vec2",uvec2:"vec2",bvec2:"vec2",vec3:"vec3",ivec3:"vec3",uvec3:"vec3",bvec3:"vec3",vec4:"vec4",ivec4:"vec4",uvec4:"vec4",bvec4:"vec4",mat2:"mat2x2",mat3:"mat3x3",mat4:"mat4x4"},cl={},it={tsl_xor:new Ne("fn tsl_xor( a : bool, b : bool ) -> bool { return ( a || b ) && !( a && b ); }"),mod_float:new Ne("fn tsl_mod_float( x : f32, y : f32 ) -> f32 { return x - y * floor( x / y ); }"),mod_vec2:new Ne("fn tsl_mod_vec2( x : vec2f, y : vec2f ) -> vec2f { return x - y * floor( x / y ); }"),mod_vec3:new Ne("fn tsl_mod_vec3( x : vec3f, y : vec3f ) -> vec3f { return x - y * floor( x / y ); }"),mod_vec4:new Ne("fn tsl_mod_vec4( x : vec4f, y : vec4f ) -> vec4f { return x - y * floor( x / y ); }"),equals_bool:new Ne("fn tsl_equals_bool( a : bool, b : bool ) -> bool { return a == b; }"),equals_bvec2:new Ne("fn tsl_equals_bvec2( a : vec2f, b : vec2f ) -> vec2 { return vec2( a.x == b.x, a.y == b.y ); }"),equals_bvec3:new Ne("fn tsl_equals_bvec3( a : vec3f, b : vec3f ) -> vec3 { return vec3( a.x == b.x, a.y == b.y, a.z == b.z ); }"),equals_bvec4:new Ne("fn tsl_equals_bvec4( a : vec4f, b : vec4f ) -> vec4 { return vec4( a.x == b.x, a.y == b.y, a.z == b.z, a.w == b.w ); }"),repeatWrapping_float:new Ne("fn tsl_repeatWrapping_float( coord: f32 ) -> f32 { return fract( coord ); }"),mirrorWrapping_float:new Ne("fn tsl_mirrorWrapping_float( coord: f32 ) -> f32 { let mirrored = fract( coord * 0.5 ) * 2.0; return 1.0 - abs( 1.0 - mirrored ); }"),clampWrapping_float:new Ne("fn tsl_clampWrapping_float( coord: f32 ) -> f32 { return clamp( coord, 0.0, 1.0 ); }"),biquadraticTexture:new Ne(` -fn tsl_biquadraticTexture( map : texture_2d, coord : vec2f, iRes : vec2u, level : u32 ) -> vec4f { - - let res = vec2f( iRes ); - - let uvScaled = coord * res; - let uvWrapping = ( ( uvScaled % res ) + res ) % res; - - // https://www.shadertoy.com/view/WtyXRy - - let uv = uvWrapping - 0.5; - let iuv = floor( uv ); - let f = fract( uv ); - - let rg1 = textureLoad( map, vec2u( iuv + vec2( 0.5, 0.5 ) ) % iRes, level ); - let rg2 = textureLoad( map, vec2u( iuv + vec2( 1.5, 0.5 ) ) % iRes, level ); - let rg3 = textureLoad( map, vec2u( iuv + vec2( 0.5, 1.5 ) ) % iRes, level ); - let rg4 = textureLoad( map, vec2u( iuv + vec2( 1.5, 1.5 ) ) % iRes, level ); - - return mix( mix( rg1, rg2, f.x ), mix( rg3, rg4, f.x ), f.y ); - -} -`)},vn={dFdx:"dpdx",dFdy:"- dpdy",mod_float:"tsl_mod_float",mod_vec2:"tsl_mod_vec2",mod_vec3:"tsl_mod_vec3",mod_vec4:"tsl_mod_vec4",equals_bool:"tsl_equals_bool",equals_bvec2:"tsl_equals_bvec2",equals_bvec3:"tsl_equals_bvec3",equals_bvec4:"tsl_equals_bvec4",inversesqrt:"inverseSqrt",bitcast:"bitcast"};typeof navigator<"u"&&/Windows/g.test(navigator.userAgent)&&(it.pow_float=new Ne("fn tsl_pow_float( a : f32, b : f32 ) -> f32 { return select( -pow( -a, b ), pow( a, b ), a > 0.0 ); }"),it.pow_vec2=new Ne("fn tsl_pow_vec2( a : vec2f, b : vec2f ) -> vec2f { return vec2f( tsl_pow_float( a.x, b.x ), tsl_pow_float( a.y, b.y ) ); }",[it.pow_float]),it.pow_vec3=new Ne("fn tsl_pow_vec3( a : vec3f, b : vec3f ) -> vec3f { return vec3f( tsl_pow_float( a.x, b.x ), tsl_pow_float( a.y, b.y ), tsl_pow_float( a.z, b.z ) ); }",[it.pow_float]),it.pow_vec4=new Ne("fn tsl_pow_vec4( a : vec4f, b : vec4f ) -> vec4f { return vec4f( tsl_pow_float( a.x, b.x ), tsl_pow_float( a.y, b.y ), tsl_pow_float( a.z, b.z ), tsl_pow_float( a.w, b.w ) ); }",[it.pow_float]),vn.pow_float="tsl_pow_float",vn.pow_vec2="tsl_pow_vec2",vn.pow_vec3="tsl_pow_vec3",vn.pow_vec4="tsl_pow_vec4");let Ng="";(typeof navigator<"u"&&/Firefox|Deno/g.test(navigator.userAgent))!==!0&&(Ng+=`diagnostic( off, derivative_uniformity ); -`);class YA extends dg{constructor(e,t){super(e,t,new HA),this.uniformGroups={},this.builtins={},this.directives={},this.scopedArrays=new Map}needsToWorkingColorSpace(e){return e.isVideoTexture===!0&&e.colorSpace!==Dn}_generateTextureSample(e,t,s,n,r=this.shaderStage){return r==="fragment"?n?`textureSample( ${t}, ${t}_sampler, ${s}, ${n} )`:`textureSample( ${t}, ${t}_sampler, ${s} )`:this.isFilteredTexture(e)?this.generateFilteredTexture(e,t,s):this.generateTextureLod(e,t,s,n,"0")}_generateVideoSample(e,t,s=this.shaderStage){if(s==="fragment")return`textureSampleBaseClampToEdge( ${e}, ${e}_sampler, vec2( ${t}.x, 1.0 - ${t}.y ) )`;console.error(`WebGPURenderer: THREE.VideoTexture does not support ${s} shader.`)}_generateTextureSampleLevel(e,t,s,n,r,i=this.shaderStage){return(i==="fragment"||i==="compute")&&this.isUnfilterable(e)===!1?`textureSampleLevel( ${t}, ${t}_sampler, ${s}, ${n} )`:this.isFilteredTexture(e)?this.generateFilteredTexture(e,t,s,n):this.generateTextureLod(e,t,s,r,n)}generateWrapFunction(e){const t=`tsl_coord_${al[e.wrapS]}S_${al[e.wrapT]}_${e.isData3DTexture?"3d":"2d"}T`;let s=cl[t];if(s===void 0){const n=[],r=e.isData3DTexture?"vec3f":"vec2f";let i=`fn ${t}( coord : ${r} ) -> ${r} { - - return ${r}( -`;const a=(u,c)=>{u===li?(n.push(it.repeatWrapping_float),i+=` tsl_repeatWrapping_float( coord.${c} )`):u===ca?(n.push(it.clampWrapping_float),i+=` tsl_clampWrapping_float( coord.${c} )`):u===ci?(n.push(it.mirrorWrapping_float),i+=` tsl_mirrorWrapping_float( coord.${c} )`):(i+=` coord.${c}`,console.warn(`WebGPURenderer: Unsupported texture wrap type "${u}" for vertex shader.`))};a(e.wrapS,"x"),i+=`, -`,a(e.wrapT,"y"),e.isData3DTexture&&(i+=`, -`,a(e.wrapR,"z")),i+=` - ); - -} -`,cl[t]=s=new Ne(i,n)}return s.build(this),t}generateTextureDimension(e,t,s){const n=this.getDataFromNode(e,this.shaderStage,this.globalCache);n.dimensionsSnippet===void 0&&(n.dimensionsSnippet={});let r=n.dimensionsSnippet[s];if(n.dimensionsSnippet[s]===void 0){let i,a;const{primarySamples:u}=this.renderer.backend.utils.getTextureSampleData(e),c=u>1;e.isData3DTexture?a="vec3":a="vec2",c||e.isVideoTexture||e.isStorageTexture?i=t:i=`${t}${s?`, u32( ${s} )`:""}`,r=new Ir(new Vr(`textureDimensions( ${i} )`,a)),n.dimensionsSnippet[s]=r,(e.isDataArrayTexture||e.isData3DTexture)&&(n.arrayLayerCount=new Ir(new Vr(`textureNumLayers(${t})`,"u32"))),e.isTextureCube&&(n.cubeFaceCount=new Ir(new Vr("6u","u32")))}return r.build(this)}generateFilteredTexture(e,t,s,n="0u"){this._include("biquadraticTexture");const r=this.generateWrapFunction(e),i=this.generateTextureDimension(e,t,n);return`tsl_biquadraticTexture( ${t}, ${r}( ${s} ), ${i}, u32( ${n} ) )`}generateTextureLod(e,t,s,n,r="0u"){const i=this.generateWrapFunction(e),a=this.generateTextureDimension(e,t,r),u=e.isData3DTexture?"vec3":"vec2",c=`${u}(${i}(${s}) * ${u}(${a}))`;return this.generateTextureLoad(e,t,c,n,r)}generateTextureLoad(e,t,s,n,r="0u"){return e.isVideoTexture===!0||e.isStorageTexture===!0?`textureLoad( ${t}, ${s} )`:n?`textureLoad( ${t}, ${s}, ${n}, u32( ${r} ) )`:`textureLoad( ${t}, ${s}, u32( ${r} ) )`}generateTextureStore(e,t,s,n){return`textureStore( ${t}, ${s}, ${n} )`}isSampleCompare(e){return e.isDepthTexture===!0&&e.compareFunction!==null}isUnfilterable(e){return this.getComponentTypeFromTexture(e)!=="float"||!this.isAvailable("float32Filterable")&&e.isDataTexture===!0&&e.type===ot||this.isSampleCompare(e)===!1&&e.minFilter===bt&&e.magFilter===bt||this.renderer.backend.utils.getTextureSampleData(e).primarySamples>1}generateTexture(e,t,s,n,r=this.shaderStage){let i=null;return e.isVideoTexture===!0?i=this._generateVideoSample(t,s,r):this.isUnfilterable(e)?i=this.generateTextureLod(e,t,s,n,"0",r):i=this._generateTextureSample(e,t,s,n,r),i}generateTextureGrad(e,t,s,n,r,i=this.shaderStage){if(i==="fragment")return`textureSampleGrad( ${t}, ${t}_sampler, ${s}, ${n[0]}, ${n[1]} )`;console.error(`WebGPURenderer: THREE.TextureNode.gradient() does not support ${i} shader.`)}generateTextureCompare(e,t,s,n,r,i=this.shaderStage){if(i==="fragment")return`textureSampleCompare( ${t}, ${t}_sampler, ${s}, ${n} )`;console.error(`WebGPURenderer: THREE.DepthTexture.compareFunction() does not support ${i} shader.`)}generateTextureLevel(e,t,s,n,r,i=this.shaderStage){let a=null;return e.isVideoTexture===!0?a=this._generateVideoSample(t,s,i):a=this._generateTextureSampleLevel(e,t,s,n,r,i),a}generateTextureBias(e,t,s,n,r,i=this.shaderStage){if(i==="fragment")return`textureSampleBias( ${t}, ${t}_sampler, ${s}, ${n} )`;console.error(`WebGPURenderer: THREE.TextureNode.biasNode does not support ${i} shader.`)}getPropertyName(e,t=this.shaderStage){if(e.isNodeVarying===!0&&e.needsInterpolation===!0){if(t==="vertex")return`varyings.${e.name}`}else if(e.isNodeUniform===!0){const s=e.name,n=e.type;return n==="texture"||n==="cubeTexture"||n==="storageTexture"||n==="texture3D"?s:n==="buffer"||n==="storageBuffer"||n==="indirectStorageBuffer"?`NodeBuffer_${e.id}.${s}`:e.groupNode.name+"."+s}return super.getPropertyName(e)}getOutputStructName(){return"output"}_getUniformGroupCount(e){return Object.keys(this.uniforms[e]).length}getFunctionOperator(e){const t=KA[e];return t!==void 0?(this._include(t),t):null}getNodeAccess(e,t){return t!=="compute"?Ve.READ_ONLY:e.access}getStorageAccess(e,t){return qA[this.getNodeAccess(e,t)]}getUniformFromNode(e,t,s,n=null){const r=super.getUniformFromNode(e,t,s,n),i=this.getDataFromNode(e,s,this.globalCache);if(i.uniformGPU===void 0){let a;const u=e.groupNode,c=u.name,l=this.getBindGroupArray(c,s);if(t==="texture"||t==="cubeTexture"||t==="storageTexture"||t==="texture3D"){let d=null;const h=this.getNodeAccess(e,s);if(t==="texture"||t==="storageTexture"?d=new Di(r.name,r.node,u,h):t==="cubeTexture"?d=new Tg(r.name,r.node,u,h):t==="texture3D"&&(d=new _g(r.name,r.node,u,h)),d.store=e.isStorageTextureNode===!0,d.setVisibility(Br[s]),(s==="fragment"||s==="compute")&&this.isUnfilterable(e.value)===!1&&d.store===!1){const p=new UA(`${r.name}_sampler`,r.node,u);p.setVisibility(Br[s]),l.push(p,d),a=[p,d]}else l.push(d),a=[d]}else if(t==="buffer"||t==="storageBuffer"||t==="indirectStorageBuffer"){const d=t==="buffer"?yg:LA,h=new d(e,u);h.setVisibility(Br[s]),l.push(h),a=h}else{const d=this.uniformGroups[s]||(this.uniformGroups[s]={});let h=d[c];h===void 0&&(h=new xg(c,u),h.setVisibility(Br[s]),d[c]=h,l.push(h)),a=this.getNodeUniform(r,t),h.addUniform(a)}i.uniformGPU=a}return r}getBuiltin(e,t,s,n=this.shaderStage){const r=this.builtins[n]||(this.builtins[n]=new Map);return r.has(e)===!1&&r.set(e,{name:e,property:t,type:s}),t}hasBuiltin(e,t=this.shaderStage){return this.builtins[t]!==void 0&&this.builtins[t].has(e)}getVertexIndex(){return this.shaderStage==="vertex"?this.getBuiltin("vertex_index","vertexIndex","u32","attribute"):"vertexIndex"}buildFunctionCode(e){const t=e.layout,s=this.flowShaderNode(e),n=[];for(const i of t.inputs)n.push(i.name+" : "+this.getType(i.type));let r=`fn ${t.name}( ${n.join(", ")} ) -> ${this.getType(t.type)} { -${s.vars} -${s.code} -`;return s.result&&(r+=` return ${s.result}; -`),r+=` -} -`,r}getInstanceIndex(){return this.shaderStage==="vertex"?this.getBuiltin("instance_index","instanceIndex","u32","attribute"):"instanceIndex"}getInvocationLocalIndex(){return this.getBuiltin("local_invocation_index","invocationLocalIndex","u32","attribute")}getSubgroupSize(){return this.enableSubGroups(),this.getBuiltin("subgroup_size","subgroupSize","u32","attribute")}getInvocationSubgroupIndex(){return this.enableSubGroups(),this.getBuiltin("subgroup_invocation_id","invocationSubgroupIndex","u32","attribute")}getSubgroupIndex(){return this.enableSubGroups(),this.getBuiltin("subgroup_id","subgroupIndex","u32","attribute")}getDrawIndex(){return null}getFrontFacing(){return this.getBuiltin("front_facing","isFront","bool")}getFragCoord(){return this.getBuiltin("position","fragCoord","vec4")+".xy"}getFragDepth(){return"output."+this.getBuiltin("frag_depth","depth","f32","output")}getClipDistance(){return"varyings.hw_clip_distances"}isFlipY(){return!1}enableDirective(e,t=this.shaderStage){(this.directives[t]||(this.directives[t]=new Set)).add(e)}getDirectives(e){const t=[],s=this.directives[e];if(s!==void 0)for(const n of s)t.push(`enable ${n};`);return t.join(` -`)}enableSubGroups(){this.enableDirective("subgroups")}enableSubgroupsF16(){this.enableDirective("subgroups-f16")}enableClipDistances(){this.enableDirective("clip_distances")}enableShaderF16(){this.enableDirective("f16")}enableDualSourceBlending(){this.enableDirective("dual_source_blending")}enableHardwareClipping(e){this.enableClipDistances(),this.getBuiltin("clip_distances","hw_clip_distances",`array`,"vertex")}getBuiltins(e){const t=[],s=this.builtins[e];if(s!==void 0)for(const{name:n,property:r,type:i}of s.values())t.push(`@builtin( ${n} ) ${r} : ${i}`);return t.join(`, - `)}getScopedArray(e,t,s,n){return this.scopedArrays.has(e)===!1&&this.scopedArrays.set(e,{name:e,scope:t,bufferType:s,bufferCount:n}),e}getScopedArrays(e){if(e!=="compute")return;const t=[];for(const{name:s,scope:n,bufferType:r,bufferCount:i}of this.scopedArrays.values()){const a=this.getType(r);t.push(`var<${n}> ${s}: array< ${a}, ${i} >;`)}return t.join(` -`)}getAttributes(e){const t=[];if(e==="compute"&&(this.getBuiltin("global_invocation_id","id","vec3","attribute"),this.getBuiltin("workgroup_id","workgroupId","vec3","attribute"),this.getBuiltin("local_invocation_id","localId","vec3","attribute"),this.getBuiltin("num_workgroups","numWorkgroups","vec3","attribute"),this.renderer.hasFeature("subgroups")&&(this.enableDirective("subgroups",e),this.getBuiltin("subgroup_size","subgroupSize","u32","attribute"))),e==="vertex"||e==="compute"){const s=this.getBuiltins("attribute");s&&t.push(s);const n=this.getAttributesArray();for(let r=0,i=n.length;r`)}const n=this.getBuiltins("output");return n&&t.push(" "+n),t.join(`, -`)}getStructs(e){const t=[],s=this.structs[e];for(let n=0,r=s.length;n output : ${a}; - -`)}return t.join(` - -`)}getVar(e,t){return`var ${t} : ${this.getType(e)}`}getVars(e){const t=[],s=this.vars[e];if(s!==void 0)for(const n of s)t.push(` ${this.getVar(n.type,n.name)};`);return` -${t.join(` -`)} -`}getVaryings(e){const t=[];if(e==="vertex"&&this.getBuiltin("position","Vertex","vec4","vertex"),e==="vertex"||e==="fragment"){const r=this.varyings,i=this.vars[e];for(let a=0;a1&&(p="_multisampled"),d.isCubeTexture===!0)h="texture_cube";else if(d.isDataArrayTexture===!0||d.isCompressedArrayTexture===!0)h="texture_2d_array";else if(d.isDepthTexture===!0)h=`texture_depth${p}_2d`;else if(d.isVideoTexture===!0)h="texture_external";else if(d.isData3DTexture===!0)h="texture_3d";else if(u.node.isStorageTextureNode===!0){const m=ia(d),x=this.getStorageAccess(u.node,e);h=`texture_storage_2d<${m}, ${x}>`}else{const m=this.getComponentTypeFromTexture(d).charAt(0);h=`texture${p}_2d<${m}32>`}s.push(`@binding( ${l.binding++} ) @group( ${l.group} ) var ${u.name} : ${h};`)}else if(u.type==="buffer"||u.type==="storageBuffer"||u.type==="indirectStorageBuffer"){const d=u.node,h=this.getType(d.bufferType),p=d.bufferCount,f=p>0&&u.type==="buffer"?", "+p:"",m=d.isAtomic?`atomic<${h}>`:`${h}`,x=` ${u.name} : array< ${m}${f} > -`,N=d.isStorageBufferNode?`storage, ${this.getStorageAccess(d,e)}`:"uniform";n.push(this._getWGSLStructBinding("NodeBuffer_"+d.id,x,N,l.binding++,l.group))}else{const d=this.getType(this.getVectorType(u.type)),h=u.groupNode.name;(i[h]||(i[h]={index:l.binding++,id:l.group,snippets:[]})).snippets.push(` ${u.name} : ${d}`)}}for(const u in i){const c=i[u];r.push(this._getWGSLStructBinding(u,c.snippets.join(`, -`),"uniform",c.index,c.id))}let a=s.join(` -`);return a+=n.join(` -`),a+=r.join(` -`),a}buildCode(){const e=this.material!==null?{fragment:{},vertex:{}}:{compute:{}};this.sortBindingGroups();for(const t in e){const s=e[t];s.uniforms=this.getUniforms(t),s.attributes=this.getAttributes(t),s.varyings=this.getVaryings(t),s.structs=this.getStructs(t),s.vars=this.getVars(t),s.codes=this.getCodes(t),s.directives=this.getDirectives(t),s.scopedArrays=this.getScopedArrays(t);let n=`// code - -`;n+=this.flowCode[t];const r=this.flowNodes[t],i=r[r.length-1],a=i.outputNode,u=a!==void 0&&a.isOutputStructNode===!0;for(const c of r){const l=this.getFlowData(c),d=c.name;if(d&&(n.length>0&&(n+=` -`),n+=` // flow -> ${d} - `),n+=`${l.code} - `,c===i&&t!=="compute"){if(n+=`// result - - `,t==="vertex")n+=`varyings.Vertex = ${l.result};`;else if(t==="fragment")if(u)s.returnType=a.nodeType,n+=`return ${l.result};`;else{let h=" @location(0) color: vec4";const p=this.getBuiltins("output");p&&(h+=`, - `+p),s.returnType="OutputStruct",s.structs+=this._getWGSLStruct("OutputStruct",h),s.structs+=` -var output : OutputStruct; - -`,n+=`output.color = ${l.result}; - - return output;`}}}s.flow=n}this.material!==null?(this.vertexShader=this._getWGSLVertexCode(e.vertex),this.fragmentShader=this._getWGSLFragmentCode(e.fragment)):this.computeShader=this._getWGSLComputeCode(e.compute,(this.object.workgroupSize||[64]).join(", "))}getMethod(e,t=null){let s;return t!==null&&(s=this._getWGSLMethod(e+"_"+t)),s===void 0&&(s=this._getWGSLMethod(e)),s||e}getType(e){return XA[e]||e}isAvailable(e){let t=ul[e];return t===void 0&&(e==="float32Filterable"?t=this.renderer.hasFeature("float32-filterable"):e==="clipDistance"&&(t=this.renderer.hasFeature("clip-distances")),ul[e]=t),t}_getWGSLMethod(e){return it[e]!==void 0&&this._include(e),vn[e]}_include(e){const t=it[e];return t.build(this),this.currentFunctionNode!==null&&this.currentFunctionNode.includes.push(t),t}_getWGSLVertexCode(e){return`${this.getSignature()} -// directives -${e.directives} - -// uniforms -${e.uniforms} - -// varyings -${e.varyings} -var varyings : VaryingsStruct; - -// codes -${e.codes} - -@vertex -fn main( ${e.attributes} ) -> VaryingsStruct { - - // vars - ${e.vars} - - // flow - ${e.flow} - - return varyings; - -} -`}_getWGSLFragmentCode(e){return`${this.getSignature()} -// global -${Ng} - -// uniforms -${e.uniforms} - -// structs -${e.structs} - -// codes -${e.codes} - -@fragment -fn main( ${e.varyings} ) -> ${e.returnType} { - - // vars - ${e.vars} - - // flow - ${e.flow} - -} -`}_getWGSLComputeCode(e,t){return`${this.getSignature()} -// directives -${e.directives} - -// system -var instanceIndex : u32; - -// locals -${e.scopedArrays} - -// uniforms -${e.uniforms} - -// codes -${e.codes} - -@compute @workgroup_size( ${t} ) -fn main( ${e.attributes} ) { - - // system - instanceIndex = id.x + id.y * numWorkgroups.x * u32(${t}) + id.z * numWorkgroups.x * numWorkgroups.y * u32(${t}); - - // vars - ${e.vars} - - // flow - ${e.flow} - -} -`}_getWGSLStruct(e,t){return` -struct ${e} { -${t} -};`}_getWGSLStructBinding(e,t,s,n=0,r=0){const i=e+"Struct";return`${this._getWGSLStruct(i,t)} -@binding( ${n} ) @group( ${r} ) -var<${s}> ${e} : ${i};`}}class jA{constructor(e){this.backend=e}getCurrentDepthStencilFormat(e){let t;return e.depthTexture!==null?t=this.getTextureFormatGPU(e.depthTexture):e.depth&&e.stencil?t=_.Depth24PlusStencil8:e.depth&&(t=_.Depth24Plus),t}getTextureFormatGPU(e){return this.backend.get(e).format}getTextureSampleData(e){let t;if(e.isFramebufferTexture)t=1;else if(e.isDepthTexture&&!e.renderTarget){const r=this.backend.renderer,i=r.getRenderTarget();t=i?i.samples:r.samples}else e.renderTarget&&(t=e.renderTarget.samples);t=t||1;const s=t>1&&e.renderTarget!==null&&e.isDepthTexture!==!0&&e.isFramebufferTexture!==!0;return{samples:t,primarySamples:s?1:t,isMSAA:s}}getCurrentColorFormat(e){let t;return e.textures!==null?t=this.getTextureFormatGPU(e.textures[0]):t=this.getPreferredCanvasFormat(),t}getCurrentColorSpace(e){return e.textures!==null?e.textures[0].colorSpace:this.backend.renderer.outputColorSpace}getPrimitiveTopology(e,t){if(e.isPoints)return Ks.PointList;if(e.isLineSegments||e.isMesh&&t.wireframe===!0)return Ks.LineList;if(e.isLine)return Ks.LineStrip;if(e.isMesh)return Ks.TriangleList}getSampleCount(e){let t=1;return e>1&&(t=Math.pow(2,Math.floor(Math.log2(e))),t===2&&(t=4)),t}getSampleCountRenderContext(e){return e.textures!==null?this.getSampleCount(e.sampleCount):this.getSampleCount(this.backend.renderer.samples)}getPreferredCanvasFormat(){return navigator.userAgent.includes("Quest")?_.BGRA8Unorm:navigator.gpu.getPreferredCanvasFormat()}}const QA=new Map([[Int8Array,["sint8","snorm8"]],[Uint8Array,["uint8","unorm8"]],[Int16Array,["sint16","snorm16"]],[Uint16Array,["uint16","unorm16"]],[Int32Array,["sint32","snorm32"]],[Uint32Array,["uint32","unorm32"]],[Float32Array,["float32"]]]),ZA=new Map([[yl,["float16"]]]),JA=new Map([[Int32Array,"sint32"],[Int16Array,"sint32"],[Uint32Array,"uint32"],[Uint16Array,"uint32"],[Float32Array,"float32"]]);class eR{constructor(e){this.backend=e}createAttribute(e,t){const s=this._getBufferAttribute(e),n=this.backend,r=n.get(s);let i=r.buffer;if(i===void 0){const a=n.device;let u=s.array;if(e.normalized===!1&&(u.constructor===Int16Array||u.constructor===Uint16Array)){const l=new Uint32Array(u.length);for(let d=0;d1&&(u.multisampled=!0,i.texture.isDepthTexture||(u.sampleType=Is.UnfilterableFloat)),i.texture.isDepthTexture)u.sampleType=Is.Depth;else if(i.texture.isDataTexture||i.texture.isDataArrayTexture||i.texture.isData3DTexture){const l=i.texture.type;l===ze?u.sampleType=Is.SInt:l===Le?u.sampleType=Is.UInt:l===ot&&(this.backend.hasFeature("float32-filterable")?u.sampleType=Is.Float:u.sampleType=Is.UnfilterableFloat)}i.isSampledCubeTexture?u.viewDimension=$e.Cube:i.texture.isDataArrayTexture||i.texture.isCompressedArrayTexture?u.viewDimension=$e.TwoDArray:i.isSampledTexture3D&&(u.viewDimension=$e.ThreeD),a.texture=u}else console.error(`WebGPUBindingUtils: Unsupported binding "${i}".`);n.push(a)}return s.createBindGroupLayout({entries:n})}createBindings(e,t,s,n=0){const{backend:r,bindGroupLayoutCache:i}=this,a=r.get(e);let u=i.get(e.bindingsReference);u===void 0&&(u=this.createBindingsLayout(e),i.set(e.bindingsReference,u));let c;s>0&&(a.groups===void 0&&(a.groups=[],a.versions=[]),a.versions[s]===n&&(c=a.groups[s])),c===void 0&&(c=this.createBindGroup(e,u),s>0&&(a.groups[s]=c,a.versions[s]=n)),a.group=c,a.layout=u}updateBinding(e){const t=this.backend,s=t.device,n=e.buffer,r=t.get(e).buffer;s.queue.writeBuffer(r,0,n,0)}createBindGroup(e,t){const s=this.backend,n=s.device;let r=0;const i=[];for(const a of e.bindings){if(a.isUniformBuffer){const u=s.get(a);if(u.buffer===void 0){const c=a.byteLength,l=GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST,d=n.createBuffer({label:"bindingBuffer_"+a.name,size:c,usage:l});u.buffer=d}i.push({binding:r,resource:{buffer:u.buffer}})}else if(a.isStorageBuffer){const u=s.get(a);if(u.buffer===void 0){const c=a.attribute;u.buffer=s.get(c).buffer}i.push({binding:r,resource:{buffer:u.buffer}})}else if(a.isSampler){const u=s.get(a.texture);i.push({binding:r,resource:u.sampler})}else if(a.isSampledTexture){const u=s.get(a.texture);let c;if(u.externalTexture!==void 0)c=n.importExternalTexture({source:u.externalTexture});else{const l=a.store?1:u.texture.mipLevelCount,d=`view-${u.texture.width}-${u.texture.height}-${l}`;if(c=u[d],c===void 0){const h=BA.All;let p;a.isSampledCubeTexture?p=$e.Cube:a.isSampledTexture3D?p=$e.ThreeD:a.texture.isDataArrayTexture||a.texture.isCompressedArrayTexture?p=$e.TwoDArray:p=$e.TwoD,c=u[d]=u.texture.createView({aspect:h,dimension:p,mipLevelCount:l})}}i.push({binding:r,resource:c})}r++}return n.createBindGroup({label:"bindGroup_"+e.name,layout:t,entries:i})}}class sR{constructor(e){this.backend=e}_getSampleCount(e){return this.backend.utils.getSampleCountRenderContext(e)}createRenderPipeline(e,t){const{object:s,material:n,geometry:r,pipeline:i}=e,{vertexProgram:a,fragmentProgram:u}=i,c=this.backend,l=c.device,d=c.utils,h=c.get(i),p=[];for(const Ze of e.getBindings()){const Dt=c.get(Ze);p.push(Dt.layout)}const f=c.attributeUtils.createShaderVertexBuffers(e);let m;n.transparent===!0&&n.blending!==en&&(m=this._getBlending(n));let x={};n.stencilWrite===!0&&(x={compare:this._getStencilCompare(n),failOp:this._getStencilOperation(n.stencilFail),depthFailOp:this._getStencilOperation(n.stencilZFail),passOp:this._getStencilOperation(n.stencilZPass)});const N=this._getColorWriteMask(n),v=[];if(e.context.textures!==null){const Ze=e.context.textures;for(let Dt=0;Dt1},layout:l.createPipelineLayout({bindGroupLayouts:p})},ie={},Z=e.context.depth,Qe=e.context.stencil;if((Z===!0||Qe===!0)&&(Z===!0&&(ie.format=I,ie.depthWriteEnabled=n.depthWrite,ie.depthCompare=L),Qe===!0&&(ie.stencilFront=x,ie.stencilBack={},ie.stencilReadMask=n.stencilFuncMask,ie.stencilWriteMask=n.stencilWriteMask),X.depthStencil=ie),t===null)h.pipeline=l.createRenderPipeline(X);else{const Ze=new Promise(Dt=>{l.createRenderPipelineAsync(X).then(Ms=>{h.pipeline=Ms,Dt()})});t.push(Ze)}}createBundleEncoder(e){const t=this.backend,{utils:s,device:n}=t,r=s.getCurrentDepthStencilFormat(e),i=s.getCurrentColorFormat(e),a=this._getSampleCount(e),u={label:"renderBundleEncoder",colorFormats:[i],depthStencilFormat:r,sampleCount:a};return n.createRenderBundleEncoder(u)}createComputePipeline(e,t){const s=this.backend,n=s.device,r=s.get(e.computeProgram).module,i=s.get(e),a=[];for(const u of t){const c=s.get(u);a.push(c.layout)}i.pipeline=n.createComputePipeline({compute:r,layout:n.createPipelineLayout({bindGroupLayouts:a})})}_getBlending(e){let t,s;const n=e.blending,r=e.blendSrc,i=e.blendDst,a=e.blendEquation;if(n===Hl){const u=e.blendSrcAlpha!==null?e.blendSrcAlpha:r,c=e.blendDstAlpha!==null?e.blendDstAlpha:i,l=e.blendEquationAlpha!==null?e.blendEquationAlpha:a;t={srcFactor:this._getBlendFactor(r),dstFactor:this._getBlendFactor(i),operation:this._getBlendOperation(a)},s={srcFactor:this._getBlendFactor(u),dstFactor:this._getBlendFactor(c),operation:this._getBlendOperation(l)}}else{const u=e.premultipliedAlpha,c=(l,d,h,p)=>{t={srcFactor:l,dstFactor:d,operation:ls.Add},s={srcFactor:h,dstFactor:p,operation:ls.Add}};if(u)switch(n){case Ys:c(H.One,H.OneMinusSrcAlpha,H.One,H.OneMinusSrcAlpha);break;case qr:c(H.One,H.One,H.One,H.One);break;case Hr:c(H.Zero,H.OneMinusSrc,H.Zero,H.One);break;case $r:c(H.Zero,H.Src,H.Zero,H.SrcAlpha);break}else switch(n){case Ys:c(H.SrcAlpha,H.OneMinusSrcAlpha,H.One,H.OneMinusSrcAlpha);break;case qr:c(H.SrcAlpha,H.One,H.SrcAlpha,H.One);break;case Hr:c(H.Zero,H.OneMinusSrc,H.Zero,H.One);break;case $r:c(H.Zero,H.Src,H.Zero,H.Src);break}}if(t!==void 0&&s!==void 0)return{color:t,alpha:s};console.error("THREE.WebGPURenderer: Invalid blending: ",n)}_getBlendFactor(e){let t;switch(e){case $l:t=H.Zero;break;case Wl:t=H.One;break;case zl:t=H.Src;break;case Il:t=H.OneMinusSrc;break;case kl:t=H.SrcAlpha;break;case Ll:t=H.OneMinusSrcAlpha;break;case Gl:t=H.Dst;break;case Dl:t=H.OneMinusDstColor;break;case Vl:t=H.DstAlpha;break;case Pl:t=H.OneMinusDstAlpha;break;case Ol:t=H.SrcAlphaSaturated;break;case __:t=H.Constant;break;case b_:t=H.OneMinusConstant;break;default:console.error("THREE.WebGPURenderer: Blend factor not supported.",e)}return t}_getStencilCompare(e){let t;const s=e.stencilFunc;switch(s){case Im:t=Fe.Never;break;case Lm:t=Fe.Always;break;case Dm:t=Fe.Less;break;case Pm:t=Fe.LessEqual;break;case Um:t=Fe.Equal;break;case Fm:t=Fe.GreaterEqual;break;case Bm:t=Fe.Greater;break;case Mm:t=Fe.NotEqual;break;default:console.error("THREE.WebGPURenderer: Invalid stencil function.",s)}return t}_getStencilOperation(e){let t;switch(e){case Hm:t=Qt.Keep;break;case $m:t=Qt.Zero;break;case Wm:t=Qt.Replace;break;case zm:t=Qt.Invert;break;case km:t=Qt.IncrementClamp;break;case Om:t=Qt.DecrementClamp;break;case Gm:t=Qt.IncrementWrap;break;case Vm:t=Qt.DecrementWrap;break;default:console.error("THREE.WebGPURenderer: Invalid stencil operation.",t)}return t}_getBlendOperation(e){let t;switch(e){case Gs:t=ls.Add;break;case Ul:t=ls.Subtract;break;case Fl:t=ls.ReverseSubtract;break;case Km:t=ls.Min;break;case qm:t=ls.Max;break;default:console.error("THREE.WebGPUPipelineUtils: Blend equation not supported.",e)}return t}_getPrimitiveState(e,t,s){const n={},r=this.backend.utils;switch(n.topology=r.getPrimitiveTopology(e,s),t.index!==null&&e.isLine===!0&&e.isLineSegments!==!0&&(n.stripIndexFormat=t.index.array instanceof Uint16Array?cn.Uint16:cn.Uint32),s.side){case No:n.frontFace=go.CCW,n.cullMode=mo.Back;break;case Ct:n.frontFace=go.CCW,n.cullMode=mo.Front;break;case js:n.frontFace=go.CCW,n.cullMode=mo.None;break;default:console.error("THREE.WebGPUPipelineUtils: Unknown material.side value.",s.side);break}return n}_getColorWriteMask(e){return e.colorWrite===!0?rl.All:rl.None}_getDepthCompare(e){let t;if(e.depthTest===!1)t=Fe.Always;else{const s=e.depthFunc;switch(s){case Jl:t=Fe.Never;break;case Zl:t=Fe.Always;break;case Ql:t=Fe.Less;break;case jl:t=Fe.LessEqual;break;case Yl:t=Fe.Equal;break;case Xl:t=Fe.GreaterEqual;break;case Kl:t=Fe.Greater;break;case ql:t=Fe.NotEqual;break;default:console.error("THREE.WebGPUPipelineUtils: Invalid depth function.",s)}}return t}}class nR extends bg{constructor(e={}){super(e),this.isWebGPUBackend=!0,this.parameters.alpha=e.alpha===void 0?!0:e.alpha,this.parameters.requiredLimits=e.requiredLimits===void 0?{}:e.requiredLimits,this.trackTimestamp=e.trackTimestamp===!0,this.device=null,this.context=null,this.colorBuffer=null,this.defaultRenderPassdescriptor=null,this.utils=new jA(this),this.attributeUtils=new eR(this),this.bindingUtils=new tR(this),this.pipelineUtils=new sR(this),this.textureUtils=new OA(this),this.occludedResolveCache=new Map}async init(e){await super.init(e);const t=this.parameters;let s;if(t.device===void 0){const i={powerPreference:t.powerPreference},a=typeof navigator<"u"?await navigator.gpu.requestAdapter(i):null;if(a===null)throw new Error("WebGPUBackend: Unable to create WebGPU adapter.");const u=Object.values(ra),c=[];for(const d of u)a.features.has(d)&&c.push(d);const l={requiredFeatures:c,requiredLimits:t.requiredLimits};s=await a.requestDevice(l)}else s=t.device;s.lost.then(i=>{const a={api:"WebGPU",message:i.message||"Unknown reason",reason:i.reason||null,originalEvent:i};e.onDeviceLost(a)});const n=t.context!==void 0?t.context:e.domElement.getContext("webgpu");this.device=s,this.context=n;const r=t.alpha?"premultiplied":"opaque";this.trackTimestamp=this.trackTimestamp&&this.hasFeature(ra.TimestampQuery),this.context.configure({device:this.device,format:this.utils.getPreferredCanvasFormat(),usage:GPUTextureUsage.RENDER_ATTACHMENT|GPUTextureUsage.COPY_SRC,alphaMode:r}),this.updateSize()}get coordinateSystem(){return ln}async getArrayBufferAsync(e){return await this.attributeUtils.getArrayBufferAsync(e)}getContext(){return this.context}_getDefaultRenderPassDescriptor(){let e=this.defaultRenderPassdescriptor;if(e===null){const s=this.renderer;e={colorAttachments:[{view:null}]},(this.renderer.depth===!0||this.renderer.stencil===!0)&&(e.depthStencilAttachment={view:this.textureUtils.getDepthBuffer(s.depth,s.stencil).createView()});const n=e.colorAttachments[0];this.renderer.samples>0?n.view=this.colorBuffer.createView():n.resolveTarget=void 0,this.defaultRenderPassdescriptor=e}const t=e.colorAttachments[0];return this.renderer.samples>0?t.resolveTarget=this.context.getCurrentTexture().createView():t.view=this.context.getCurrentTexture().createView(),e}_getRenderPassDescriptor(e,t={}){const s=e.renderTarget,n=this.get(s);let r=n.descriptors;if(r===void 0||n.width!==s.width||n.height!==s.height||n.dimensions!==s.dimensions||n.activeMipmapLevel!==s.activeMipmapLevel||n.activeCubeFace!==e.activeCubeFace||n.samples!==s.samples||n.loadOp!==t.loadOp){r={},n.descriptors=r;const u=()=>{s.removeEventListener("dispose",u),this.delete(s)};s.addEventListener("dispose",u)}const i=e.getCacheKey();let a=r[i];if(a===void 0){const u=e.textures,c=[];let l;for(let d=0;d0&&(t.currentOcclusionQuerySet&&t.currentOcclusionQuerySet.destroy(),t.currentOcclusionQueryBuffer&&t.currentOcclusionQueryBuffer.destroy(),t.currentOcclusionQuerySet=t.occlusionQuerySet,t.currentOcclusionQueryBuffer=t.occlusionQueryBuffer,t.currentOcclusionQueryObjects=t.occlusionQueryObjects,r=s.createQuerySet({type:"occlusion",count:n,label:`occlusionQuerySet_${e.id}`}),t.occlusionQuerySet=r,t.occlusionQueryIndex=0,t.occlusionQueryObjects=new Array(n),t.lastOcclusionObject=null);let i;e.textures===null?i=this._getDefaultRenderPassDescriptor():i=this._getRenderPassDescriptor(e,{loadOp:ye.Load}),this.initTimestampQuery(e,i),i.occlusionQuerySet=r;const a=i.depthStencilAttachment;if(e.textures!==null){const l=i.colorAttachments;for(let d=0;d0&&t.currentPass.executeBundles(t.renderBundles),s>t.occlusionQueryIndex&&t.currentPass.endOcclusionQuery(),t.currentPass.end(),s>0){const n=s*8;let r=this.occludedResolveCache.get(n);r===void 0&&(r=this.device.createBuffer({size:n,usage:GPUBufferUsage.QUERY_RESOLVE|GPUBufferUsage.COPY_SRC}),this.occludedResolveCache.set(n,r));const i=this.device.createBuffer({size:n,usage:GPUBufferUsage.COPY_DST|GPUBufferUsage.MAP_READ});t.encoder.resolveQuerySet(t.occlusionQuerySet,0,s,r,0),t.encoder.copyBufferToBuffer(r,0,i,0,n),t.occlusionQueryBuffer=i,this.resolveOccludedAsync(e)}if(this.prepareTimestampBuffer(e,t.encoder),this.device.queue.submit([t.encoder.finish()]),e.textures!==null){const n=e.textures;for(let r=0;ra?(c.x=Math.min(t.dispatchCount,a),c.y=Math.ceil(t.dispatchCount/a)):c.x=t.dispatchCount,r.dispatchWorkgroups(c.x,c.y,c.z)}finishCompute(e){const t=this.get(e);t.passEncoderGPU.end(),this.prepareTimestampBuffer(e,t.cmdEncoderGPU),this.device.queue.submit([t.cmdEncoderGPU.finish()])}async waitForGPU(){await this.device.queue.onSubmittedWorkDone()}draw(e,t){const{object:s,context:n,pipeline:r}=e,i=e.getBindings(),a=this.get(n),u=this.get(r).pipeline,c=a.currentSets,l=a.currentPass,d=e.getDrawParameters();if(d===null)return;c.pipeline!==u&&(l.setPipeline(u),c.pipeline=u);const h=c.bindingGroups;for(let x=0,N=i.length;x1?0:B;f===!0?l.drawIndexed(N[B],F,x[B]/p.array.BYTES_PER_ELEMENT,0,L):l.draw(N[B],F,x[B],L)}}else if(f===!0){const{vertexCount:x,instanceCount:N,firstVertex:v}=d,w=e.getIndirect();if(w!==null){const B=this.get(w).buffer;l.drawIndexedIndirect(B,0)}else l.drawIndexed(x,N,v,0,0);t.update(s,x,N)}else{const{vertexCount:x,instanceCount:N,firstVertex:v}=d,w=e.getIndirect();if(w!==null){const B=this.get(w).buffer;l.drawIndirect(B,0)}else l.draw(x,N,v,0);t.update(s,x,N)}}needsRenderUpdate(e){const t=this.get(e),{object:s,material:n}=e,r=this.utils,i=r.getSampleCountRenderContext(e.context),a=r.getCurrentColorSpace(e.context),u=r.getCurrentColorFormat(e.context),c=r.getCurrentDepthStencilFormat(e.context),l=r.getPrimitiveTopology(s,n);let d=!1;return(t.material!==n||t.materialVersion!==n.version||t.transparent!==n.transparent||t.blending!==n.blending||t.premultipliedAlpha!==n.premultipliedAlpha||t.blendSrc!==n.blendSrc||t.blendDst!==n.blendDst||t.blendEquation!==n.blendEquation||t.blendSrcAlpha!==n.blendSrcAlpha||t.blendDstAlpha!==n.blendDstAlpha||t.blendEquationAlpha!==n.blendEquationAlpha||t.colorWrite!==n.colorWrite||t.depthWrite!==n.depthWrite||t.depthTest!==n.depthTest||t.depthFunc!==n.depthFunc||t.stencilWrite!==n.stencilWrite||t.stencilFunc!==n.stencilFunc||t.stencilFail!==n.stencilFail||t.stencilZFail!==n.stencilZFail||t.stencilZPass!==n.stencilZPass||t.stencilFuncMask!==n.stencilFuncMask||t.stencilWriteMask!==n.stencilWriteMask||t.side!==n.side||t.alphaToCoverage!==n.alphaToCoverage||t.sampleCount!==i||t.colorSpace!==a||t.colorFormat!==u||t.depthStencilFormat!==c||t.primitiveTopology!==l||t.clippingContextCacheKey!==e.clippingContextCacheKey)&&(t.material=n,t.materialVersion=n.version,t.transparent=n.transparent,t.blending=n.blending,t.premultipliedAlpha=n.premultipliedAlpha,t.blendSrc=n.blendSrc,t.blendDst=n.blendDst,t.blendEquation=n.blendEquation,t.blendSrcAlpha=n.blendSrcAlpha,t.blendDstAlpha=n.blendDstAlpha,t.blendEquationAlpha=n.blendEquationAlpha,t.colorWrite=n.colorWrite,t.depthWrite=n.depthWrite,t.depthTest=n.depthTest,t.depthFunc=n.depthFunc,t.stencilWrite=n.stencilWrite,t.stencilFunc=n.stencilFunc,t.stencilFail=n.stencilFail,t.stencilZFail=n.stencilZFail,t.stencilZPass=n.stencilZPass,t.stencilFuncMask=n.stencilFuncMask,t.stencilWriteMask=n.stencilWriteMask,t.side=n.side,t.alphaToCoverage=n.alphaToCoverage,t.sampleCount=i,t.colorSpace=a,t.colorFormat=u,t.depthStencilFormat=c,t.primitiveTopology=l,t.clippingContextCacheKey=e.clippingContextCacheKey,d=!0),d}getRenderCacheKey(e){const{object:t,material:s}=e,n=this.utils,r=e.context;return[s.transparent,s.blending,s.premultipliedAlpha,s.blendSrc,s.blendDst,s.blendEquation,s.blendSrcAlpha,s.blendDstAlpha,s.blendEquationAlpha,s.colorWrite,s.depthWrite,s.depthTest,s.depthFunc,s.stencilWrite,s.stencilFunc,s.stencilFail,s.stencilZFail,s.stencilZPass,s.stencilFuncMask,s.stencilWriteMask,s.side,n.getSampleCountRenderContext(r),n.getCurrentColorSpace(r),n.getCurrentColorFormat(r),n.getCurrentDepthStencilFormat(r),n.getPrimitiveTopology(t,s),e.getGeometryCacheKey(),e.clippingContextCacheKey].join()}createSampler(e){this.textureUtils.createSampler(e)}destroySampler(e){this.textureUtils.destroySampler(e)}createDefaultTexture(e){this.textureUtils.createDefaultTexture(e)}createTexture(e,t){this.textureUtils.createTexture(e,t)}updateTexture(e,t){this.textureUtils.updateTexture(e,t)}generateMipmaps(e){this.textureUtils.generateMipmaps(e)}destroyTexture(e){this.textureUtils.destroyTexture(e)}copyTextureToBuffer(e,t,s,n,r,i){return this.textureUtils.copyTextureToBuffer(e,t,s,n,r,i)}initTimestampQuery(e,t){if(!this.trackTimestamp)return;const s=this.get(e);if(!s.timeStampQuerySet){const n=e.isComputeNode?"compute":"render",r=this.device.createQuerySet({type:"timestamp",count:2,label:`timestamp_${n}_${e.id}`});Object.assign(t,{timestampWrites:{querySet:r,beginningOfPassWriteIndex:0,endOfPassWriteIndex:1}}),s.timeStampQuerySet=r}}prepareTimestampBuffer(e,t){if(!this.trackTimestamp)return;const s=this.get(e),n=2*BigInt64Array.BYTES_PER_ELEMENT;s.currentTimestampQueryBuffers===void 0&&(s.currentTimestampQueryBuffers={resolveBuffer:this.device.createBuffer({label:"timestamp resolve buffer",size:n,usage:GPUBufferUsage.QUERY_RESOLVE|GPUBufferUsage.COPY_SRC}),resultBuffer:this.device.createBuffer({label:"timestamp result buffer",size:n,usage:GPUBufferUsage.COPY_DST|GPUBufferUsage.MAP_READ})});const{resolveBuffer:r,resultBuffer:i}=s.currentTimestampQueryBuffers;t.resolveQuerySet(s.timeStampQuerySet,0,2,r,0),i.mapState==="unmapped"&&t.copyBufferToBuffer(r,0,i,0,n)}async resolveTimestampAsync(e,t="render"){if(!this.trackTimestamp)return;const s=this.get(e);if(s.currentTimestampQueryBuffers===void 0)return;const{resultBuffer:n}=s.currentTimestampQueryBuffers;n.mapState==="unmapped"&&n.mapAsync(GPUMapMode.READ).then(()=>{const r=new BigUint64Array(n.getMappedRange()),i=Number(r[1]-r[0])/1e6;this.renderer.info.updateTimestamp(t,i),n.unmap()})}createNodeBuilder(e,t){return new YA(e,t)}createProgram(e){const t=this.get(e);t.module={module:this.device.createShaderModule({code:e.code,label:e.stage+(e.name!==""?`_${e.name}`:"")}),entryPoint:"main"}}destroyProgram(e){this.delete(e)}createRenderPipeline(e,t){this.pipelineUtils.createRenderPipeline(e,t)}createComputePipeline(e,t){this.pipelineUtils.createComputePipeline(e,t)}beginBundle(e){const t=this.get(e);t._currentPass=t.currentPass,t._currentSets=t.currentSets,t.currentSets={attributes:{},bindingGroups:[],pipeline:null,index:null},t.currentPass=this.pipelineUtils.createBundleEncoder(e)}finishBundle(e,t){const s=this.get(e),r=s.currentPass.finish();this.get(t).bundleGPU=r,s.currentSets=s._currentSets,s.currentPass=s._currentPass}addBundle(e,t){this.get(e).renderBundles.push(this.get(t).bundleGPU)}createBindings(e,t,s,n){this.bindingUtils.createBindings(e,t,s,n)}updateBindings(e,t,s,n){this.bindingUtils.createBindings(e,t,s,n)}updateBinding(e){this.bindingUtils.updateBinding(e)}createIndexAttribute(e){this.attributeUtils.createAttribute(e,GPUBufferUsage.INDEX|GPUBufferUsage.COPY_SRC|GPUBufferUsage.COPY_DST)}createAttribute(e){this.attributeUtils.createAttribute(e,GPUBufferUsage.VERTEX|GPUBufferUsage.COPY_SRC|GPUBufferUsage.COPY_DST)}createStorageAttribute(e){this.attributeUtils.createAttribute(e,GPUBufferUsage.STORAGE|GPUBufferUsage.VERTEX|GPUBufferUsage.COPY_SRC|GPUBufferUsage.COPY_DST)}createIndirectStorageAttribute(e){this.attributeUtils.createAttribute(e,GPUBufferUsage.STORAGE|GPUBufferUsage.INDIRECT|GPUBufferUsage.COPY_SRC|GPUBufferUsage.COPY_DST)}updateAttribute(e){this.attributeUtils.updateAttribute(e)}destroyAttribute(e){this.attributeUtils.destroyAttribute(e)}updateSize(){this.colorBuffer=this.textureUtils.getColorBuffer(),this.defaultRenderPassdescriptor=null}getMaxAnisotropy(){return 16}hasFeature(e){return this.device.features.has(e)}copyTextureToTexture(e,t,s=null,n=null,r=0){let i=0,a=0,u=0,c=0,l=0,d=0,h=e.image.width,p=e.image.height;s!==null&&(c=s.x,l=s.y,d=s.z||0,h=s.width,p=s.height),n!==null&&(i=n.x,a=n.y,u=n.z||0);const f=this.device.createCommandEncoder({label:"copyTextureToTexture_"+e.id+"_"+t.id}),m=this.get(e).texture,x=this.get(t).texture;f.copyTextureToTexture({texture:m,mipLevel:r,origin:{x:c,y:l,z:d}},{texture:x,mipLevel:r,origin:{x:i,y:a,z:u}},[h,p,1]),this.device.queue.submit([f.finish()])}copyFramebufferToTexture(e,t,s){const n=this.get(t);let r=null;t.renderTarget?e.isDepthTexture?r=this.get(t.depthTexture).texture:r=this.get(t.textures[0]).texture:e.isDepthTexture?r=this.textureUtils.getDepthBuffer(t.depth,t.stencil):r=this.context.getCurrentTexture();const i=this.get(e).texture;if(r.format!==i.format){console.error("WebGPUBackend: copyFramebufferToTexture: Source and destination formats do not match.",r.format,i.format);return}let a;if(n.currentPass?(n.currentPass.end(),a=n.encoder):a=this.device.createCommandEncoder({label:"copyFramebufferToTexture_"+e.id}),a.copyTextureToTexture({texture:r,origin:[s.x,s.y,0]},{texture:i},[s.z,s.w]),e.generateMipmaps&&this.textureUtils.generateMipmaps(e),n.currentPass){const{descriptor:u}=n;for(let c=0;c(console.warn("THREE.WebGPURenderer: WebGPU is not available, running under WebGL2 backend."),new nl(e)));const s=new t(e);super(s,e),this.library=new iR,this.isWebGPURenderer=!0}}class xR extends ll{constructor(){super(),this.isBundleGroup=!0,this.type="BundleGroup",this.static=!0,this.version=0}set needsUpdate(e){e===!0&&this.version++}}const Sg=new ce,Fr=new Mi(Sg);class TR{constructor(e,t=P(0,0,1,1)){this.renderer=e,this.outputNode=t,this.outputColorTransform=!0,this.needsUpdate=!0,Sg.name="PostProcessing"}render(){this._update();const e=this.renderer,t=e.toneMapping,s=e.outputColorSpace;e.toneMapping=ss,e.outputColorSpace=kt,Fr.render(e),e.toneMapping=t,e.outputColorSpace=s}_update(){if(this.needsUpdate===!0){const e=this.renderer,t=e.toneMapping,s=e.outputColorSpace;Fr.material.fragmentNode=this.outputColorTransform===!0?tu(this.outputNode,t,s):this.outputNode.context({toneMapping:t,outputColorSpace:s}),Fr.material.needsUpdate=!0,this.needsUpdate=!1}}async renderAsync(){this._update();const e=this.renderer,t=e.toneMapping,s=e.outputColorSpace;e.toneMapping=ss,e.outputColorSpace=kt,await Fr.renderAsync(e),e.toneMapping=t,e.outputColorSpace=s}}class _R extends aa{constructor(e=1,t=1){super(),this.image={width:e,height:t},this.magFilter=ft,this.minFilter=ft,this.isStorageTexture=!0}}class bR extends xf{constructor(e,t){super(e,t,Uint32Array),this.isIndirectStorageBufferAttribute=!0}}class oR extends Lg{constructor(e){super(e),this.textures={},this.nodes={}}load(e,t,s,n){const r=new Ig(this.manager);r.setPath(this.path),r.setRequestHeader(this.requestHeader),r.setWithCredentials(this.withCredentials),r.load(e,i=>{try{t(this.parse(JSON.parse(i)))}catch(a){n?n(a):console.error(a),this.manager.itemError(e)}},s,n)}parseNodes(e){const t={};if(e!==void 0){for(const n of e){const{uuid:r,type:i}=n;t[r]=this.createNodeFromType(i),t[r].uuid=r}const s={nodes:t,textures:this.textures};for(const n of e)n.meta=s,t[n.uuid].deserialize(n),delete n.meta}return t}parse(e){const t=this.createNodeFromType(e.type);t.uuid=e.uuid;const n={nodes:this.parseNodes(e.nodes),textures:this.textures};return e.meta=n,t.deserialize(e),delete e.meta,t}setTextures(e){return this.textures=e,this}setNodes(e){return this.nodes=e,this}createNodeFromType(e){return this.nodes[e]===void 0?(console.error("THREE.NodeLoader: Node type not found:",e),g()):E(new this.nodes[e])}}class aR extends Vg{constructor(e){super(e),this.nodes={},this.nodeMaterials={}}parse(e){const t=super.parse(e),s=this.nodes,n=e.inputNodes;for(const r in n){const i=n[r];t[r]=s[i]}return t}setNodes(e){return this.nodes=e,this}setNodeMaterials(e){return this.nodeMaterials=e,this}createMaterialFromType(e){const t=this.nodeMaterials[e];return t!==void 0?new t:super.createMaterialFromType(e)}}class NR extends Gg{constructor(e){super(e),this.nodes={},this.nodeMaterials={},this._nodesJSON=null}setNodes(e){return this.nodes=e,this}setNodeMaterials(e){return this.nodeMaterials=e,this}parse(e,t){this._nodesJSON=e.nodes;const s=super.parse(e,t);return this._nodesJSON=null,s}parseNodes(e,t){if(e!==void 0){const s=new oR;return s.setNodes(this.nodes),s.setTextures(t),s.parseNodes(e)}return{}}parseMaterials(e,t){const s={};if(e!==void 0){const n=this.parseNodes(this._nodesJSON,t),r=new aR;r.setTextures(t),r.setNodes(n),r.setNodeMaterials(this.nodeMaterials);for(let i=0,a=e.length;i '),Ie=b(''),qe=b(''),ze=b(''),Ce=b(''),Le=b(' '),Se=b(''),Te=b(''),Fe=b('
'),Me=b('
');function Ne(te,s){ge(s,!0);let k=H(s,"value",15),K=H(s,"placeholder",3,"Select…"),ne=H(s,"class",3,""),o=C(!1),A=C(void 0),h=C(void 0),i=C(-1),T="",U,x=xe(()=>s.options.find(e=>e.value===k()));function W(e){var t,n;k(e.value),(t=s.onChange)==null||t.call(s,e.value),I(),(n=a(A))==null||n.focus()}function se(){a(o)?I():E()}function E(){c(o,!0),c(i,Math.max(0,s.options.findIndex(e=>e.value===k())),!0),requestAnimationFrame(()=>{var e,t;(t=(e=a(h))==null?void 0:e.querySelectorAll('[role="option"]')[a(i)])==null||t.scrollIntoView({block:"nearest"})})}function I(){c(o,!1),c(i,-1)}function le(e){if(e.key==="ArrowDown"||e.key==="Enter"||e.key===" ")e.preventDefault(),a(o)?B(1):E();else if(e.key==="ArrowUp")e.preventDefault(),a(o)?B(-1):E();else if(e.key==="Escape")I();else if(e.key==="Home"&&a(o))e.preventDefault(),c(i,0);else if(e.key==="End"&&a(o))e.preventDefault(),c(i,s.options.length-1);else if(a(o)&&e.key==="Enter"&&a(i)>=0)e.preventDefault(),W(s.options[a(i)]);else if(e.key.length===1&&/\S/.test(e.key)){a(o)||E(),clearTimeout(U),T+=e.key.toLowerCase(),U=setTimeout(()=>T="",600);const t=s.options.findIndex(n=>n.label.toLowerCase().startsWith(T));t>=0&&c(i,t,!0)}}function B(e){const t=s.options.length;c(i,(a(i)+e+t)%t),requestAnimationFrame(()=>{var n,l;(l=(n=a(h))==null?void 0:n.querySelectorAll('[role="option"]')[a(i)])==null||l.scrollIntoView({block:"nearest"})})}ke(()=>{if(!a(o))return;function e(t){var n,l;!((n=a(A))!=null&&n.contains(t.target))&&!((l=a(h))!=null&&l.contains(t.target))&&I()}return document.addEventListener("click",e,!0),()=>document.removeEventListener("click",e,!0)});var q=Me(),G=u(q);{var re=e=>{var t=Ee(),n=u(t,!0);v(t),g(()=>L(n,s.label)),f(e,t)};p(G,e=>{s.label&&e(re)})}var m=_(G,2);let J;var N=u(m);{var oe=e=>{var t=Ie(),n=u(t);S(n,{get name(){return s.icon},size:15}),v(t),f(e,t)};p(N,e=>{s.icon&&e(oe)})}var z=_(N,2);let O;var P=u(z);{var de=e=>{var t=qe();g(()=>ee(t,`background:${a(x).color??""}`)),f(e,t)};p(P,e=>{var t;(t=a(x))!=null&&t.color&&e(de)})}var ie=_(P);v(z);var F=_(z,2);let Q;var ve=u(F);S(ve,{name:"chevron",size:14}),v(F),v(m),ae(m,e=>c(A,e),()=>a(A));var ce=_(m,2);{var ue=e=>{var t=Fe();Ae(t,23,()=>s.options,n=>n.value,(n,l,R)=>{var y=Te();let X;var Y=u(y);{var fe=r=>{var d=ze(),w=u(d);S(w,{get name(){return a(l).icon},size:15}),v(d),f(r,d)};p(Y,r=>{a(l).icon&&r(fe)})}var Z=_(Y,2);{var be=r=>{var d=Ce();g(()=>ee(d,`background:${a(l).color??""}`)),f(r,d)};p(Z,r=>{a(l).color&&r(be)})}var M=_(Z,2),_e=u(M,!0);v(M);var $=_(M,2);{var me=r=>{var d=Le(),w=u(d,!0);v(d),g(()=>L(w,a(l).badge)),f(r,d)};p($,r=>{a(l).badge!==void 0&&r(me)})}var pe=_($,2);{var ye=r=>{var d=Se(),w=u(d);S(w,{name:"sparkle",size:12}),v(d),f(r,d)};p(pe,r=>{a(l).value===k()&&r(ye)})}v(y),g(()=>{j(y,"aria-selected",a(l).value===k()),X=D(y,1,"dd-option svelte-1fd3ybn",null,X,{"dd-active":a(R)===a(i),"dd-selected":a(l).value===k()}),L(_e,a(l).label)}),De("mouseenter",y,()=>c(i,a(R),!0)),V("click",y,()=>W(a(l))),f(n,y)}),v(t),ae(t,n=>c(h,n),()=>a(h)),g(()=>j(t,"aria-label",s.label??K())),f(e,t)};p(ce,e=>{a(o)&&e(ue)})}v(q),g(()=>{D(q,1,`dd ${ne()??""}`,"svelte-1fd3ybn"),J=D(m,1,"dd-trigger svelte-1fd3ybn",null,J,{"dd-open":a(o)}),j(m,"aria-expanded",a(o)),O=D(z,1,"dd-value svelte-1fd3ybn",null,O,{"dd-placeholder":!a(x)}),L(ie,` ${(a(x)?a(x).label:K())??""}`),Q=D(F,1,"dd-chevron svelte-1fd3ybn",null,Q,{"dd-chevron-open":a(o)})}),V("click",m,se),V("keydown",m,le),f(te,q),he()}we(["click","keydown"]);export{Ne as D}; diff --git a/apps/dashboard/build/_app/immutable/chunks/CmbJHhgy.js.br b/apps/dashboard/build/_app/immutable/chunks/CmbJHhgy.js.br deleted file mode 100644 index e0c34cb..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/CmbJHhgy.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CmbJHhgy.js.gz b/apps/dashboard/build/_app/immutable/chunks/CmbJHhgy.js.gz deleted file mode 100644 index b2dc537..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/CmbJHhgy.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CnZzd20v.js.br b/apps/dashboard/build/_app/immutable/chunks/CnZzd20v.js.br deleted file mode 100644 index 9ca395f..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/CnZzd20v.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CnZzd20v.js.gz b/apps/dashboard/build/_app/immutable/chunks/CnZzd20v.js.gz deleted file mode 100644 index 7c30757..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/CnZzd20v.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CqMQEF-F.js b/apps/dashboard/build/_app/immutable/chunks/CqMQEF-F.js deleted file mode 100644 index 7367f60..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/CqMQEF-F.js +++ /dev/null @@ -1 +0,0 @@ -import{w as N,g as T}from"./D8mhvFt8.js";import{e as E}from"./BhIgFntf.js";import{E as p}from"./CcUbQ_Wl.js";const y=4,R=5500,F=1500;function x(){const{subscribe:b,update:u}=N([]);let m=1,f=0;const c=new Map,a=new Map,l=new Map;function w(e,s){l.set(e,Date.now());const t=setTimeout(()=>{c.delete(e),l.delete(e),h(e)},s);c.set(e,t)}function g(e){const s=m++,t=Date.now(),o={id:s,createdAt:t,...e};u(n=>{const r=[o,...n];if(r.length>y){for(const i of r.slice(y)){const d=c.get(i.id);d&&clearTimeout(d),c.delete(i.id),a.delete(i.id),l.delete(i.id)}return r.slice(0,y)}return r}),w(s,e.dwellMs)}function h(e){const s=c.get(e);s&&(clearTimeout(s),c.delete(e)),a.delete(e),l.delete(e),u(t=>t.filter(o=>o.id!==e))}function D(e,s){const t=c.get(e);if(!t)return;clearTimeout(t),c.delete(e);const o=l.get(e)??Date.now(),n=Date.now()-o,r=Math.max(200,s-n);a.set(e,{remaining:r})}function C(e){const s=a.get(e);s&&(a.delete(e),w(e,s.remaining))}function S(){for(const e of c.values())clearTimeout(e);c.clear(),a.clear(),l.clear(),u(()=>[])}function _(e){const s=p[e.type]??"#818CF8",t=e.data;switch(e.type){case"DreamCompleted":{const o=Number(t.memories_replayed??0),n=Number(t.connections_found??0),r=Number(t.insights_generated??0),i=Number(t.duration_ms??0),d=[];return d.push(`Replayed ${o} ${o===1?"memory":"memories"}`),n>0&&d.push(`${n} new connection${n===1?"":"s"}`),r>0&&d.push(`${r} insight${r===1?"":"s"}`),{type:e.type,title:"Dream consolidated",body:`${d.join(" · ")} in ${(i/1e3).toFixed(1)}s`,color:s,dwellMs:7e3}}case"ConsolidationCompleted":{const o=Number(t.nodes_processed??0),n=Number(t.decay_applied??0),r=Number(t.embeddings_generated??0),i=Number(t.duration_ms??0),d=[];return n>0&&d.push(`${n} decayed`),r>0&&d.push(`${r} embedded`),{type:e.type,title:"Consolidation swept",body:`${o} node${o===1?"":"s"}${d.length?" · "+d.join(" · "):""} in ${(i/1e3).toFixed(1)}s`,color:s,dwellMs:6e3}}case"ConnectionDiscovered":{const o=Date.now();if(o-f0?`suppression #${o} · Rac1 cascade ~${n} neighbors`:`suppression #${o}`,color:s,dwellMs:5500}}case"MemoryUnsuppressed":{const o=Number(t.remaining_count??0);return{type:e.type,title:"Recovered",body:o>0?`${o} suppression${o===1?"":"s"} remain`:"fully unsuppressed",color:s,dwellMs:5e3}}case"Rac1CascadeSwept":{const o=Number(t.seeds??0),n=Number(t.neighbors_affected??0);return{type:e.type,title:"Rac1 cascade",body:`${o} seed${o===1?"":"s"} · ${n} dendritic spine${n===1?"":"s"} pruned`,color:s,dwellMs:6e3}}case"MemoryDeleted":return{type:e.type,title:"Memory deleted",body:String(t.id??"").slice(0,8),color:s,dwellMs:4e3};case"HookVerdictRecorded":{const o=String(t.verdict??"NOTE"),n=String(t.reason??"Sanhedrin receipt updated");return{type:e.type,title:`Sanhedrin ${o}`,body:n,color:s,dwellMs:o==="VETO"?8e3:R}}case"Heartbeat":case"SearchPerformed":case"RetentionDecayed":case"ActivationSpread":case"ImportanceScored":case"MemoryCreated":case"MemoryUpdated":case"DreamStarted":case"DreamProgress":case"ConsolidationStarted":case"Connected":return null;default:return null}}let M=null;return E.subscribe(e=>{if(e.length===0)return;const s=[];for(const t of e){if(t===M)break;s.push(t)}if(s.length!==0){M=e[0];for(let t=s.length-1;t>=0;t--){const o=_(s[t]);o&&g(o)}}}),{subscribe:b,dismiss:h,clear:S,pauseDwell:D,resumeDwell:C,push:g}}const $=x();function I(){[{type:"DreamCompleted",title:"Dream consolidated",body:"Replayed 127 memories · 43 new connections · 5 insights in 2.4s",color:p.DreamCompleted,dwellMs:7e3},{type:"ConnectionDiscovered",title:"Bridge discovered",body:"semantic · weight 0.87",color:p.ConnectionDiscovered,dwellMs:4500},{type:"MemorySuppressed",title:"Forgetting",body:"suppression #2 · Rac1 cascade ~8 neighbors",color:p.MemorySuppressed,dwellMs:5500},{type:"ConsolidationCompleted",title:"Consolidation swept",body:"892 nodes · 156 decayed · 48 embedded in 1.1s",color:p.ConsolidationCompleted,dwellMs:6e3}].forEach((u,m)=>{setTimeout(()=>{$.push(u)},m*800)}),T($)}export{I as f,$ as t}; diff --git a/apps/dashboard/build/_app/immutable/chunks/CqMQEF-F.js.br b/apps/dashboard/build/_app/immutable/chunks/CqMQEF-F.js.br deleted file mode 100644 index 3a8183c..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/CqMQEF-F.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CqMQEF-F.js.gz b/apps/dashboard/build/_app/immutable/chunks/CqMQEF-F.js.gz deleted file mode 100644 index 1fcfe84..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/CqMQEF-F.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..33b1dfb 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{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..bf30bd0 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/D7A-gG4Z.js b/apps/dashboard/build/_app/immutable/chunks/D7A-gG4Z.js deleted file mode 100644 index a7eb8ff..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/D7A-gG4Z.js +++ /dev/null @@ -1 +0,0 @@ -import"./Bzak7iHL.js";import{t as k,G as f,T as v,ai as C,av as g,M as x,I as _,J as w,aw as A,ax as z,ay as u,L,az as H,aA as V,aB as E,N as d,a as N,au as T,e as b,r as I}from"./wpu9U-D0.js";import{a as M,s as O}from"./60_R_Vbt.js";import{p as n}from"./ByYB047u.js";function Z(h,c,t=!1,l=!1,p=!1){var a=h,r="";k(()=>{var o=C;if(r===(r=c()??"")){f&&v();return}if(o.nodes!==null&&(g(o.nodes.start,o.nodes.end),o.nodes=null),r!==""){if(f){x.data;for(var e=v(),y=e;e!==null&&(e.nodeType!==_||e.data!=="");)y=e,e=w(e);if(e===null)throw A(),z;u(x,y),a=L(e);return}var m=t?V:l?E:void 0,s=H(t?"svg":l?"math":"template",m);s.innerHTML=r;var i=t||l?s:s.content;if(u(d(i),i.lastChild),t||l)for(;d(i);)a.before(d(i));else a.before(i)}})}const R={blackbox:'',memorypr:'',graph:'',reasoning:'',memories:'',timeline:'',feed:'',explore:'',activation:'',dreams:'',schedule:'',importance:'',duplicates:'',contradictions:'',patterns:'',intentions:'',stats:'',settings:'',command:'',search:'',filter:'',sparkle:'',chevron:'',close:'',pulse:'',logo:''};var S=T('');function W(h,c){let t=n(c,"size",3,20),l=n(c,"strokeWidth",3,1.6),p=n(c,"class",3,""),a=n(c,"draw",3,!1);var r=S(),o=b(r);Z(o,()=>R[c.name],!0),I(r),k(()=>{M(r,"width",t()),M(r,"height",t()),M(r,"stroke-width",l()),O(r,0,`vestige-icon ${a()?"vestige-icon-draw":""} ${p()??""}`,"svelte-1eqehiz")}),N(h,r)}export{W as I}; diff --git a/apps/dashboard/build/_app/immutable/chunks/D7A-gG4Z.js.br b/apps/dashboard/build/_app/immutable/chunks/D7A-gG4Z.js.br deleted file mode 100644 index 009a8a4..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/D7A-gG4Z.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/D7A-gG4Z.js.gz b/apps/dashboard/build/_app/immutable/chunks/D7A-gG4Z.js.gz deleted file mode 100644 index d5e541c..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/D7A-gG4Z.js.gz and /dev/null 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..81cd83a 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/D8mhvFt8.js b/apps/dashboard/build/_app/immutable/chunks/D8mhvFt8.js deleted file mode 100644 index def1896..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/D8mhvFt8.js +++ /dev/null @@ -1,2 +0,0 @@ -var qe=Object.defineProperty;var pe=t=>{throw TypeError(t)};var xe=(t,e,s)=>e in t?qe(t,e,{enumerable:!0,configurable:!0,writable:!0,value:s}):t[e]=s;var J=(t,e,s)=>xe(t,typeof e!="symbol"?e+"":e,s),ie=(t,e,s)=>e.has(t)||pe("Cannot "+s);var r=(t,e,s)=>(ie(t,e,"read from private field"),s?s.call(t):e.get(t)),_=(t,e,s)=>e.has(t)?pe("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(t):e.set(t,s),f=(t,e,s,i)=>(ie(t,e,"write to private field"),i?i.call(t,s):e.set(t,s),s),b=(t,e,s)=>(ie(t,e,"access private method"),s);import{aX as Be,g as me,Q as Ce,P as Re,aY as ge,R as z,aL as Se,M as Y,G as k,ai as H,aZ as be,q as He,T as Pe,W as Ve,a_ as ye,$ as M,D as Ae,a$ as ne,_ as ae,a0 as We,b0 as ve,b1 as $e,b2 as Ee,b3 as ze,b4 as je,b5 as U,b6 as ee,b7 as we,b8 as Je,b9 as De,a2 as Ne,aF as Qe,Z as fe,L as te,n as Xe,X as Ze,ba as Q,E as Ge,F as Ke,bb as Ue,bc as et,y as tt,bd as st,ad as rt,be as oe,N as it,I as Fe,V as nt,J as at,ax as ue,K as X,bf as ft,aQ as ot,bg as ut,aI as ct,p as ht,ay as lt,aE as dt,aw as _t,b as pt,ab as P,aa as gt,a4 as bt}from"./wpu9U-D0.js";function yt(t){let e=0,s=Se(0),i;return()=>{Be()&&(me(s),Ce(()=>(e===0&&(i=Re(()=>t(()=>ge(s)))),e+=1,()=>{z(()=>{e-=1,e===0&&(i==null||i(),i=void 0,ge(s))})})))}}var vt=Ge|Ke;function Et(t,e,s,i){new wt(t,e,s,i)}var E,j,S,x,y,A,w,m,F,B,I,V,W,$,O,se,l,Oe,Ie,Le,ce,G,K,he;class wt{constructor(e,s,i,o){_(this,l);J(this,"parent");J(this,"is_pending",!1);J(this,"transform_error");_(this,E);_(this,j,k?Y:null);_(this,S);_(this,x);_(this,y);_(this,A,null);_(this,w,null);_(this,m,null);_(this,F,null);_(this,B,0);_(this,I,0);_(this,V,!1);_(this,W,new Set);_(this,$,new Set);_(this,O,null);_(this,se,yt(()=>(f(this,O,Se(r(this,B))),()=>{f(this,O,null)})));var n;f(this,E,e),f(this,S,s),f(this,x,u=>{var h=H;h.b=this,h.f|=be,i(u)}),this.parent=H.b,this.transform_error=o??((n=this.parent)==null?void 0:n.transform_error)??(u=>u),f(this,y,He(()=>{if(k){const u=r(this,j);Pe();const h=u.data===Ve;if(u.data.startsWith(ye)){const a=JSON.parse(u.data.slice(ye.length));b(this,l,Ie).call(this,a)}else h?b(this,l,Le).call(this):b(this,l,Oe).call(this)}else b(this,l,ce).call(this)},vt)),k&&f(this,E,Y)}defer_effect(e){je(e,r(this,W),r(this,$))}is_rendered(){return!this.is_pending&&(!this.parent||this.parent.is_rendered())}has_pending_snippet(){return!!r(this,S).pending}update_pending_count(e){b(this,l,he).call(this,e),f(this,B,r(this,B)+e),!(!r(this,O)||r(this,V))&&(f(this,V,!0),z(()=>{f(this,V,!1),r(this,O)&&Qe(r(this,O),r(this,B))}))}get_effect_pending(){return r(this,se).call(this),me(r(this,O))}error(e){var s=r(this,S).onerror;let i=r(this,S).failed;if(!s&&!i)throw e;r(this,A)&&(fe(r(this,A)),f(this,A,null)),r(this,w)&&(fe(r(this,w)),f(this,w,null)),r(this,m)&&(fe(r(this,m)),f(this,m,null)),k&&(te(r(this,j)),Xe(),te(Ze()));var o=!1,n=!1;const u=()=>{if(o){et();return}o=!0,n&&Ue(),r(this,m)!==null&&ae(r(this,m),()=>{f(this,m,null)}),b(this,l,K).call(this,()=>{ne.ensure(),b(this,l,ce).call(this)})},h=c=>{try{n=!0,s==null||s(c,u),n=!1}catch(a){Q(a,r(this,y)&&r(this,y).parent)}i&&f(this,m,b(this,l,K).call(this,()=>{ne.ensure();try{return M(()=>{var a=H;a.b=this,a.f|=be,i(r(this,E),()=>c,()=>u)})}catch(a){return Q(a,r(this,y).parent),null}}))};z(()=>{var c;try{c=this.transform_error(e)}catch(a){Q(a,r(this,y)&&r(this,y).parent);return}c!==null&&typeof c=="object"&&typeof c.then=="function"?c.then(h,a=>Q(a,r(this,y)&&r(this,y).parent)):h(c)})}}E=new WeakMap,j=new WeakMap,S=new WeakMap,x=new WeakMap,y=new WeakMap,A=new WeakMap,w=new WeakMap,m=new WeakMap,F=new WeakMap,B=new WeakMap,I=new WeakMap,V=new WeakMap,W=new WeakMap,$=new WeakMap,O=new WeakMap,se=new WeakMap,l=new WeakSet,Oe=function(){try{f(this,A,M(()=>r(this,x).call(this,r(this,E))))}catch(e){this.error(e)}},Ie=function(e){const s=r(this,S).failed;s&&f(this,m,M(()=>{s(r(this,E),()=>e,()=>()=>{})}))},Le=function(){const e=r(this,S).pending;e&&(this.is_pending=!0,f(this,w,M(()=>e(r(this,E)))),z(()=>{var s=f(this,F,document.createDocumentFragment()),i=Ae();s.append(i),f(this,A,b(this,l,K).call(this,()=>(ne.ensure(),M(()=>r(this,x).call(this,i))))),r(this,I)===0&&(r(this,E).before(s),f(this,F,null),ae(r(this,w),()=>{f(this,w,null)}),b(this,l,G).call(this))}))},ce=function(){try{if(this.is_pending=this.has_pending_snippet(),f(this,I,0),f(this,B,0),f(this,A,M(()=>{r(this,x).call(this,r(this,E))})),r(this,I)>0){var e=f(this,F,document.createDocumentFragment());We(r(this,A),e);const s=r(this,S).pending;f(this,w,M(()=>s(r(this,E))))}else b(this,l,G).call(this)}catch(s){this.error(s)}},G=function(){this.is_pending=!1;for(const e of r(this,W))ve(e,$e),Ee(e);for(const e of r(this,$))ve(e,ze),Ee(e);r(this,W).clear(),r(this,$).clear()},K=function(e){var s=H,i=De,o=Ne;U(r(this,y)),ee(r(this,y)),we(r(this,y).ctx);try{return e()}catch(n){return Je(n),null}finally{U(s),ee(i),we(o)}},he=function(e){var s;if(!this.has_pending_snippet()){this.parent&&b(s=this.parent,l,he).call(s,e);return}f(this,I,r(this,I)+e),r(this,I)===0&&(b(this,l,G).call(this),r(this,w)&&ae(r(this,w),()=>{f(this,w,null)}),r(this,F)&&(r(this,E).before(r(this,F)),f(this,F,null)))};const Tt=["touchstart","touchmove"];function mt(t){return Tt.includes(t)}const q=Symbol("events"),Me=new Set,le=new Set;function Rt(t,e,s,i={}){function o(n){if(i.capture||de.call(e,n),!n.cancelBubble)return st(()=>s==null?void 0:s.call(this,n))}return t.startsWith("pointer")||t.startsWith("touch")||t==="wheel"?z(()=>{e.addEventListener(t,o,i)}):e.addEventListener(t,o,i),o}function Ot(t,e,s,i,o){var n={capture:i,passive:o},u=Rt(t,e,s,n);(e===document.body||e===window||e===document||e instanceof HTMLMediaElement)&&tt(()=>{e.removeEventListener(t,u,n)})}function It(t,e,s){(e[q]??(e[q]={}))[t]=s}function Lt(t){for(var e=0;e{throw L});throw T}}finally{t[q]=e,delete t.currentTarget,ee(R),U(D)}}}function Mt(t,e){var s=e==null?"":typeof e=="object"?e+"":e;s!==(t.__t??(t.__t=t.nodeValue))&&(t.__t=s,t.nodeValue=s+"")}function St(t,e){return Ye(t,e)}function Yt(t,e){oe(),e.intro=e.intro??!1;const s=e.target,i=k,o=Y;try{for(var n=it(s);n&&(n.nodeType!==Fe||n.data!==nt);)n=at(n);if(!n)throw ue;X(!0),te(n);const u=Ye(t,{...e,anchor:n});return X(!1),u}catch(u){if(u instanceof Error&&u.message.split(` -`).some(h=>h.startsWith("https://svelte.dev/e/")))throw u;return u!==ue&&console.warn("Failed to hydrate: ",u),e.recover===!1&&ft(),oe(),ot(s),X(!1),St(t,e)}finally{X(i),te(o)}}const Z=new Map;function Ye(t,{target:e,anchor:s,props:i={},events:o,context:n,intro:u=!0,transformError:h}){oe();var c=void 0,a=ut(()=>{var R=s??e.appendChild(Ae());Et(R,{pending:()=>{}},g=>{ht({});var d=Ne;if(n&&(d.c=n),o&&(i.$$events=o),k&<(g,null),c=t(g,i)||{},k&&(H.nodes.end=Y,Y===null||Y.nodeType!==Fe||Y.data!==dt))throw _t(),ue;pt()},h);var D=new Set,T=g=>{for(var d=0;d{var N;for(var g of D)for(const v of[e,document]){var d=Z.get(v),p=d.get(g);--p==0?(v.removeEventListener(g,de),d.delete(g),d.size===0&&Z.delete(v)):d.set(g,p)}le.delete(T),R!==s&&((N=R.parentNode)==null||N.removeChild(R))}});return _e.set(c,a),c}let _e=new WeakMap;function kt(t,e){const s=_e.get(t);return s?(_e.delete(t),s(e)):Promise.resolve()}function ke(t,e,s){if(t==null)return e(void 0),s&&s(void 0),P;const i=Re(()=>t.subscribe(e,s));return i.unsubscribe?()=>i.unsubscribe():i}const C=[];function At(t,e){return{subscribe:Dt(t,e).subscribe}}function Dt(t,e=P){let s=null;const i=new Set;function o(h){if(gt(t,h)&&(t=h,s)){const c=!C.length;for(const a of i)a[1](),C.push(a,t);if(c){for(let a=0;a{i.delete(a),i.size===0&&s&&(s(),s=null)}}return{set:o,update:n,subscribe:u}}function qt(t,e,s){const i=!Array.isArray(t),o=i?[t]:t;if(!o.every(Boolean))throw new Error("derived() expects stores as input, got a falsy value");const n=e.length<2;return At(s,(u,h)=>{let c=!1;const a=[];let R=0,D=P;const T=()=>{if(R)return;D();const d=e(i?a[0]:a,u,h);n?u(d):D=typeof d=="function"?d:P},g=o.map((d,p)=>ke(d,N=>{a[p]=N,R&=~(1<{R|=1<e=s)(),e}export{It as a,qt as b,ke as c,Lt as d,Ot as e,xt as g,Yt as h,St as m,Mt as s,kt as u,Dt as w}; diff --git a/apps/dashboard/build/_app/immutable/chunks/D8mhvFt8.js.br b/apps/dashboard/build/_app/immutable/chunks/D8mhvFt8.js.br deleted file mode 100644 index 8b6310a..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/D8mhvFt8.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/D8mhvFt8.js.gz b/apps/dashboard/build/_app/immutable/chunks/D8mhvFt8.js.gz deleted file mode 100644 index 3f5feb9..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/D8mhvFt8.js.gz and /dev/null 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..a0d4169 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/DKve45Wd.js b/apps/dashboard/build/_app/immutable/chunks/DKve45Wd.js deleted file mode 100644 index 66e7aff..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/DKve45Wd.js +++ /dev/null @@ -1 +0,0 @@ -import{q as T,G as o,T as h,E as b,U as p,V as A,W as E,X as R,L as g,K as l}from"./wpu9U-D0.js";import{B as v}from"./BWk3o_TN.js";function S(t,u,_=!1){o&&h();var n=new v(t),c=_?b: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/DKve45Wd.js.br b/apps/dashboard/build/_app/immutable/chunks/DKve45Wd.js.br deleted file mode 100644 index 3241073..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/DKve45Wd.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DKve45Wd.js.gz b/apps/dashboard/build/_app/immutable/chunks/DKve45Wd.js.gz deleted file mode 100644 index 10e3b75..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/DKve45Wd.js.gz and /dev/null 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..84039b9 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..44aee17 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..decf350 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/DPdYG9yN.js b/apps/dashboard/build/_app/immutable/chunks/DPdYG9yN.js deleted file mode 100644 index a887f81..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/DPdYG9yN.js +++ /dev/null @@ -1 +0,0 @@ -const c=()=>{var e;return typeof window<"u"&&((e=window.matchMedia)==null?void 0:e.call(window,"(prefers-reduced-motion: reduce)").matches)};function f(e,r={}){if(c())return{};const i=r.strength??8;let t=0;function o(a){const n=e.getBoundingClientRect(),m=a.clientX-(n.left+n.width/2),l=a.clientY-(n.top+n.height/2),p=Math.max(-1,Math.min(1,m/(n.width/2)))*i,v=Math.max(-1,Math.min(1,l/(n.height/2)))*i;cancelAnimationFrame(t),t=requestAnimationFrame(()=>{e.style.transform=`translate(${p}px, ${v}px)`})}function s(){cancelAnimationFrame(t),e.style.transform=""}return e.style.transition="transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1)",e.addEventListener("pointermove",o),e.addEventListener("pointerleave",s),{destroy(){e.removeEventListener("pointermove",o),e.removeEventListener("pointerleave",s),cancelAnimationFrame(t)}}}function u(e){if(c())return{};let r=0;function i(o){const s=e.getBoundingClientRect();cancelAnimationFrame(r),r=requestAnimationFrame(()=>{e.style.setProperty("--spot-x",`${o.clientX-s.left}px`),e.style.setProperty("--spot-y",`${o.clientY-s.top}px`),e.style.setProperty("--spot-o","1")})}function t(){e.style.setProperty("--spot-o","0")}return e.addEventListener("pointermove",i),e.addEventListener("pointerleave",t),{destroy(){e.removeEventListener("pointermove",i),e.removeEventListener("pointerleave",t),cancelAnimationFrame(r)}}}export{f as m,u as s}; diff --git a/apps/dashboard/build/_app/immutable/chunks/DPdYG9yN.js.br b/apps/dashboard/build/_app/immutable/chunks/DPdYG9yN.js.br deleted file mode 100644 index 7b2a8c1..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/DPdYG9yN.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DPdYG9yN.js.gz b/apps/dashboard/build/_app/immutable/chunks/DPdYG9yN.js.gz deleted file mode 100644 index e238558..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/DPdYG9yN.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..3106ebb 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..0a7fc9c 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/DcKTNC6e.js b/apps/dashboard/build/_app/immutable/chunks/DcKTNC6e.js deleted file mode 100644 index 24ae411..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/DcKTNC6e.js +++ /dev/null @@ -1 +0,0 @@ -import"./Bzak7iHL.js";import{p as B,o as E,t as N,a as O,b as T,i as m,g as d,j as z,s as C,e as D,r as G,u as H}from"./wpu9U-D0.js";import{s as I}from"./D8mhvFt8.js";import{s as J}from"./60_R_Vbt.js";import{p as a}from"./ByYB047u.js";var K=z(" ");function U(v,t){var x;B(t,!0);let A=a(t,"decimals",3,0),_=a(t,"scale",3,1),g=a(t,"prefix",3,""),w=a(t,"suffix",3,""),F=a(t,"duration",3,900),M=a(t,"class",3,""),b=a(t,"group",3,!0),r=C(0),i=0,c=0,o=0,l=!1;const q=typeof window<"u"&&((x=window.matchMedia)==null?void 0:x.call(window,"(prefers-reduced-motion: reduce)").matches);function y(e){return e===1?1:1-Math.pow(2,-10*e)}function p(e){if(q){m(r,e,!0);return}cancelAnimationFrame(i),c=d(r),o=0;function s(f){o||(o=f);const n=Math.min(1,(f-o)/F());m(r,c+(e-c)*y(n)),n<1?i=requestAnimationFrame(s):m(r,e,!0)}i=requestAnimationFrame(s)}E(()=>{const e=t.value;return l||(l=!0),p(e),()=>cancelAnimationFrame(i)});let j=H(()=>(()=>{const s=(d(r)*_()).toFixed(A());if(!b())return s;const[f,n]=s.split("."),h=f.replace(/\B(?=(\d{3})+(?!\d))/g,",");return n!==void 0?`${h}.${n}`:h})());var u=K(),k=D(u);G(u),N(()=>{J(u,1,`tabular-nums ${M()??""}`),I(k,`${g()??""}${d(j)??""}${w()??""}`)}),O(v,u),T()}export{U as A}; diff --git a/apps/dashboard/build/_app/immutable/chunks/DcKTNC6e.js.br b/apps/dashboard/build/_app/immutable/chunks/DcKTNC6e.js.br deleted file mode 100644 index 62a2556..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/DcKTNC6e.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DcKTNC6e.js.gz b/apps/dashboard/build/_app/immutable/chunks/DcKTNC6e.js.gz deleted file mode 100644 index c230386..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/DcKTNC6e.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..3a51670 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/Dp1pzeXC.js b/apps/dashboard/build/_app/immutable/chunks/Dp1pzeXC.js deleted file mode 100644 index 287a815..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/Dp1pzeXC.js +++ /dev/null @@ -1 +0,0 @@ -const y="modulepreload",w=function(f,a){return new URL(f,a).href},E={},g=function(a,u,d){let h=Promise.resolve();if(u&&u.length>0){let s=function(e){return Promise.all(e.map(r=>Promise.resolve(r).then(l=>({status:"fulfilled",value:l}),l=>({status:"rejected",reason:l}))))};const t=document.getElementsByTagName("link"),o=document.querySelector("meta[property=csp-nonce]"),v=(o==null?void 0:o.nonce)||(o==null?void 0:o.getAttribute("nonce"));h=s(u.map(e=>{if(e=w(e,d),e in E)return;E[e]=!0;const r=e.endsWith(".css"),l=r?'[rel="stylesheet"]':"";if(!!d)for(let i=t.length-1;i>=0;i--){const c=t[i];if(c.href===e&&(!r||c.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${e}"]${l}`))return;const n=document.createElement("link");if(n.rel=r?"stylesheet":y,r||(n.as="script"),n.crossOrigin="",n.href=e,v&&n.setAttribute("nonce",v),document.head.appendChild(n),r)return new Promise((i,c)=>{n.addEventListener("load",i),n.addEventListener("error",()=>c(new Error(`Unable to preload CSS for ${e}`)))})}))}function m(s){const t=new Event("vite:preloadError",{cancelable:!0});if(t.payload=s,window.dispatchEvent(t),!t.defaultPrevented)throw s}return h.then(s=>{for(const t of s||[])t.status==="rejected"&&m(t.reason);return a().catch(m)})};export{g as _}; diff --git a/apps/dashboard/build/_app/immutable/chunks/Dp1pzeXC.js.br b/apps/dashboard/build/_app/immutable/chunks/Dp1pzeXC.js.br deleted file mode 100644 index 8835f5a..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/Dp1pzeXC.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/Dp1pzeXC.js.gz b/apps/dashboard/build/_app/immutable/chunks/Dp1pzeXC.js.gz deleted file mode 100644 index 4f6661e..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/Dp1pzeXC.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DrafHjYM.js b/apps/dashboard/build/_app/immutable/chunks/DrafHjYM.js deleted file mode 100644 index 1f1e177..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/DrafHjYM.js +++ /dev/null @@ -1 +0,0 @@ -import{v as s,w as v,x as o,y as c,z as b,A as m,B as h,C as y}from"./wpu9U-D0.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/DrafHjYM.js.br b/apps/dashboard/build/_app/immutable/chunks/DrafHjYM.js.br deleted file mode 100644 index a9014f7..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/DrafHjYM.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DrafHjYM.js.gz b/apps/dashboard/build/_app/immutable/chunks/DrafHjYM.js.gz deleted file mode 100644 index 25caa92..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/DrafHjYM.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DzesjbbJ.js b/apps/dashboard/build/_app/immutable/chunks/DzesjbbJ.js deleted file mode 100644 index 6d7e420..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/DzesjbbJ.js +++ /dev/null @@ -1 +0,0 @@ -import{D as y,q as u,H as _,F as o,G as t,I as g,J as i,K as l,L as d,M as p,N as m}from"./wpu9U-D0.js";function F(n,r){let s=null,E=t;var a;if(t){s=p;for(var e=m(document.head);e!==null&&(e.nodeType!==g||e.data!==n);)e=i(e);if(e===null)l(!1);else{var f=i(e);e.remove(),d(f)}}t||(a=document.head.appendChild(y()));try{u(()=>r(a),_|o)}finally{E&&(l(!0),d(s))}}export{F as h}; diff --git a/apps/dashboard/build/_app/immutable/chunks/DzesjbbJ.js.br b/apps/dashboard/build/_app/immutable/chunks/DzesjbbJ.js.br deleted file mode 100644 index 7c75650..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/DzesjbbJ.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DzesjbbJ.js.gz b/apps/dashboard/build/_app/immutable/chunks/DzesjbbJ.js.gz deleted file mode 100644 index e0c61a9..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/DzesjbbJ.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CcUbQ_Wl.js b/apps/dashboard/build/_app/immutable/chunks/DzfRjky4.js similarity index 52% rename from apps/dashboard/build/_app/immutable/chunks/CcUbQ_Wl.js rename to apps/dashboard/build/_app/immutable/chunks/DzfRjky4.js index b9a927a..0f1e075 100644 --- a/apps/dashboard/build/_app/immutable/chunks/CcUbQ_Wl.js +++ b/apps/dashboard/build/_app/immutable/chunks/DzfRjky4.js @@ -1 +1 @@ -const e={fact:"#00A8FF",concept:"#9D00FF",event:"#FFB800",person:"#00FFD1",place:"#00D4FF",note:"#8B95A5",pattern:"#FF3CAC",decision:"#FF4757"},F={MemoryCreated:"#00FFD1",MemoryUpdated:"#00A8FF",MemoryDeleted:"#FF4757",MemoryPromoted:"#00FF88",MemoryDemoted:"#FF6B35",MemorySuppressed:"#A33FFF",MemoryUnsuppressed:"#14E8C6",Rac1CascadeSwept:"#6E3FFF",SearchPerformed:"#818CF8",DeepReferenceCompleted:"#C4B5FD",HookVerdictRecorded:"#F59E0B",DreamStarted:"#9D00FF",DreamProgress:"#B44AFF",DreamCompleted:"#C084FC",ConsolidationStarted:"#FFB800",ConsolidationCompleted:"#FF9500",RetentionDecayed:"#FF4757",ConnectionDiscovered:"#00D4FF",ActivationSpread:"#14E8C6",ImportanceScored:"#FF3CAC",Heartbeat:"#8B95A5"};export{F as E,e as N}; +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..ec4983f 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/EM_PBt2C.js b/apps/dashboard/build/_app/immutable/chunks/EM_PBt2C.js new file mode 100644 index 0000000..ebc2526 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/EM_PBt2C.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 gt,i as _t,b as L,s as C,p as x,n as ft,f as $t,g as ut,a as X,d as it,S as Nt,P as re,e as oe,h as se,o as Dt,j as q,k as ie,l as qt,m as ce,q as le,t as Kt,u as Pt,v as fe}from"./RBGf_S-E.js";class wt{constructor(a,e){this.status=a,typeof e=="string"?this.body={message:e}:e?this.body=e:this.body={message:`Error: ${a}`}}toString(){return JSON.stringify(this.body)}}class vt{constructor(a,e){this.status=a,this.location=e}}class yt extends Error{constructor(a,e,r){super(r),this.status=a,this.text=e}}const ue=/^(\[)?(\.\.\.)?(\w+)(?:=(\w+))?(\])?$/;function he(t){const a=[];return{pattern:t==="/"?/^\/$/:new RegExp(`^${pe(t).map(r=>{const n=/^\[\.\.\.(\w+)(?:=(\w+))?\]$/.exec(r);if(n)return a.push({name:n[1],matcher:n[2],optional:!1,rest:!0,chained:!0}),"(?:/([^]*))?";const o=/^\[\[(\w+)(?:=(\w+))?\]\]$/.exec(r);if(o)return a.push({name:o[1],matcher:o[2],optional:!0,rest:!1,chained:!0}),"(?:/([^/]+))?";if(!r)return;const s=r.split(/\[(.+?)\](?!\])/);return"/"+s.map((c,l)=>{if(l%2){if(c.startsWith("x+"))return ct(String.fromCharCode(parseInt(c.slice(2),16)));if(c.startsWith("u+"))return ct(String.fromCharCode(...c.slice(2).split("-").map(_=>parseInt(_,16))));const h=ue.exec(c),[,u,w,f,d]=h;return a.push({name:f,matcher:d,optional:!!u,rest:!!w,chained:w?l===1&&s[0]==="":!1}),w?"([^]*?)":u?"([^/]*)?":"([^/]+?)"}return ct(c)}).join("")}).join("")}/?$`),params:a}}function de(t){return t!==""&&!/^\([^)]+\)$/.test(t)}function pe(t){return t.slice(1).split("/").filter(de)}function me(t,a,e){const r={},n=t.slice(1),o=n.filter(i=>i!==void 0);let s=0;for(let i=0;ih).join("/"),s=0),l===void 0)if(c.rest)l="";else continue;if(!c.matcher||e[c.matcher](l)){r[c.name]=l;const h=a[i+1],u=n[i+1];h&&!h.rest&&h.optional&&u&&c.chained&&(s=0),!h&&!u&&Object.keys(r).length===o.length&&(s=0);continue}if(c.optional&&c.chained){s++;continue}return}if(!s)return r}function ct(t){return t.normalize().replace(/[[\]]/g,"\\$&").replace(/%/g,"%25").replace(/\//g,"%2[Ff]").replace(/\?/g,"%3[Ff]").replace(/#/g,"%23").replace(/[.*+?^${}()|\\]/g,"\\$&")}function ge({nodes:t,server_loads:a,dictionary:e,matchers:r}){const n=new Set(a);return Object.entries(e).map(([i,[c,l,h]])=>{const{pattern:u,params:w}=he(i),f={id:i,exec:d=>{const _=u.exec(d);if(_)return me(_,w,r)},errors:[1,...h||[]].map(d=>t[d]),layouts:[0,...l||[]].map(s),leaf:o(c)};return f.errors.length=f.layouts.length=Math.max(f.errors.length,f.layouts.length),f});function o(i){const c=i<0;return c&&(i=~i),[c,t[i]]}function s(i){return i===void 0?i:[n.has(i),t[i]]}}function Ft(t,a=JSON.parse){try{return a(sessionStorage[t])}catch{}}function It(t,a,e=JSON.stringify){const r=e(a);try{sessionStorage[t]=r}catch{}}function _e(t){return t.filter(a=>a!=null)}function Et(t){return t instanceof wt||t instanceof yt?t.status:500}function we(t){return t instanceof yt?t.text:"Internal Error"}const ve=new Set(["icon","shortcut icon","apple-touch-icon"]),I=Ft(Kt)??{},M=Ft(qt)??{},P={url:Pt({}),page:Pt({}),navigating:ae(null),updated:ne()};function bt(t){I[t]=C()}function ye(t,a){let e=t+1;for(;I[e];)delete I[e],e+=1;for(e=a+1;M[e];)delete M[e],e+=1}function V(t,a=!1){return a?location.replace(t.href):location.href=t.href,new Promise(()=>{})}async function Bt(){if("serviceWorker"in navigator){const t=await navigator.serviceWorker.getRegistration(L||"/");t&&await t.update()}}function Tt(){}let kt,ht,Q,U,dt,b;const Z=[],tt=[];let v=null;function pt(){var t;(t=v==null?void 0:v.fork)==null||t.then(a=>a==null?void 0:a.discard()),v=null}const W=new Map,Mt=new Set,Ee=new Set,F=new Set;let g={branch:[],error:null,url:null},Vt=!1,et=!1,Ot=!0,H=!1,K=!1,Ht=!1,St=!1,Yt,E,R,O;const at=new Set,Ct=new Map;async function Fe(t,a,e){var o,s,i,c,l;(o=globalThis.__sveltekit_8fmifr)!=null&&o.data&&globalThis.__sveltekit_8fmifr.data,document.URL!==location.href&&(location.href=location.href),b=t,await((i=(s=t.hooks).init)==null?void 0:i.call(s)),kt=ge(t),U=document.documentElement,dt=a,ht=t.nodes[0],Q=t.nodes[1],ht(),Q(),E=(c=history.state)==null?void 0:c[N],R=(l=history.state)==null?void 0:l[B],E||(E=R=Date.now(),history.replaceState({...history.state,[N]:E,[B]:R},""));const r=I[E];function n(){r&&(history.scrollRestoration="manual",scrollTo(r.x,r.y))}e?(n(),await Ce(dt,e)):(await D({type:"enter",url:gt(b.hash?Ne(new URL(location.href)):location.href),replace_state:!0}),n()),Oe()}function be(){Z.length=0,St=!1}function zt(t){tt.some(a=>a==null?void 0:a.snapshot)&&(M[t]=tt.map(a=>{var e;return(e=a==null?void 0:a.snapshot)==null?void 0:e.capture()}))}function 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:gt(t),keepfocus:a.keepFocus,noscroll:a.noScroll,replace_state:a.replaceState,state:a.state,redirect_count:e,nav_token:r,accept:()=>{a.invalidateAll&&(St=!0,n=[...Ct.keys()]),a.invalidate&&a.invalidate.forEach(Te)}}),a.invalidateAll&&J().then(J).then(()=>{Ct.forEach(({resource:o},s)=>{var i;n!=null&&n.includes(s)&&((i=o.refresh)==null||i.call(o))})})}async function ke(t){if(t.id!==(v==null?void 0:v.id)){pt();const a={};at.add(a),v={id:t.id,token:a,promise:Xt({...t,preload:a}).then(e=>(at.delete(a),e.type==="loaded"&&e.state.error&&pt(),e)),fork:null}}return v.promise}async function lt(t){var e;const a=(e=await ot(t,!1))==null?void 0:e.route;a&&await Promise.all([...a.layouts,a.leaf].filter(Boolean).map(r=>r[1]()))}async function Jt(t,a,e){var n;g=t.state;const r=document.querySelector("style[data-sveltekit]");if(r&&r.remove(),Object.assign(x,t.props.page),Yt=new b.root({target:a,props:{...t.props,stores:P,components:tt},hydrate:e,sync:!1}),await Promise.resolve(),Gt(R),e){const o={from:null,to:{params:g.params,route:{id:((n=g.route)==null?void 0:n.id)??null},url:new URL(location.href),scroll:I[E]??C()},willUnload:!1,type:"enter",complete:Promise.resolve()};F.forEach(s=>s(o))}et=!0}function nt({url:t,params:a,branch:e,status:r,error:n,route:o,form:s}){let i="never";if(L&&(t.pathname===L||t.pathname===L+"/"))i="always";else for(const f of e)(f==null?void 0:f.slash)!==void 0&&(i=f.slash);t.pathname=se(t.pathname,i),t.search=t.search;const c={type:"loaded",state:{url:t,params:a,branch:e,error:n,route:o},props:{constructors:_e(e).map(f=>f.node.component),page:At(x)}};s!==void 0&&(c.props.form=s);let l={},h=!x,u=0;for(let f=0;fi(new URL(s))))return!0;return!1}function xt(t,a){return(t==null?void 0:t.type)==="data"?t:(t==null?void 0:t.type)==="skip"?a??null:null}function xe(t,a){if(!t)return new Set(a.searchParams.keys());const e=new Set([...t.searchParams.keys(),...a.searchParams.keys()]);for(const r of e){const n=t.searchParams.getAll(r),o=a.searchParams.getAll(r);n.every(s=>o.includes(s))&&o.every(s=>n.includes(s))&&e.delete(r)}return e}function Le({error:t,url:a,route:e,params:r}){return{type:"loaded",state:{error:t,url:a,route:e,params:r,branch:[]},props:{page:At(x),constructors:[]}}}async function Xt({id:t,invalidating:a,url:e,params:r,route:n,preload:o}){if((v==null?void 0:v.id)===t)return at.delete(v.token),v.promise;const{errors:s,layouts:i,leaf:c}=n,l=[...i,c];s.forEach(m=>m==null?void 0:m().catch(()=>{})),l.forEach(m=>m==null?void 0:m[1]().catch(()=>{}));const h=g.url?t!==rt(g.url):!1,u=g.route?n.id!==g.route.id:!1,w=xe(g.url,e);let f=!1;const d=l.map(async(m,p)=>{var A;if(!m)return;const y=g.branch[p];return m[1]===(y==null?void 0:y.loader)&&!Re(f,u,h,w,(A=y.universal)==null?void 0:A.uses,r)?y:(f=!0,Rt({loader:m[1],url:e,params:r,route:n,parent:async()=>{var z;const T={};for(let j=0;j{});const _=[];for(let m=0;mPromise.resolve({}),server_data_node:xt(o)}),i={node:await Q(),loader:Q,universal:null,server:null,data:null};return nt({url:e,params:n,branch:[s,i],status:t,error:a,route:null})}catch(s){if(s instanceof vt)return 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(g,e,t,a,o??null);r!==void 0&&(i.navigation.delta=r),n!==void 0&&(i.navigation.event=n);const c={...i.navigation,cancel:()=>{s=!0,i.reject(new Error("navigation cancelled"))}};return H||Mt.forEach(l=>l(c)),s?null:i}async function D({type:t,url:a,popped:e,keepfocus:r,noscroll:n,replace_state:o,state:s={},redirect_count:i=0,nav_token:c={},accept:l=Tt,block:h=Tt,event:u}){var j;const w=O;O=c;const f=await ot(a,!1),d=t==="enter"?Ut(g,f,a,t):Qt({url:a,type:t,delta:e==null?void 0:e.delta,intent:f,scroll:e==null?void 0:e.scroll,event:u});if(!d){h(),O===c&&(O=w);return}const _=E,m=R;l(),H=!0,et&&d.navigation.type!=="enter"&&P.navigating.set(ft.current=d.navigation);let p=f&&await Xt(f);if(!p){if(_t(a,L,b.hash))return await V(a,o);p=await Zt(a,{id:null},await Y(new yt(404,"Not Found",`Not found: ${a.pathname}`),{url:a,params:{},route:{id:null}}),404,o)}if(a=(f==null?void 0:f.url)||a,O!==c)return d.reject(new Error("navigation aborted")),!1;if(p.type==="redirect"){if(i<20){await D({type:t,url:new URL(p.location,a),popped:e,keepfocus:r,noscroll:n,replace_state:o,state:s,redirect_count:i+1,nav_token:c}),d.fulfil(void 0);return}p=await Lt({status:500,error:await Y(new Error("Redirect loop"),{url:a,params:{},route:{id:null}}),url:a,route:{id:null}})}else p.props.page.status>=400&&await P.updated.check()&&(await Bt(),await V(a,o));if(be(),bt(_),zt(m),p.props.page.url.pathname!==a.pathname&&(a.pathname=p.props.page.url.pathname),s=e?e.state:s,!e){const k=o?0:1,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)})}g=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(g.url)===rt(u);if(!(d.reload||_))if(c<=d.preload_data){a={element:l,href:l.href},e=q.tap;const m=await ot(u,!1);if(!m)return;ke(m)}else c<=d.preload_code&&(a={element:l,href:l.href},e=c,lt(u))}function s(){n.disconnect();for(const i of U.querySelectorAll("a")){const{url:c,external:l,download:h}=ut(i,L,b.hash);if(l||h)continue;const u=X(i);u.reload||(u.preload_code===q.viewport&&n.observe(i),u.preload_code===q.eager&<(c))}}F.add(s),s()}function Y(t,a){if(t instanceof wt)return t.body;const e=Et(t),r=we(t);return b.hooks.handleError({error:t,event:a,status:e,message:r})??{message:r}}function Be(t,a={}){return t=new URL(gt(t)),t.origin!==Dt?Promise.reject(new Error("goto: invalid URL")):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(g,void 0,null,"leave"),o={...n.navigation,cancel:()=>{r=!0,n.reject(new Error("navigation cancelled"))}};Mt.forEach(s=>s(o))}r?(e.preventDefault(),e.returnValue=""):history.scrollRestoration="auto"}),addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&jt()}),(a=navigator.connection)!=null&&a.saveData||Ie(),U.addEventListener("click",async e=>{if(e.button||e.which!==1||e.metaKey||e.ctrlKey||e.shiftKey||e.altKey||e.defaultPrevented)return;const r=$t(e.composedPath()[0],U);if(!r)return;const{url:n,external:o,target:s,download:i}=ut(r,L,b.hash);if(!n)return;if(s==="_parent"||s==="_top"){if(window.parent!==window)return}else if(s&&s!=="_self")return;const c=X(r);if(!(r instanceof SVGAElement)&&n.protocol!==location.protocol&&!(n.protocol==="https:"||n.protocol==="http:")||i)return;const[h,u]=(b.hash?n.hash.replace(/^#/,""):n.href).split("#"),w=h===it(location);if(o||c.reload&&(!w||!u)){Qt({url:n,type:"link",event:e})?H=!0:e.preventDefault();return}if(u!==void 0&&w){const[,f]=g.url.href.split("#");if(f===u){if(e.preventDefault(),u===""||u==="top"&&r.ownerDocument.getElementById("top")===null)scrollTo({top:0});else{const d=r.ownerDocument.getElementById(decodeURIComponent(u));d&&(d.scrollIntoView(),d.focus())}return}if(K=!0,bt(E),t(n),!c.replace_state)return;K=!1}e.preventDefault(),await new Promise(f=>{requestAnimationFrame(()=>{setTimeout(f,0)}),setTimeout(f,100)}),await D({type:"link",url:n,keepfocus:c.keepfocus,noscroll:c.noscroll,replace_state:c.replace_state??n.href===location.href,event:e})}),U.addEventListener("submit",e=>{if(e.defaultPrevented)return;const r=HTMLFormElement.prototype.cloneNode.call(e.target),n=e.submitter;if(((n==null?void 0:n.formTarget)||r.target)==="_blank"||((n==null?void 0:n.formMethod)||r.method)!=="get")return;const i=new URL((n==null?void 0:n.hasAttribute("formaction"))&&(n==null?void 0:n.formAction)||r.action);if(_t(i,L,!1))return;const c=e.target,l=X(c);if(l.reload)return;e.preventDefault(),e.stopPropagation();const h=new FormData(c,n);i.search=new URLSearchParams(h).toString(),D({type:"form",url:i,keepfocus:l.keepfocus,noscroll:l.noscroll,replace_state:l.replace_state??i.href===location.href,event:e})}),addEventListener("popstate",async e=>{var r;if(!mt){if((r=e.state)!=null&&r[N]){const n=e.state[N];if(O={},n===E)return;const o=I[n],s=e.state[Nt]??{},i=new URL(e.state[re]??location.href),c=e.state[B],l=g.url?it(location)===it(g.url):!1;if(c===R&&(Ht||l)){s!==x.state&&(x.state=s),t(i),I[E]=C(),o&&scrollTo(o.x,o.y),E=n;return}const u=n-E;await D({type:"popstate",url:i,popped:{state:s,scroll:o,delta:u},accept:()=>{E=n,R=c},block:()=>{history.go(-u)},nav_token:O,event:e})}else if(!K){const n=new URL(location.href);t(n),b.hash&&location.reload()}}}),addEventListener("hashchange",()=>{K&&(K=!1,history.replaceState({...history.state,[N]:++E,[B]:R},"",location.href))});for(const e of document.querySelectorAll("link"))ve.has(e.rel)&&(e.href=e.href);addEventListener("pageshow",e=>{e.persisted&&P.navigating.set(ft.current=null)});function t(e){g.url=x.url=e,P.page.set(At(x)),P.page.notify()}}async function Ce(t,{status:a=200,error:e,node_ids:r,params:n,route:o,server_route:s,data:i,form:c}){Vt=!0;const l=new URL(location.href);let h;({params:n={},route:o={id:null}}=await ot(l,!1)||{}),h=kt.find(({id:f})=>f===o.id);let u,w=!0;try{const f=r.map(async(_,m)=>{const p=i[m];return p!=null&&p.uses&&(p.uses=je(p.uses)),Rt({loader:b.nodes[_],url:l,params:n,route:o,parent:async()=>{const y={};for(let S=0;S{const i=history.state;mt=!0,location.replace(new URL(`#${r}`,location.href)),history.replaceState(i,"",t),a&&scrollTo(o,s),mt=!1})}else{const o=document.body,s=o.getAttribute("tabindex");o.tabIndex=-1,o.focus({preventScroll:!0,focusVisible:!1}),s!==null?o.setAttribute("tabindex",s):o.removeAttribute("tabindex")}const n=getSelection();if(n&&n.type!=="None"){const o=[];for(let s=0;s{if(n.rangeCount===o.length){for(let s=0;s{o=u,s=w});return i.catch(()=>{}),{navigation:{from:{params:t.params,route:{id:((l=t.route)==null?void 0:l.id)??null},url:t.url,scroll:C()},to:e&&{params:(a==null?void 0:a.params)??null,route:{id:((h=a==null?void 0:a.route)==null?void 0:h.id)??null},url:e,scroll:n},willUnload:!a,type:r,complete:i},fulfil:o,reject:s}}function At(t){return{data:t.data,error:t.error,form:t.form,params:t.params,route:t.route,state:t.state,status:t.status,url:t.url}}function Ne(t){const a=new URL(t);return a.hash=decodeURIComponent(t.hash),a}function te(t){let a;if(b.hash){const[,,e]=t.hash.split("#",3);a=e??""}else a=t.hash.slice(1);return decodeURIComponent(a)}export{Fe as a,Be as g,P as s}; diff --git a/apps/dashboard/build/_app/immutable/chunks/EM_PBt2C.js.br b/apps/dashboard/build/_app/immutable/chunks/EM_PBt2C.js.br new file mode 100644 index 0000000..fdd9ab0 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/EM_PBt2C.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/EM_PBt2C.js.gz b/apps/dashboard/build/_app/immutable/chunks/EM_PBt2C.js.gz new file mode 100644 index 0000000..c7f89a2 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/EM_PBt2C.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/EqHb-9AZ.js b/apps/dashboard/build/_app/immutable/chunks/EqHb-9AZ.js deleted file mode 100644 index 459bed2..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/EqHb-9AZ.js +++ /dev/null @@ -1 +0,0 @@ -import{t as b}from"./60_R_Vbt.js";import{G as c}from"./wpu9U-D0.js";function A(i,u={},r,f){for(var a in r){var o=r[a];u[a]!==o&&(r[a]==null?i.style.removeProperty(a):i.style.setProperty(a,o,f))}}function P(i,u,r,f){var a=i.__style;if(c||a!==u){var o=b(u,f);(!c||o!==i.getAttribute("style"))&&(o==null?i.removeAttribute("style"):i.style.cssText=o),i.__style=u}else f&&(Array.isArray(f)?(A(i,r==null?void 0:r[0],f[0]),A(i,r==null?void 0:r[1],f[1],"important")):A(i,r,f));return f}export{P as s}; diff --git a/apps/dashboard/build/_app/immutable/chunks/EqHb-9AZ.js.br b/apps/dashboard/build/_app/immutable/chunks/EqHb-9AZ.js.br deleted file mode 100644 index c593293..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/EqHb-9AZ.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/EqHb-9AZ.js.gz b/apps/dashboard/build/_app/immutable/chunks/EqHb-9AZ.js.gz deleted file mode 100644 index 1fda5c7..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/EqHb-9AZ.js.gz and /dev/null 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..1095b97 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/LDOJP_6N.js b/apps/dashboard/build/_app/immutable/chunks/LDOJP_6N.js deleted file mode 100644 index 28f1b46..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/LDOJP_6N.js +++ /dev/null @@ -1 +0,0 @@ -import{q as p,E as t}from"./wpu9U-D0.js";import{B as c}from"./BWk3o_TN.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/LDOJP_6N.js.br b/apps/dashboard/build/_app/immutable/chunks/LDOJP_6N.js.br deleted file mode 100644 index 23ccb63..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/LDOJP_6N.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/LDOJP_6N.js.gz b/apps/dashboard/build/_app/immutable/chunks/LDOJP_6N.js.gz deleted file mode 100644 index 792b2a5..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/LDOJP_6N.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/Ma4NfFrG.js b/apps/dashboard/build/_app/immutable/chunks/Ma4NfFrG.js deleted file mode 100644 index 8a212fc..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/Ma4NfFrG.js +++ /dev/null @@ -1,2 +0,0 @@ -const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./CfobEeQC.js","./C-SOZ1Oi.js","./Bzak7iHL.js","./TZu9D97Z.js","./wpu9U-D0.js","./D8mhvFt8.js","./DKve45Wd.js","./BWk3o_TN.js","./60_R_Vbt.js","./EqHb-9AZ.js","./CnZzd20v.js","./ByYB047u.js","./Bxs5UR9-.js","./g5OnrUYZ.js","./CcUbQ_Wl.js","./DrafHjYM.js","./BLadwbF7.js","./Dp1pzeXC.js","./D7A-gG4Z.js","../assets/Icon.tTjeJXhC.css","./CmbJHhgy.js","../assets/Dropdown.C2Z-7Phd.css","./CZfHMhLI.js","./BhIgFntf.js","../assets/11.BxoW8Jf1.css","./BZQzXWp7.js","./CLrXVRi2.js","./C8kRUgax.js"])))=>i.map(i=>d[i]); -var x=Object.defineProperty;var C=(m,t,e)=>t in m?x(m,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):m[t]=e;var i=(m,t,e)=>C(m,typeof t!="symbol"?t+"":t,e);import{_ as d}from"./Dp1pzeXC.js";import{V as p,M as w,a as T}from"./C-SOZ1Oi.js";const f=new p(0,0,0),R=30,l=44;function S(){return typeof navigator<"u"&&"gpu"in navigator}class V{constructor(t){i(this,"container");i(this,"deps");i(this,"renderer");i(this,"scene");i(this,"camera");i(this,"storm");i(this,"post",null);i(this,"booted",!1);i(this,"target",new p(0,0,0));i(this,"flythrough",0);i(this,"prevCamPos",new p);i(this,"camVel",new p);this.container=t}get cameraRef(){return this.camera}async boot(){if(this.booted)return;if(!S())throw new Error("WebGPU not supported");const t=await d(()=>import("./CfobEeQC.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]),import.meta.url),e=await d(()=>import("./BZQzXWp7.js"),__vite__mapDeps([25,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]),import.meta.url),s=await d(()=>import("./CLrXVRi2.js"),__vite__mapDeps([26,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),{SemanticComputeStorm:a}=await d(async()=>{const{SemanticComputeStorm:n}=await import("./C8kRUgax.js");return{SemanticComputeStorm:n}},__vite__mapDeps([27,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);if(this.deps={WebGPURenderer:t.WebGPURenderer,PostProcessing:t.PostProcessing,StormCtor:a,tsl:e,bloomMod:s},!t.WebGPURenderer||!t.Scene||!t.PerspectiveCamera||!t.Color)throw new Error("[cinema] three/webgpu is missing expected exports");const h=Math.max(1,this.container.clientWidth),c=Math.max(1,this.container.clientHeight);this.scene=new t.Scene,this.scene.background=new t.Color(131594),this.camera=new t.PerspectiveCamera(60,h/c,.1,2e3),this.camera.position.set(0,18,60);const o=new this.deps.WebGPURenderer({antialias:!0,alpha:!1});o.setPixelRatio(Math.min(window.devicePixelRatio,2)),o.setSize(h,c),await o.init(),this.container.appendChild(o.domElement),this.renderer=o,this.storm=new this.deps.StormCtor(o,this.scene,{});try{const{pass:n,mrt:b,output:g,emissive:_}=this.deps.tsl,r=n(this.scene,this.camera);if(typeof(r==null?void 0:r.setMRT)!="function"||typeof(r==null?void 0:r.getTextureNode)!="function")throw new Error("three/tsl pass() API mismatch — setMRT/getTextureNode missing");r.setMRT(b({output:g,emissive:_}));const M=r.getTextureNode("output"),P=r.getTextureNode("emissive"),v=this.deps.bloomMod.bloom(P,.6,.65,.35),u=new this.deps.PostProcessing(o);u.outputNode=M.add(v),this.post=u}catch(n){console.warn("[cinema] selective bloom unavailable, rendering without MRT:",n),this.post=null}this.booted=!0}transitionTo(t,e,s="II",a=99){this.booted&&this.storm.transitionTo(t,f,s,a)}dreamBeat(){this.booted&&this.storm.dreamBeat()}setFlythrough(t){this.flythrough=w.clamp(t,0,1),this.booted&&this.storm.setStreak(t)}setStreak(t){this.booted&&this.storm.setStreak(t)}setCameraVel(t){this.booted&&this.storm.setCameraVel(t)}async render(t){if(!this.booted)return;this.camVel.copy(this.camera.position).sub(this.prevCamPos).divideScalar(Math.max(t,.001)),this.prevCamPos.copy(this.camera.position);const e=w.lerp(R,6,this.flythrough),s=this.camera.position.length();if(sl||!Number.isFinite(s)){const n=Math.min(l,Math.max(e,s||l));s>.001?this.camera.position.setLength(n):this.camera.position.set(0,12,n)}this.camera.lookAt(f);const a=this.camVel.clone().applyMatrix3(new T().setFromMatrix4(this.camera.matrixWorldInverse)).negate();this.storm.setCameraVel(a);const h=this.camera.position.length(),c=this.camera.fov*Math.PI/180,o=Math.tan(c/2)*h*.82;this.storm.setContainRadius(o),await this.storm.update(t),this.post?await this.post.renderAsync():await this.renderer.renderAsync(this.scene,this.camera)}resize(){if(!this.booted)return;const t=Math.max(1,this.container.clientWidth),e=Math.max(1,this.container.clientHeight);this.camera.aspect=t/e,this.camera.updateProjectionMatrix(),this.renderer.setSize(t,e)}dispose(){var t,e,s,a,h;this.booted&&((t=this.storm)==null||t.dispose(),(s=(e=this.renderer)==null?void 0:e.dispose)==null||s.call(e),(h=(a=this.renderer)==null?void 0:a.domElement)!=null&&h.parentNode&&this.renderer.domElement.parentNode.removeChild(this.renderer.domElement),this.booted=!1)}}export{V as CinemaSandbox,S as isWebGPUSupported}; diff --git a/apps/dashboard/build/_app/immutable/chunks/Ma4NfFrG.js.br b/apps/dashboard/build/_app/immutable/chunks/Ma4NfFrG.js.br deleted file mode 100644 index d7d7852..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/Ma4NfFrG.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/Ma4NfFrG.js.gz b/apps/dashboard/build/_app/immutable/chunks/Ma4NfFrG.js.gz deleted file mode 100644 index 6441d50..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/Ma4NfFrG.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/P1-U_Xsj.js b/apps/dashboard/build/_app/immutable/chunks/P1-U_Xsj.js deleted file mode 100644 index 192a855..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/P1-U_Xsj.js +++ /dev/null @@ -1 +0,0 @@ -import{x as f,P as d,Q as l,a6 as u,aa as p}from"./wpu9U-D0.js";function m(e,n,t){f(()=>{var r=d(()=>n(e,t==null?void 0:t())||{});if(t&&(r!=null&&r.update)){var a=!1,i={};l(()=>{var s=t();u(s),a&&p(i,s)&&(i=s,r.update(s))}),a=!0}if(r!=null&&r.destroy)return()=>r.destroy()})}const y=()=>{var e;return typeof window<"u"&&((e=window.matchMedia)==null?void 0:e.call(window,"(prefers-reduced-motion: reduce)").matches)};function x(e,n={}){const{y:t=16,delay:r=0,once:a=!0,threshold:i=.12}=n;if(y())return e.classList.add("reveal-in"),{};e.classList.add("reveal"),e.style.setProperty("--reveal-y",`${t}px`),r&&e.style.setProperty("--reveal-delay",`${r}ms`);const s=new IntersectionObserver(o=>{for(const c of o)c.isIntersecting?(e.classList.add("reveal-in"),a&&s.unobserve(e)):a||e.classList.remove("reveal-in")},{threshold:i,rootMargin:"0px 0px -8% 0px"});return s.observe(e),{destroy(){s.disconnect()}}}export{m as a,x as r}; diff --git a/apps/dashboard/build/_app/immutable/chunks/P1-U_Xsj.js.br b/apps/dashboard/build/_app/immutable/chunks/P1-U_Xsj.js.br deleted file mode 100644 index 6cee039..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/P1-U_Xsj.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/P1-U_Xsj.js.gz b/apps/dashboard/build/_app/immutable/chunks/P1-U_Xsj.js.gz deleted file mode 100644 index 36ae23b..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/P1-U_Xsj.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/RBGf_S-E.js b/apps/dashboard/build/_app/immutable/chunks/RBGf_S-E.js new file mode 100644 index 0000000..c284ce0 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/RBGf_S-E.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 Re(){const{set:t,subscribe:e}=G(!1);let n;async function r(){clearTimeout(n);try{const s=await fetch(`${M}/_app/version.json`,{headers:{pragma:"no-cache","cache-control":"no-cache"}});if(!s.ok)return!1;const o=(await s.json()).version!==F;return o&&(t(!0),D.v(),clearTimeout(n)),o}catch{return!1}}return{subscribe:e,check:r}}function Q(t,e,n){return t.origin!==Y||!t.pathname.startsWith(e)?!0:n?t.pathname!==location.pathname:!1}function Se(t){}const H=new Set(["load","prerender","csr","ssr","trailingSlash","config"]);[...H];const Z=new Set([...H]);[...Z];let E,O,T;const ee=I.toString().includes("$$")||/function \w+\(\) \{\}/.test(I.toString());var _,m,w,p,v,y,A,R,P,S,V,k,j;ee?(E={data:{},form:null,error:null,params:{},route:{id:null},state:{},status:-1,url:new URL("https://example.com")},O={current:null},T={current:!1}):(E=new(P=class{constructor(){c(this,_,u({}));c(this,m,u(null));c(this,w,u(null));c(this,p,u({}));c(this,v,u({id:null}));c(this,y,u({}));c(this,A,u(-1));c(this,R,u(new URL("https://example.com")))}get data(){return f(a(this,_))}set data(e){d(a(this,_),e)}get form(){return f(a(this,m))}set form(e){d(a(this,m),e)}get error(){return f(a(this,w))}set error(e){d(a(this,w),e)}get params(){return f(a(this,p))}set params(e){d(a(this,p),e)}get route(){return f(a(this,v))}set route(e){d(a(this,v),e)}get state(){return f(a(this,y))}set state(e){d(a(this,y),e)}get status(){return f(a(this,A))}set status(e){d(a(this,A),e)}get url(){return f(a(this,R))}set url(e){d(a(this,R),e)}},_=new WeakMap,m=new WeakMap,w=new WeakMap,p=new WeakMap,v=new WeakMap,y=new WeakMap,A=new WeakMap,R=new WeakMap,P),O=new(V=class{constructor(){c(this,S,u(null))}get current(){return f(a(this,S))}set current(e){d(a(this,S),e)}},S=new WeakMap,V),T=new(j=class{constructor(){c(this,k,u(!1))}get current(){return f(a(this,k))}set current(e){d(a(this,k),e)}},k=new WeakMap,j),D.v=()=>T.current=!0);function Ue(t){Object.assign(E,t)}export{be as H,_e as N,ge as P,he as S,ye as a,J as b,Re as c,le as d,ie as e,pe as f,ve as g,ae as h,Q as i,N as j,oe as k,fe as l,ue as m,O as n,Y as o,E as p,ce as q,me as r,we as s,de as t,Ae as u,Ue as v,Se as w}; diff --git a/apps/dashboard/build/_app/immutable/chunks/RBGf_S-E.js.br b/apps/dashboard/build/_app/immutable/chunks/RBGf_S-E.js.br new file mode 100644 index 0000000..76ee001 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/RBGf_S-E.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/RBGf_S-E.js.gz b/apps/dashboard/build/_app/immutable/chunks/RBGf_S-E.js.gz new file mode 100644 index 0000000..8f91368 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/RBGf_S-E.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/TZu9D97Z.js b/apps/dashboard/build/_app/immutable/chunks/TZu9D97Z.js deleted file mode 100644 index f3cc3c8..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/TZu9D97Z.js +++ /dev/null @@ -1 +0,0 @@ -import{o as l,a2 as o,al as u,P as n,as as c,at as r,O as i}from"./wpu9U-D0.js";import{h as f,m,u as _}from"./D8mhvFt8.js";function a(e){throw new Error("https://svelte.dev/e/lifecycle_outside_component")}function s(e){o===null&&a(),u&&o.l!==null?d(o).m.push(e):l(()=>{const t=n(e);if(typeof t=="function")return t})}function p(e){o===null&&a(),s(()=>()=>n(e))}function d(e){var t=e.l;return t.u??(t.u={a:[],b:[],m:[]})}const b=Object.freeze(Object.defineProperty({__proto__:null,flushSync:c,hydrate:f,mount:m,onDestroy:p,onMount:s,settled:r,tick:i,unmount:_,untrack:n},Symbol.toStringTag,{value:"Module"}));export{p as a,s as o,b as s}; diff --git a/apps/dashboard/build/_app/immutable/chunks/TZu9D97Z.js.br b/apps/dashboard/build/_app/immutable/chunks/TZu9D97Z.js.br deleted file mode 100644 index 86c1102..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/TZu9D97Z.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/TZu9D97Z.js.gz b/apps/dashboard/build/_app/immutable/chunks/TZu9D97Z.js.gz deleted file mode 100644 index 86a9647..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/TZu9D97Z.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..70ccd9e 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/dCAmqaEc.js b/apps/dashboard/build/_app/immutable/chunks/dCAmqaEc.js deleted file mode 100644 index 32ca8e1..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/dCAmqaEc.js +++ /dev/null @@ -1 +0,0 @@ -import{s as ae}from"./TZu9D97Z.js";import{c as ne,H as $,N as M,r as mt,i as _t,b as L,s as C,p as x,n as ft,f as Nt,g as ut,a as J,d as it,S as $t,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"./Bxs5UR9-.js";import{O as X,at as ue}from"./wpu9U-D0.js";import{w as he}from"./D8mhvFt8.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 de=/^(\[)?(\.\.\.)?(\w+)(?:=(\w+))?(\])?$/;function pe(t){const a=[];return{pattern:t==="/"?/^\/$/:new RegExp(`^${me(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=de.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 ge(t){return t!==""&&!/^\([^)]+\)$/.test(t)}function me(t){return t.slice(1).split("/").filter(ge)}function _e(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 we({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}=pe(i),f={id:i,exec:d=>{const _=u.exec(d);if(_)return _e(_,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 ve(t){return t.filter(a=>a!=null)}function Et(t){return t instanceof wt||t instanceof yt?t.status:500}function ye(t){return t instanceof yt?t.text:"Internal Error"}const{onMount:Ee}=ae,be=new Set(["icon","shortcut icon","apple-touch-icon"]),I=Ft(Kt)??{},B=Ft(qt)??{},P={url:Pt({}),page:Pt({}),navigating:he(null),updated:ne()};function bt(t){I[t]=C()}function ke(t,a){let e=t+1;for(;I[e];)delete I[e],e+=1;for(e=a+1;B[e];)delete B[e],e+=1}function V(t,a=!1){return a?location.replace(t.href):location.href=t.href,new Promise(()=>{})}async function Mt(){if("serviceWorker"in navigator){const t=await navigator.serviceWorker.getRegistration(L||"/");t&&await t.update()}}function Tt(){}let kt,ht,Q,U,dt,b;const Z=[],tt=[];let v=null;function pt(){var t;(t=v==null?void 0:v.fork)==null||t.then(a=>a==null?void 0:a.discard()),v=null}const G=new Map,Bt=new Set,Vt=new Set,F=new Set;let m={branch:[],error:null,url:null},Ht=!1,et=!1,Ot=!0,H=!1,K=!1,Yt=!1,St=!1,zt,E,R,O;const at=new Set,Ct=new Map;async function He(t,a,e){var o,s,i,c,l;(o=globalThis.__sveltekit_1wncwk)!=null&&o.data&&globalThis.__sveltekit_1wncwk.data,document.URL!==location.href&&(location.href=location.href),b=t,await((i=(s=t.hooks).init)==null?void 0:i.call(s)),kt=we(t),U=document.documentElement,dt=a,ht=t.nodes[0],Q=t.nodes[1],ht(),Q(),E=(c=history.state)==null?void 0:c[$],R=(l=history.state)==null?void 0:l[M],E||(E=R=Date.now(),history.replaceState({...history.state,[$]:E,[M]:R},""));const r=I[E];function n(){r&&(history.scrollRestoration="manual",scrollTo(r.x,r.y))}e?(n(),await $e(dt,e)):(await D({type:"enter",url:mt(b.hash?Ke(new URL(location.href)):location.href),replace_state:!0}),n()),Ne()}function Se(){Z.length=0,St=!1}function Wt(t){tt.some(a=>a==null?void 0:a.snapshot)&&(B[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=B[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),Wt(R),It(qt,B)}async function Jt(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(je)}}),a.invalidateAll&&X().then(X).then(()=>{Ct.forEach(({resource:o},s)=>{var i;n!=null&&n.includes(s)&&((i=o.refresh)==null||i.call(o))})})}async function Re(t){if(t.id!==(v==null?void 0:v.id)){pt();const a={};at.add(a),v={id:t.id,token:a,promise:Qt({...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 Xt(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),zt=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:ve(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 Ue(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 Ae({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 Qt({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=Ue(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)&&!Le(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 Jt(new URL(s.location,location.href),{},0);throw s}}async function Ie(t){const a=t.href;if(G.has(a))return G.get(a);let e;try{const r=(async()=>{let n=await b.hooks.reroute({url:new URL(t),fetch:async(o,s)=>xe(o,s,t).promise})??t;if(typeof n=="string"){const o=new URL(t);b.hash?o.hash=n:o.pathname=n,n=o}return n})();G.set(a,r),e=await r}catch{G.delete(a);return}return e}async function ot(t,a){if(t&&!_t(t,L,b.hash)){const e=await Ie(t);if(!e)return;const r=Te(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 Te(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 Zt({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||Bt.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):Zt({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 Qt(f);if(!p){if(_t(a,L,b.hash))return await V(a,o);p=await te(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 Mt(),await V(a,o));if(Se(),bt(_),Wt(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,W={[$]:E+=k,[M]:R+=k,[$t]:s};(o?history.replaceState:history.pushState).call(history,W,"",a),o||ke(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(Vt,N=>N(d.navigation)))).filter(N=>typeof N=="function");if(k.length>0){let N=function(){k.forEach(st=>{F.delete(st)})};k.push(N),k.forEach(st=>{F.add(st)})}m=p.state,p.props.page&&(p.props.page.url=a);const W=y&&await y;W?S=W.commit():(zt.$set(p.props),fe(p.props.page),S=(j=ue)==null?void 0:j()),Yt=!0}else await Xt(p,dt,!1);const{activeElement:A}=document;await S,await X(),await X();let T=null;if(Ot){const k=e?e.scroll:n?C():null;k?scrollTo(k.x,k.y):(T=a.hash&&document.getElementById(ee(a)))?T.scrollIntoView():scrollTo(0,0)}const z=document.activeElement!==A&&document.activeElement!==document.body;!r&&!z&&qe(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 te(t,a,e,r,n){return t.origin===Dt&&t.pathname===location.pathname&&!Ht?await Lt({status:r,error:e,url:t,route:a}):await V(t,n)}function Oe(){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=Nt(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=J(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;Re(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=J(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=ye(t);return b.hooks.handleError({error:t,event:a,status:e,message:r})??{message:r}}function Ce(t,a){Ee(()=>(t.add(a),()=>{t.delete(a)}))}function Ye(t){Ce(Vt,t)}function ze(t,a={}){return t=new URL(mt(t)),t.origin!==Dt?Promise.reject(new Error("goto: invalid URL")):Jt(t,a,0)}function je(t){if(typeof t=="function")Z.push(t);else{const{href:a}=new URL(t,location.href);Z.push(e=>e.href===a)}}function Ne(){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"))}};Bt.forEach(s=>s(o))}r?(e.preventDefault(),e.returnValue=""):history.scrollRestoration="auto"}),addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&jt()}),(a=navigator.connection)!=null&&a.saveData||Oe(),U.addEventListener("click",async e=>{if(e.button||e.which!==1||e.metaKey||e.ctrlKey||e.shiftKey||e.altKey||e.defaultPrevented)return;const r=Nt(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=J(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)){Zt({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=J(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[$]){const n=e.state[$];if(O={},n===E)return;const o=I[n],s=e.state[$t]??{},i=new URL(e.state[re]??location.href),c=e.state[M],l=m.url?it(location)===it(m.url):!1;if(c===R&&(Yt||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,[$]:++E,[M]:R},"",location.href))});for(const e of document.querySelectorAll("link"))be.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 $e(t,{status:a=200,error:e,node_ids:r,params:n,route:o,server_route:s,data:i,form:c}){Ht=!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=De(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 Ke(t){const a=new URL(t);return a.hash=decodeURIComponent(t.hash),a}function ee(t){let a;if(b.hash){const[,,e]=t.hash.split("#",3);a=e??""}else a=t.hash.slice(1);return decodeURIComponent(a)}export{He as a,ze as g,Ye as o,P as s}; diff --git a/apps/dashboard/build/_app/immutable/chunks/dCAmqaEc.js.br b/apps/dashboard/build/_app/immutable/chunks/dCAmqaEc.js.br deleted file mode 100644 index af54f98..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/dCAmqaEc.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/dCAmqaEc.js.gz b/apps/dashboard/build/_app/immutable/chunks/dCAmqaEc.js.gz deleted file mode 100644 index dd113f9..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/dCAmqaEc.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/g5OnrUYZ.js b/apps/dashboard/build/_app/immutable/chunks/g5OnrUYZ.js deleted file mode 100644 index 0ca5483..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/g5OnrUYZ.js +++ /dev/null @@ -1 +0,0 @@ -import{x as S,Q as h,P as k,R as x,S as T}from"./wpu9U-D0.js";function t(r,i){return r===i||(r==null?void 0:r[T])===i}function A(r={},i,a,c){return S(()=>{var f,s;return h(()=>{f=s,s=[],k(()=>{r!==a(...s)&&(i(r,...s),f&&t(a(...f),r)&&i(null,...f))})}),()=>{x(()=>{s&&t(a(...s),r)&&i(null,...s)})}}),r}export{A as b}; diff --git a/apps/dashboard/build/_app/immutable/chunks/g5OnrUYZ.js.br b/apps/dashboard/build/_app/immutable/chunks/g5OnrUYZ.js.br deleted file mode 100644 index 86c4713..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/g5OnrUYZ.js.br +++ /dev/null @@ -1 +0,0 @@ -< dkN_6ߥ`/1'<\8DڢҖ'nc}CK"pCB^+m0_ e<1!070Dt/K`1OzVZ*E3qGP4*> R]\HPhg׉`Nx9%^r;%U]6%`ruN͡h \ No newline at end of file diff --git a/apps/dashboard/build/_app/immutable/chunks/g5OnrUYZ.js.gz b/apps/dashboard/build/_app/immutable/chunks/g5OnrUYZ.js.gz deleted file mode 100644 index 4f31ccd..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/g5OnrUYZ.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/wpu9U-D0.js b/apps/dashboard/build/_app/immutable/chunks/wpu9U-D0.js deleted file mode 100644 index b5a27f7..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/wpu9U-D0.js +++ /dev/null @@ -1 +0,0 @@ -var wn=Object.defineProperty;var mt=e=>{throw TypeError(e)};var yn=(e,t,n)=>t in e?wn(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var ye=(e,t,n)=>yn(e,typeof t!="symbol"?t+"":t,n),We=(e,t,n)=>t.has(e)||mt("Cannot "+n);var p=(e,t,n)=>(We(e,t,"read from private field"),n?n.call(e):t.get(e)),F=(e,t,n)=>t.has(e)?mt("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,n),W=(e,t,n,r)=>(We(e,t,"write to private field"),r?r.call(e,n):t.set(e,n),n),X=(e,t,n)=>(We(e,t,"access private method"),n);var mn=Array.isArray,En=Array.prototype.indexOf,Ae=Array.prototype.includes,yr=Array.from,mr=Object.defineProperty,xe=Object.getOwnPropertyDescriptor,gn=Object.getOwnPropertyDescriptors,Tn=Object.prototype,bn=Array.prototype,Ct=Object.getPrototypeOf,Et=Object.isExtensible;const An=()=>{};function Er(e){return e()}function Sn(e){for(var t=0;t{e=r,t=s});return{promise:n,resolve:e,reject:t}}function gr(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,Pe=4,Me=8,Pt=1<<24,K=16,U=32,we=64,Rn=128,P=512,g=1024,R=2048,V=4096,Y=8192,Q=16384,ne=32768,Ve=65536,gt=1<<17,Mt=1<<18,Fe=1<<19,Ft=1<<20,Tr=1<<25,_e=65536,Je=1<<21,it=1<<22,ee=1<<23,ue=Symbol("$state"),br=Symbol("legacy props"),Ar=Symbol(""),fe=new class extends Error{constructor(){super(...arguments);ye(this,"name","StaleReactionError");ye(this,"message","The reaction that called `getAbortSignal()` was re-run or destroyed")}};var kt;const Rr=!!((kt=globalThis.document)!=null&&kt.contentType)&&globalThis.document.contentType.includes("xml"),Le=3,Lt=8;function Nn(){throw new Error("https://svelte.dev/e/async_derived_orphan")}function Nr(e,t,n){throw new Error("https://svelte.dev/e/each_key_duplicate")}function On(e){throw new Error("https://svelte.dev/e/effect_in_teardown")}function xn(){throw new Error("https://svelte.dev/e/effect_in_unowned_derived")}function kn(e){throw new Error("https://svelte.dev/e/effect_orphan")}function Dn(){throw new Error("https://svelte.dev/e/effect_update_depth_exceeded")}function Or(){throw new Error("https://svelte.dev/e/hydration_failed")}function xr(e){throw new Error("https://svelte.dev/e/props_invalid_value")}function Cn(){throw new Error("https://svelte.dev/e/state_descriptors_fixed")}function In(){throw new Error("https://svelte.dev/e/state_prototype_fixed")}function Pn(){throw new Error("https://svelte.dev/e/state_unsafe_mutation")}function kr(){throw new Error("https://svelte.dev/e/svelte_boundary_reset_onerror")}const Dr=1,Cr=2,Ir=4,Pr=8,Mr=16,Fr=1,Lr=2,jr=4,Hr=8,Yr=16,jt=1,Mn=2,Fn="[",Ln="[!",qr="[?",jn="]",lt={},T=Symbol(),Hn="http://www.w3.org/1999/xhtml",Vr="http://www.w3.org/2000/svg",Ur="http://www.w3.org/1998/Math/MathML";function ot(e){console.warn("https://svelte.dev/e/hydration_mismatch")}function Br(){console.warn("https://svelte.dev/e/select_multiple_invalid_value")}function Gr(){console.warn("https://svelte.dev/e/svelte_boundary_reset_noop")}let D=!1;function $r(e){D=e}let y;function ve(e){if(e===null)throw ot(),lt;return y=e}function Yn(){return ve(se(y))}function zr(e){if(D){if(se(y)!==null)throw ot(),lt;y=e}}function Kr(e=1){if(D){for(var t=e,n=y;t--;)n=se(n);y=n}}function Wr(e=!0){for(var t=0,n=y;;){if(n.nodeType===Lt){var r=n.data;if(r===jn){if(t===0)return n;t-=1}else(r===Fn||r===Ln||r[0]==="["&&!isNaN(Number(r.slice(1))))&&(t+=1)}var s=se(n);e&&n.remove(),n=s}}function Xr(e){if(!e||e.nodeType!==Lt)throw ot(),lt;return e.data}function Ht(e){return e===this.v}function qn(e,t){return e!=e?t==t:e!==t||e!==null&&typeof e=="object"||typeof e=="function"}function Yt(e){return!qn(e,this.v)}let Ge=!1;function Zr(){Ge=!0}let S=null;function Ue(e){S=e}function Jr(e,t=!1,n){S={p:S,i:!1,c:null,e:null,s:e,x:null,l:Ge&&!t?{s:null,u:null,$:[]}:null}}function Qr(e){var t=S,n=t.e;if(n!==null){t.e=null;for(var r of n)rn(r)}return t.i=!0,S=t.p,{}}function je(){return!Ge||S!==null&&S.l===null}let ae=[];function qt(){var e=ae;ae=[],Sn(e)}function Tt(e){if(ae.length===0&&!ke){var t=ae;queueMicrotask(()=>{t===ae&&qt()})}ae.push(e)}function Vn(){for(;ae.length>0;)qt()}function Un(e){var t=h;if(t===null)return _.f|=ee,e;if((t.f&ne)===0&&(t.f&Pe)===0)throw e;Be(e,t)}function Be(e,t){for(;t!==null;){if((t.f&Rn)!==0){if((t.f&ne)===0)throw e;try{t.b.error(e);return}catch(n){e=n}}t=t.parent}throw e}const Bn=-7169;function E(e,t){e.f=e.f&Bn|t}function ut(e){(e.f&P)!==0||e.deps===null?E(e,g):E(e,V)}function Vt(e){if(e!==null)for(const t of e)(t.f&A)===0||(t.f&_e)===0||(t.f^=_e,Vt(t.deps))}function Gn(e,t,n){(e.f&R)!==0?t.add(e):(e.f&V)!==0&&n.add(e),Vt(e.deps),E(e,g)}const Ye=new Set;let d=null,bt=null,b=null,O=[],$e=null,Qe=!1,ke=!1;var Ee,ge,le,Te,Ce,Ie,oe,$,be,C,et,tt,nt,Ut;const ht=class ht{constructor(){F(this,C);ye(this,"current",new Map);ye(this,"previous",new Map);F(this,Ee,new Set);F(this,ge,new Set);F(this,le,0);F(this,Te,0);F(this,Ce,null);F(this,Ie,new Set);F(this,oe,new Set);F(this,$,new Map);ye(this,"is_fork",!1);F(this,be,!1)}skip_effect(t){p(this,$).has(t)||p(this,$).set(t,{d:[],m:[]})}unskip_effect(t){var n=p(this,$).get(t);if(n){p(this,$).delete(t);for(var r of n.d)E(r,R),z(r);for(r of n.m)E(r,V),z(r)}}process(t){var s;O=[],this.apply();var n=[],r=[];for(const f of t)X(this,C,tt).call(this,f,n,r);if(X(this,C,et).call(this)){X(this,C,nt).call(this,r),X(this,C,nt).call(this,n);for(const[f,i]of p(this,$))zt(f,i)}else{for(const f of p(this,Ee))f();p(this,Ee).clear(),p(this,le)===0&&X(this,C,Ut).call(this),bt=this,d=null,At(r),At(n),bt=null,(s=p(this,Ce))==null||s.resolve()}b=null}capture(t,n){n!==T&&!this.previous.has(t)&&this.previous.set(t,n),(t.f&ee)===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(),O.length>0){if(Bt(),d!==null&&d!==this)return}else p(this,le)===0&&this.process([]);this.deactivate()}discard(){for(const t of p(this,ge))t(this);p(this,ge).clear()}increment(t){W(this,le,p(this,le)+1),t&&W(this,Te,p(this,Te)+1)}decrement(t){W(this,le,p(this,le)-1),t&&W(this,Te,p(this,Te)-1),!p(this,be)&&(W(this,be,!0),Tt(()=>{W(this,be,!1),X(this,C,et).call(this)?O.length>0&&this.flush():this.revive()}))}revive(){for(const t of p(this,Ie))p(this,oe).delete(t),E(t,R),z(t);for(const t of p(this,oe))E(t,V),z(t);this.flush()}oncommit(t){p(this,Ee).add(t)}ondiscard(t){p(this,ge).add(t)}settled(){return(p(this,Ce)??W(this,Ce,It())).promise}static ensure(){if(d===null){const t=d=new ht;Ye.add(d),ke||Tt(()=>{d===t&&t.flush()})}return d}apply(){}};Ee=new WeakMap,ge=new WeakMap,le=new WeakMap,Te=new WeakMap,Ce=new WeakMap,Ie=new WeakMap,oe=new WeakMap,$=new WeakMap,be=new WeakMap,C=new WeakSet,et=function(){return this.is_fork||p(this,Te)>0},tt=function(t,n,r){t.f^=g;for(var s=t.first;s!==null;){var f=s.f,i=(f&(U|we))!==0,o=i&&(f&g)!==0,a=o||(f&Y)!==0||p(this,$).has(s);if(!a&&s.fn!==null){i?s.f^=g:(f&Pe)!==0?n.push(s):He(s)&&((f&K)!==0&&p(this,oe).add(s),Ne(s));var l=s.first;if(l!==null){s=l;continue}}for(;s!==null;){var c=s.next;if(c!==null){s=c;break}s=s.parent}}},nt=function(t){for(var n=0;n1){this.previous.clear();var t=b,n=!0;for(const f of Ye){if(f===this){n=!1;continue}const i=[];for(const[a,l]of this.current){if(f.current.has(a))if(n&&l!==f.current.get(a))f.current.set(a,l);else continue;i.push(a)}if(i.length===0)continue;const o=[...f.current.keys()].filter(a=>!this.current.has(a));if(o.length>0){var r=O;O=[];const a=new Set,l=new Map;for(const c of i)Gt(c,o,a,l);if(O.length>0){d=f,f.apply();for(const c of O)X(s=f,C,tt).call(s,c,[],[]);f.deactivate()}O=r}}d=null,b=t}Ye.delete(this)};let Se=ht;function $n(e){var t=ke;ke=!0;try{for(var n;;){if(Vn(),O.length===0&&(d==null||d.flush(),O.length===0))return $e=null,n;Bt()}}finally{ke=t}}function Bt(){Qe=!0;var e=null;try{for(var t=0;O.length>0;){var n=Se.ensure();if(t++>1e3){var r,s;zn()}n.process(O),te.clear()}}finally{O=[],Qe=!1,$e=null}}function zn(){try{Dn()}catch(e){Be(e,$e)}}let L=null;function At(e){var t=e.length;if(t!==0){for(var n=0;n0)){te.clear();for(const s of L){if((s.f&(Q|Y))!==0)continue;const f=[s];let i=s.parent;for(;i!==null;)L.has(i)&&(L.delete(i),f.push(i)),i=i.parent;for(let o=f.length-1;o>=0;o--){const a=f[o];(a.f&(Q|Y))===0&&Ne(a)}}L.clear()}}L=null}}function Gt(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?Gt(s,t,n,r):(f&(it|K))!==0&&(f&R)===0&&$t(s,t,r)&&(E(s,R),z(s))}}function $t(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(Ae.call(t,s))return!0;if((s.f&A)!==0&&$t(s,t,n))return n.set(s,!0),!0}return n.set(e,!1),!1}function z(e){var t=$e=e,n=t.b;if(n!=null&&n.is_pending&&(e.f&(Pe|Me|Pt))!==0&&(e.f&ne)===0){n.defer_effect(e);return}for(;t.parent!==null;){t=t.parent;var r=t.f;if(Qe&&t===h&&(r&K)!==0&&(r&Mt)===0&&(r&ne)!==0)return;if((r&(we|U))!==0){if((r&g)===0)return;t.f^=g}}O.push(t)}function zt(e,t){if(!((e.f&U)!==0&&(e.f&g)!==0)){(e.f&R)!==0?t.d.push(e):(e.f&V)!==0&&t.m.push(e),E(e,g);for(var n=e.first;n!==null;)zt(n,t),n=n.next}}function Kn(e,t,n,r){const s=je()?ct:Jn;var f=e.filter(u=>!u.settled);if(n.length===0&&f.length===0){r(t.map(s));return}var i=h,o=Wn(),a=f.length===1?f[0].promise:f.length>1?Promise.all(f.map(u=>u.promise)):null;function l(u){o();try{r(u)}catch(v){(i.f&Q)===0&&Be(v,i)}rt()}if(n.length===0){a.then(()=>l(t.map(s)));return}function c(){o(),Promise.all(n.map(u=>Zn(u))).then(u=>l([...t.map(s),...u])).catch(u=>Be(u,i))}a?a.then(c):c()}function Wn(){var e=h,t=_,n=S,r=d;return function(f=!0){Re(e),re(t),Ue(n),f&&(r==null||r.activate())}}function rt(e=!0){Re(null),re(null),Ue(null),e&&(d==null||d.deactivate())}function Xn(){var e=h.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 ct(e){var t=A|R,n=_!==null&&(_.f&A)!==0?_:null;return h!==null&&(h.f|=Fe),{ctx:S,deps:null,effects:null,equals:Ht,f:t,fn:e,reactions:null,rv:0,v:T,wv:0,parent:n??h,ac:null}}function Zn(e,t,n){h===null&&Nn();var s=void 0,f=vt(T),i=!_,o=new Map;return or(()=>{var v;var a=It();s=a.promise;try{Promise.resolve(e()).then(a.resolve,a.reject).finally(rt)}catch(m){a.reject(m),rt()}var l=d;if(i){var c=Xn();(v=o.get(l))==null||v.reject(fe),o.delete(l),o.set(l,a)}const u=(m,w=void 0)=>{if(l.activate(),w)w!==fe&&(f.f|=ee,ft(f,w));else{(f.f&ee)!==0&&(f.f^=ee),ft(f,m);for(const[G,N]of o){if(o.delete(G),G===l)break;N.reject(fe)}}c&&c()};a.promise.then(u,m=>u(null,m||"unknown"))}),lr(()=>{for(const a of o.values())a.reject(fe)}),new Promise(a=>{function l(c){function u(){c===s?a(f):l(s)}c.then(u,u)}l(s)})}function es(e){const t=ct(e);return on(t),t}function Jn(e){const t=ct(e);return t.equals=Yt,t}function Qn(e){var t=e.effects;if(t!==null){e.effects=null;for(var n=0;n0&&!Xt&&nr()}return t}function nr(){Xt=!1;for(const e of st)(e.f&g)!==0&&E(e,V),He(e)&&Ne(e);st.clear()}function ns(e,t=1){var n=me(e),r=t===1?n++:n--;return J(e,n),r}function Xe(e){J(e,e.v+1)}function Zt(e,t){var n=e.reactions;if(n!==null)for(var r=je(),s=n.length,f=0;f{if(ce===f)return o();var a=_,l=ce;re(null),xt(f);var c=o();return re(a),xt(l),c};return r&&n.set("length",Z(e.length)),new Proxy(e,{defineProperty(o,a,l){(!("value"in l)||l.configurable===!1||l.enumerable===!1||l.writable===!1)&&Cn();var c=n.get(a);return c===void 0?i(()=>{var u=Z(l.value);return n.set(a,u),u}):J(c,l.value,!0),!0},deleteProperty(o,a){var l=n.get(a);if(l===void 0){if(a in o){const c=i(()=>Z(T));n.set(a,c),Xe(s)}}else J(l,T),Xe(s);return!0},get(o,a,l){var m;if(a===ue)return e;var c=n.get(a),u=a in o;if(c===void 0&&(!u||(m=xe(o,a))!=null&&m.writable)&&(c=i(()=>{var w=Oe(u?o[a]:T),G=Z(w);return G}),n.set(a,c)),c!==void 0){var v=me(c);return v===T?void 0:v}return Reflect.get(o,a,l)},getOwnPropertyDescriptor(o,a){var l=Reflect.getOwnPropertyDescriptor(o,a);if(l&&"value"in l){var c=n.get(a);c&&(l.value=me(c))}else if(l===void 0){var u=n.get(a),v=u==null?void 0:u.v;if(u!==void 0&&v!==T)return{enumerable:!0,configurable:!0,value:v,writable:!0}}return l},has(o,a){var v;if(a===ue)return!0;var l=n.get(a),c=l!==void 0&&l.v!==T||Reflect.has(o,a);if(l!==void 0||h!==null&&(!c||(v=xe(o,a))!=null&&v.writable)){l===void 0&&(l=i(()=>{var m=c?Oe(o[a]):T,w=Z(m);return w}),n.set(a,l));var u=me(l);if(u===T)return!1}return c},set(o,a,l,c){var yt;var u=n.get(a),v=a in o;if(r&&a==="length")for(var m=l;mZ(T)),n.set(m+"",w))}if(u===void 0)(!v||(yt=xe(o,a))!=null&&yt.writable)&&(u=i(()=>Z(void 0)),J(u,Oe(l)),n.set(a,u));else{v=u.v!==T;var G=i(()=>Oe(l));J(u,G)}var N=Reflect.getOwnPropertyDescriptor(o,a);if(N!=null&&N.set&&N.set.call(c,l),!v){if(r&&typeof a=="string"){var wt=n.get("length"),Ke=Number(a);Number.isInteger(Ke)&&Ke>=wt.v&&J(wt,Ke+1)}Xe(s)}return!0},ownKeys(o){me(s);var a=Reflect.ownKeys(o).filter(u=>{var v=n.get(u);return v===void 0||v.v!==T});for(var[l,c]of n)c.v!==T&&!(l in o)&&a.push(l);return a},setPrototypeOf(){In()}})}function St(e){try{if(e!==null&&typeof e=="object"&&ue in e)return e[ue]}catch{}return e}function rs(e,t){return Object.is(St(e),St(t))}var Rt,rr,Jt,Qt,en;function ss(){if(Rt===void 0){Rt=window,rr=document,Jt=/Firefox/.test(navigator.userAgent);var e=Element.prototype,t=Node.prototype,n=Text.prototype;Qt=xe(t,"firstChild").get,en=xe(t,"nextSibling").get,Et(e)&&(e.__click=void 0,e.__className=void 0,e.__attributes=null,e.__style=void 0,e.__e=void 0),Et(n)&&(n.__t=void 0)}}function de(e=""){return document.createTextNode(e)}function j(e){return Qt.call(e)}function se(e){return en.call(e)}function fs(e,t){if(!D)return j(e);var n=j(y);if(n===null)n=y.appendChild(de());else if(t&&n.nodeType!==Le){var r=de();return n==null||n.before(r),ve(r),r}return t&&ze(n),ve(n),n}function as(e,t=!1){if(!D){var n=j(e);return n instanceof Comment&&n.data===""?se(n):n}if(t){if((y==null?void 0:y.nodeType)!==Le){var r=de();return y==null||y.before(r),ve(r),r}ze(y)}return y}function is(e,t=1,n=!1){let r=D?y:e;for(var s;t--;)s=r,r=se(r);if(!D)return r;if(n){if((r==null?void 0:r.nodeType)!==Le){var f=de();return r===null?s==null||s.after(f):r.before(f),ve(f),f}ze(r)}return ve(r),r}function sr(e){e.textContent=""}function ls(){return!1}function fr(e,t,n){return document.createElementNS(t??Hn,e,void 0)}function ze(e){if(e.nodeValue.length<65536)return;let t=e.nextSibling;for(;t!==null&&t.nodeType===Le;)t.remove(),e.nodeValue+=t.nodeValue,t=e.nextSibling}function os(e){D&&j(e)!==null&&sr(e)}let Nt=!1;function ar(){Nt||(Nt=!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 dt(e){var t=_,n=h;re(null),Re(null);try{return e()}finally{re(t),Re(n)}}function us(e,t,n,r=n){e.addEventListener(t,()=>dt(n));const s=e.__on_r;s?e.__on_r=()=>{s(),r(!0)}:e.__on_r=()=>r(!0),ar()}function tn(e){h===null&&(_===null&&kn(),xn()),he&&On()}function ir(e,t){var n=t.last;n===null?t.last=t.first=e:(n.next=e,e.prev=n,t.last=e)}function B(e,t,n){var r=h;r!==null&&(r.f&Y)!==0&&(e|=Y);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{Ne(s)}catch(o){throw pe(s),o}else t!==null&&z(s);var f=s;if(n&&f.deps===null&&f.teardown===null&&f.nodes===null&&f.first===f.last&&(f.f&Fe)===0&&(f=f.first,(e&K)!==0&&(e&Ve)!==0&&f!==null&&(f.f|=Ve)),f!==null&&(f.parent=r,r!==null&&ir(f,r),_!==null&&(_.f&A)!==0&&(e&we)===0)){var i=_;(i.effects??(i.effects=[])).push(f)}return s}function nn(){return _!==null&&!H}function lr(e){const t=B(Me,null,!1);return E(t,g),t.teardown=e,t}function cs(e){tn();var t=h.f,n=!_&&(t&U)!==0&&(t&ne)===0;if(n){var r=S;(r.e??(r.e=[])).push(e)}else return rn(e)}function rn(e){return B(Pe|Ft,e,!1)}function _s(e){return tn(),B(Me|Ft,e,!0)}function vs(e){Se.ensure();const t=B(we|Fe,e,!0);return(n={})=>new Promise(r=>{n.outro?_r(t,()=>{pe(t),r(void 0)}):(pe(t),r(void 0))})}function ds(e){return B(Pe,e,!1)}function or(e){return B(it|Fe,e,!0)}function ps(e,t=0){return B(Me|t,e,!0)}function hs(e,t=[],n=[],r=[]){Kn(r,t,n,s=>{B(Me,()=>e(...s.map(me)),!0)})}function ws(e,t=0){var n=B(K|t,e,!0);return n}function ys(e){return B(U|Fe,e,!0)}function sn(e){var t=e.teardown;if(t!==null){const n=he,r=_;Ot(!0),re(null);try{t.call(null)}finally{Ot(n),re(r)}}}function pt(e,t=!1){var n=e.first;for(e.first=e.last=null;n!==null;){const s=n.ac;s!==null&&dt(()=>{s.abort(fe)});var r=n.next;(n.f&we)!==0?n.parent=null:pe(n,t),n=r}}function ur(e){for(var t=e.first;t!==null;){var n=t.next;(t.f&U)===0&&pe(t),t=n}}function pe(e,t=!0){var n=!1;(t||(e.f&Mt)!==0)&&e.nodes!==null&&e.nodes.end!==null&&(cr(e.nodes.start,e.nodes.end),n=!0),pt(e,t&&!n),De(e,0),E(e,Q);var r=e.nodes&&e.nodes.t;if(r!==null)for(const f of r)f.stop();sn(e);var s=e.parent;s!==null&&s.first!==null&&fn(e),e.next=e.prev=e.teardown=e.ctx=e.deps=e.fn=e.nodes=e.ac=null}function cr(e,t){for(;e!==null;){var n=e===t?null:se(e);e.remove(),e=n}}function fn(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 _r(e,t,n=!0){var r=[];an(e,r,!0);var s=()=>{n&&pe(e),t&&t()},f=r.length;if(f>0){var i=()=>--f||s();for(var o of r)o.out(i)}else s()}function an(e,t,n){if((e.f&Y)===0){e.f^=Y;var r=e.nodes&&e.nodes.t;if(r!==null)for(const o of r)(o.is_global||n)&&t.push(o);for(var s=e.first;s!==null;){var f=s.next,i=(s.f&Ve)!==0||(s.f&U)!==0&&(e.f&K)!==0;an(s,t,i?n:!1),s=f}}}function ms(e){ln(e,!0)}function ln(e,t){if((e.f&Y)!==0){e.f^=Y,(e.f&g)===0&&(E(e,R),z(e));for(var n=e.first;n!==null;){var r=n.next,s=(n.f&Ve)!==0||(n.f&U)!==0;ln(n,s?t:!1),n=r}var f=e.nodes&&e.nodes.t;if(f!==null)for(const i of f)(i.is_global||t)&&i.in()}}function Es(e,t){if(e.nodes)for(var n=e.nodes.start,r=e.nodes.end;n!==null;){var s=n===r?null:se(n);t.append(n),n=s}}let qe=!1,he=!1;function Ot(e){he=e}let _=null,H=!1;function re(e){_=e}let h=null;function Re(e){h=e}let M=null;function on(e){_!==null&&(M===null?M=[e]:M.push(e))}let x=null,k=0,I=null;function vr(e){I=e}let un=1,ie=0,ce=ie;function xt(e){ce=e}function cn(){return++un}function He(e){var t=e.f;if((t&R)!==0)return!0;if(t&A&&(e.f&=~_e),(t&V)!==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 _n(e,t,n=!0){var r=e.reactions;if(r!==null&&!(M!==null&&Ae.call(M,e)))for(var s=0;s{e.ac.abort(fe)}),e.ac=null);try{e.f|=Je;var c=e.fn,u=c();e.f|=ne;var v=e.deps,m=d==null?void 0:d.is_fork;if(x!==null){var w;if(m||De(e,k),v!==null&&k>0)for(v.length=k+x.length,w=0;we});function pr(e){return(Ze==null?void 0:Ze.createHTML(e))??e}function hn(e){var t=fr("template");return t.innerHTML=pr(e.replaceAll("","")),t.content}function q(e,t){var n=h;n.nodes===null&&(n.nodes={start:e,end:t,a:null,t:null})}function Ss(e,t){var n=(t&jt)!==0,r=(t&Mn)!==0,s,f=!e.startsWith("");return()=>{if(D)return q(y,null),y;s===void 0&&(s=hn(f?e:""+e),n||(s=j(s)));var i=r||Jt?document.importNode(s,!0):s.cloneNode(!0);if(n){var o=j(i),a=i.lastChild;q(o,a)}else q(i,i);return i}}function hr(e,t,n="svg"){var r=!e.startsWith(""),s=(t&jt)!==0,f=`<${n}>${r?e:""+e}`,i;return()=>{if(D)return q(y,null),y;if(!i){var o=hn(f),a=j(o);if(s)for(i=document.createDocumentFragment();j(a);)i.appendChild(j(a));else i=j(a)}var l=i.cloneNode(!0);if(s){var c=j(l),u=l.lastChild;q(c,u)}else q(l,l);return l}}function Rs(e,t){return hr(e,t,"svg")}function Ns(e=""){if(!D){var t=de(e+"");return q(t,t),t}var n=y;return n.nodeType!==Le?(n.before(n=de()),ve(n)):ze(n),q(n,n),n}function Os(){if(D)return q(y,null),y;var e=document.createDocumentFragment(),t=document.createComment(""),n=de();return e.append(t,n),q(t,n),e}function xs(e,t){if(D){var n=h;((n.f&ne)===0||n.nodes.end===null)&&(n.nodes.end=y),Yn();return}e!==null&&e.before(t)}export{ys as $,Br as A,rs as B,bt as C,de as D,Ve as E,Fe as F,D as G,Mt as H,Lt as I,se as J,$r as K,ve as L,y as M,j as N,gs as O,bs as P,ps as Q,Tt as R,ue as S,Yn as T,Xr as U,Fn as V,Ln as W,Wr as X,ms as Y,pe as Z,_r as _,xs as a,Se as a$,Es as a0,ls as a1,S as a2,_s as a3,Sn as a4,Er as a5,As as a6,ct as a7,Zr as a8,ns as a9,Vr as aA,Ur as aB,je as aC,Ir as aD,jn as aE,ft as aF,Tr as aG,Nr as aH,yr as aI,Dr as aJ,Mr as aK,vt as aL,Cr as aM,Y as aN,U as aO,Pr as aP,sr as aQ,Ar as aR,Hn as aS,Ct as aT,gn as aU,Rr as aV,ar as aW,nn as aX,Xe as aY,Rn as aZ,qr as a_,qn as aa,An as ab,ts as ac,mr as ad,xe as ae,xr as af,jr as ag,he as ah,h as ai,Q as aj,Hr as ak,Ge as al,Lr as am,Fr as an,Jn as ao,Yr as ap,br as aq,rr as ar,$n as as,Ts as at,Rs as au,cr as av,ot as aw,lt as ax,q as ay,fr as az,Qr as b,E as b0,R as b1,z as b2,V as b3,Gn as b4,Re as b5,re as b6,Ue as b7,Un as b8,_ as b9,Be as ba,kr as bb,Gr as bc,dt as bd,ss as be,Or as bf,vs as bg,Os as c,Oe as d,fs as e,as as f,me as g,is as h,J as i,Ss as j,os as k,gr as l,Ns as m,Kr as n,cs as o,Jr as p,ws as q,zr as r,Z as s,hs as t,es as u,us as v,d as w,ds as x,lr as y,mn as z}; diff --git a/apps/dashboard/build/_app/immutable/chunks/wpu9U-D0.js.br b/apps/dashboard/build/_app/immutable/chunks/wpu9U-D0.js.br deleted file mode 100644 index 6e70ac5..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/wpu9U-D0.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/wpu9U-D0.js.gz b/apps/dashboard/build/_app/immutable/chunks/wpu9U-D0.js.gz deleted file mode 100644 index 65c3116..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/wpu9U-D0.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/entry/app.C-NL1yUd.js b/apps/dashboard/build/_app/immutable/entry/app.C-NL1yUd.js new file mode 100644 index 0000000..e3fba45 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/entry/app.C-NL1yUd.js @@ -0,0 +1,2 @@ +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["../nodes/0.DHxskm8N.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/EM_PBt2C.js","../chunks/RBGf_S-E.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.BgGPnSIe.js","../nodes/2.CD5F7bS_.js","../nodes/3.CQLLmTOU.js","../nodes/4.BSlP3-UA.js","../chunks/B_YDQCB6.js","../nodes/5.B300rRjT.js","../chunks/DMu1Byux.js","../assets/5.DQ_AfUnN.css","../nodes/6.B_eyyG0t.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.Dp-knJux.js","../nodes/11.BLR7H2sn.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.DHxskm8N.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.BgGPnSIe.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.CQLLmTOU.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.B_eyyG0t.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.Dp-knJux.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.BLR7H2sn.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.C-NL1yUd.js.br b/apps/dashboard/build/_app/immutable/entry/app.C-NL1yUd.js.br new file mode 100644 index 0000000..fa9e751 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/entry/app.C-NL1yUd.js.br differ diff --git a/apps/dashboard/build/_app/immutable/entry/app.C-NL1yUd.js.gz b/apps/dashboard/build/_app/immutable/entry/app.C-NL1yUd.js.gz new file mode 100644 index 0000000..0bb9964 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/entry/app.C-NL1yUd.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/entry/app.Dg6iMka9.js b/apps/dashboard/build/_app/immutable/entry/app.Dg6iMka9.js deleted file mode 100644 index 36a42cf..0000000 --- a/apps/dashboard/build/_app/immutable/entry/app.Dg6iMka9.js +++ /dev/null @@ -1,2 +0,0 @@ -const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["../nodes/0.BmfNagK9.js","../chunks/Bzak7iHL.js","../chunks/TZu9D97Z.js","../chunks/wpu9U-D0.js","../chunks/D8mhvFt8.js","../chunks/DKve45Wd.js","../chunks/BWk3o_TN.js","../chunks/60_R_Vbt.js","../chunks/LDOJP_6N.js","../chunks/CnZzd20v.js","../chunks/g5OnrUYZ.js","../chunks/ByYB047u.js","../chunks/dCAmqaEc.js","../chunks/Bxs5UR9-.js","../chunks/BhIgFntf.js","../chunks/BLadwbF7.js","../chunks/EqHb-9AZ.js","../chunks/CqMQEF-F.js","../chunks/CcUbQ_Wl.js","../chunks/CZfHMhLI.js","../chunks/D7A-gG4Z.js","../assets/Icon.tTjeJXhC.css","../assets/0.Cf27G70K.css","../nodes/1.MT4EnKSP.js","../nodes/2.DEnQkIHv.js","../nodes/3.C8tBBpzF.js","../nodes/4.BpOpkZuP.js","../chunks/P1-U_Xsj.js","../chunks/BHDZZvku.js","../assets/PageHeader.Dmxpik8H.css","../chunks/DcKTNC6e.js","../chunks/DPdYG9yN.js","../nodes/5.CK5gRe3F.js","../assets/5.Tl8-WHJj.css","../nodes/6.DZyLUVX2.js","../chunks/CmbJHhgy.js","../assets/Dropdown.C2Z-7Phd.css","../assets/6.DQ_AfUnN.css","../nodes/7.yWYTsQ1Q.js","../chunks/DzesjbbJ.js","../assets/7.F0TwMZ5M.css","../nodes/8.D5dP0-E2.js","../nodes/9.Vz-x3Q_x.js","../nodes/10.DYHIt_do.js","../assets/10.g4OzM5ih.css","../nodes/11.Db7dgOeT.js","../chunks/C-SOZ1Oi.js","../chunks/DrafHjYM.js","../chunks/Dp1pzeXC.js","../assets/11.BxoW8Jf1.css","../nodes/12.CO2CXIFj.js","../nodes/13.BQoci-vM.js","../nodes/14.CDp0vmq1.js","../assets/14.Bjd0S47S.css","../nodes/15.C05K0kWE.js","../assets/15.BzuIe_Oj.css","../nodes/16.CrQpRrFW.js","../nodes/17.3ASmJvJ6.js","../assets/17.ChjqzJHo.css","../nodes/18.43xZFMsD.js","../assets/18.BnHgRQtR.css","../nodes/19.BYwd4oWS.js","../assets/19.C2qtIyf6.css","../nodes/20.Di2Q3Va0.js","../assets/20.CO50G5tF.css","../nodes/21.BAlasPHS.js","../nodes/22.C719k-1W.js","../assets/22.DKhUrxcR.css"])))=>i.map(i=>d[i]); -var U=e=>{throw TypeError(e)};var X=(e,t,r)=>t.has(e)||U("Cannot "+r);var n=(e,t,r)=>(X(e,t,"read from private field"),r?r.call(e):t.get(e)),B=(e,t,r)=>t.has(e)?U("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,r),F=(e,t,r,i)=>(X(e,t,"write to private field"),i?i.call(e,r):t.set(e,r),r);import{_ as o}from"../chunks/Dp1pzeXC.js";import{G as z,T as nt,q as mt,E as ut,U as ct,X as dt,L as lt,K as J,V as ft,M as pt,i as C,aq as vt,g as p,as as ht,ad as Et,ac as gt,p as Pt,a3 as Rt,o as Ot,O as Tt,f as R,h as yt,a as v,b as At,s as G,c as A,e as Lt,r as Dt,j as Q,u as I,m as It,t as Vt}from"../chunks/wpu9U-D0.js";import{h as bt,m as xt,u as kt,s as wt}from"../chunks/D8mhvFt8.js";import"../chunks/Bzak7iHL.js";import{o as jt}from"../chunks/TZu9D97Z.js";import{i as w}from"../chunks/DKve45Wd.js";import{B as Ct}from"../chunks/BWk3o_TN.js";import{b as V}from"../chunks/g5OnrUYZ.js";import{p as j}from"../chunks/ByYB047u.js";function b(e,t,r){var i;z&&(i=pt,nt());var _=new Ct(e);mt(()=>{var m=t()??null;if(z){var a=ct(i),s=a===ft,d=m!==null;if(s!==d){var x=dt();lt(x),_.anchor=x,J(!1),_.ensure(m,m&&(O=>r(O,m))),J(!0);return}}_.ensure(m,m&&(O=>r(O,m)))},ut)}function St(e){return class extends Mt{constructor(t){super({component:e,...t})}}}var h,c;class Mt{constructor(t){B(this,h);B(this,c);var m;var r=new Map,i=(a,s)=>{var d=gt(s,!1,!1);return r.set(a,d),d};const _=new Proxy({...t.props||{},$$events:{}},{get(a,s){return p(r.get(s)??i(s,Reflect.get(a,s)))},has(a,s){return s===vt?!0:(p(r.get(s)??i(s,Reflect.get(a,s))),Reflect.has(a,s))},set(a,s,d){return C(r.get(s)??i(s,d),d),Reflect.set(a,s,d)}});F(this,c,(t.hydrate?bt:xt)(t.component,{target:t.target,anchor:t.anchor,props:_,context:t.context,intro:t.intro??!1,recover:t.recover,transformError:t.transformError})),(!((m=t==null?void 0:t.props)!=null&&m.$$host)||t.sync===!1)&&ht(),F(this,h,_.$$events);for(const a of Object.keys(n(this,c)))a==="$set"||a==="$destroy"||a==="$on"||Et(this,a,{get(){return n(this,c)[a]},set(s){n(this,c)[a]=s},enumerable:!0});n(this,c).$set=a=>{Object.assign(_,a)},n(this,c).$destroy=()=>{kt(n(this,c))}}$set(t){n(this,c).$set(t)}$on(t,r){n(this,h)[t]=n(this,h)[t]||[];const i=(..._)=>r.call(this,..._);return n(this,h)[t].push(i),()=>{n(this,h)[t]=n(this,h)[t].filter(_=>_!==i)}}$destroy(){n(this,c).$destroy()}}h=new WeakMap,c=new WeakMap;const tr={};var Nt=Q('
'),qt=Q(" ",1);function Bt(e,t){Pt(t,!0);let r=j(t,"components",23,()=>[]),i=j(t,"data_0",3,null),_=j(t,"data_1",3,null),m=j(t,"data_2",3,null);Rt(()=>t.stores.page.set(t.page)),Ot(()=>{t.stores,t.page,t.constructors,r(),t.form,i(),_(),m(),t.stores.page.notify()});let a=G(!1),s=G(!1),d=G(null);jt(()=>{const u=t.stores.page.subscribe(()=>{p(a)&&(C(s,!0),Tt().then(()=>{C(d,document.title||"untitled page",!0)}))});return C(a,!0),u});const x=I(()=>t.constructors[2]);var O=qt(),Y=R(O);{var Z=u=>{const E=I(()=>t.constructors[0]);var g=A(),L=R(g);b(L,()=>p(E),(P,T)=>{V(T(P,{get data(){return i()},get form(){return t.form},get params(){return t.page.params},children:(l,Gt)=>{var H=A(),et=R(H);{var at=y=>{const S=I(()=>t.constructors[1]);var D=A(),M=R(D);b(M,()=>p(S),(N,q)=>{V(q(N,{get data(){return _()},get form(){return t.form},get params(){return t.page.params},children:(f,Yt)=>{var K=A(),st=R(K);b(st,()=>p(x),(it,_t)=>{V(_t(it,{get data(){return m()},get form(){return t.form},get params(){return t.page.params}}),k=>r()[2]=k,()=>{var k;return(k=r())==null?void 0:k[2]})}),v(f,K)},$$slots:{default:!0}}),f=>r()[1]=f,()=>{var f;return(f=r())==null?void 0:f[1]})}),v(y,D)},ot=y=>{const S=I(()=>t.constructors[1]);var D=A(),M=R(D);b(M,()=>p(S),(N,q)=>{V(q(N,{get data(){return _()},get form(){return t.form},get params(){return t.page.params}}),f=>r()[1]=f,()=>{var f;return(f=r())==null?void 0:f[1]})}),v(y,D)};w(et,y=>{t.constructors[2]?y(at):y(ot,!1)})}v(l,H)},$$slots:{default:!0}}),l=>r()[0]=l,()=>{var l;return(l=r())==null?void 0:l[0]})}),v(u,g)},$=u=>{const E=I(()=>t.constructors[0]);var g=A(),L=R(g);b(L,()=>p(E),(P,T)=>{V(T(P,{get data(){return i()},get form(){return t.form},get params(){return t.page.params}}),l=>r()[0]=l,()=>{var l;return(l=r())==null?void 0:l[0]})}),v(u,g)};w(Y,u=>{t.constructors[1]?u(Z):u($,!1)})}var tt=yt(Y,2);{var rt=u=>{var E=Nt(),g=Lt(E);{var L=P=>{var T=It();Vt(()=>wt(T,p(d))),v(P,T)};w(g,P=>{p(s)&&P(L)})}Dt(E),v(u,E)};w(tt,u=>{p(a)&&u(rt)})}v(e,O),At()}const rr=St(Bt),er=[()=>o(()=>import("../nodes/0.BmfNagK9.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]),import.meta.url),()=>o(()=>import("../nodes/1.MT4EnKSP.js"),__vite__mapDeps([23,1,15,3,4,13,2,12]),import.meta.url),()=>o(()=>import("../nodes/2.DEnQkIHv.js"),__vite__mapDeps([24,1,3,8,6]),import.meta.url),()=>o(()=>import("../nodes/3.C8tBBpzF.js"),__vite__mapDeps([25,1,15,3,2,4,12,13]),import.meta.url),()=>o(()=>import("../nodes/4.BpOpkZuP.js"),__vite__mapDeps([26,1,2,3,4,5,6,27,7,9,19,14,11,18,28,8,20,21,29,30,31]),import.meta.url),()=>o(()=>import("../nodes/5.CK5gRe3F.js"),__vite__mapDeps([32,1,2,3,4,5,6,7,27,16,9,11,28,8,20,21,29,30,12,13,19,14,33]),import.meta.url),()=>o(()=>import("../nodes/6.DZyLUVX2.js"),__vite__mapDeps([34,1,3,4,5,6,7,27,16,11,28,8,20,21,29,35,10,36,30,37]),import.meta.url),()=>o(()=>import("../nodes/7.yWYTsQ1Q.js"),__vite__mapDeps([38,1,3,4,5,6,7,39,27,19,16,11,13,2,28,8,20,21,29,31,40]),import.meta.url),()=>o(()=>import("../nodes/8.D5dP0-E2.js"),__vite__mapDeps([41,1,2,3,4,5,6,7,27,9,16,18,28,8,11,20,21,29,30,31]),import.meta.url),()=>o(()=>import("../nodes/9.Vz-x3Q_x.js"),__vite__mapDeps([42,1,3,4,5,6,7,27,16,9,19,28,8,11,20,21,29,30,31]),import.meta.url),()=>o(()=>import("../nodes/10.DYHIt_do.js"),__vite__mapDeps([43,1,3,4,5,6,7,27,16,11,14,18,28,8,20,21,29,30,44]),import.meta.url),()=>o(()=>import("../nodes/11.Db7dgOeT.js"),__vite__mapDeps([45,46,1,2,3,4,5,6,7,16,9,11,13,10,18,47,15,48,20,21,35,36,19,14,49]),import.meta.url),()=>o(()=>import("../nodes/12.CO2CXIFj.js"),__vite__mapDeps([50,1,2,3,4,5,6,7,27,16,9,12,13,19,18,11,28,8,20,21,29,30,31]),import.meta.url),()=>o(()=>import("../nodes/13.BQoci-vM.js"),__vite__mapDeps([51,1,2,3,4,5,6,7,27,19,28,8,11,20,21,29,35,16,10,36,30,31]),import.meta.url),()=>o(()=>import("../nodes/14.CDp0vmq1.js"),__vite__mapDeps([52,1,2,3,4,5,6,7,27,16,9,19,18,28,8,11,20,21,29,30,35,10,36,31,53]),import.meta.url),()=>o(()=>import("../nodes/15.C05K0kWE.js"),__vite__mapDeps([54,1,2,3,4,5,6,7,27,11,28,8,20,21,29,30,17,14,18,19,55]),import.meta.url),()=>o(()=>import("../nodes/16.CrQpRrFW.js"),__vite__mapDeps([56,1,2,3,4,5,6,7,27,16,28,8,11,20,21,29,30]),import.meta.url),()=>o(()=>import("../nodes/17.3ASmJvJ6.js"),__vite__mapDeps([57,1,2,3,4,5,6,7,39,27,16,9,10,19,11,18,28,8,20,21,29,30,31,58]),import.meta.url),()=>o(()=>import("../nodes/18.43xZFMsD.js"),__vite__mapDeps([59,1,2,3,4,5,6,7,19,16,11,18,28,8,20,21,29,35,10,36,60]),import.meta.url),()=>o(()=>import("../nodes/19.BYwd4oWS.js"),__vite__mapDeps([61,1,2,3,4,5,6,7,27,16,11,19,14,17,18,28,8,20,21,29,62]),import.meta.url),()=>o(()=>import("../nodes/20.Di2Q3Va0.js"),__vite__mapDeps([63,1,2,3,4,5,6,7,27,16,19,28,8,11,20,21,29,30,31,18,64]),import.meta.url),()=>o(()=>import("../nodes/21.BAlasPHS.js"),__vite__mapDeps([65,1,2,3,4,5,6,7,27,16,19,18,28,8,11,20,21,29,30,35,10,36]),import.meta.url),()=>o(()=>import("../nodes/22.C719k-1W.js"),__vite__mapDeps([66,1,2,3,4,5,6,7,39,16,9,47,10,13,67]),import.meta.url)],ar=[],or={"/":[3],"/(app)/activation":[4,[2]],"/(app)/blackbox":[5,[2]],"/(app)/contradictions":[6,[2]],"/(app)/dreams":[7,[2]],"/(app)/duplicates":[8,[2]],"/(app)/explore":[9,[2]],"/(app)/feed":[10,[2]],"/(app)/graph":[11,[2]],"/(app)/importance":[12,[2]],"/(app)/intentions":[13,[2]],"/(app)/memories":[14,[2]],"/(app)/memory-prs":[15,[2]],"/(app)/patterns":[16,[2]],"/(app)/reasoning":[17,[2]],"/(app)/schedule":[18,[2]],"/(app)/settings":[19,[2]],"/(app)/stats":[20,[2]],"/(app)/timeline":[21,[2]],"/waitlist":[22]},W={handleError:(({error:e})=>{console.error(e)}),reroute:(()=>{}),transport:{}},Ft=Object.fromEntries(Object.entries(W.transport).map(([e,t])=>[e,t.decode])),sr=Object.fromEntries(Object.entries(W.transport).map(([e,t])=>[e,t.encode])),ir=!1,_r=(e,t)=>Ft[e](t);export{_r as decode,Ft as decoders,or as dictionary,sr as encoders,ir as hash,W as hooks,tr as matchers,er as nodes,rr as root,ar as server_loads}; diff --git a/apps/dashboard/build/_app/immutable/entry/app.Dg6iMka9.js.br b/apps/dashboard/build/_app/immutable/entry/app.Dg6iMka9.js.br deleted file mode 100644 index 35fff68..0000000 Binary files a/apps/dashboard/build/_app/immutable/entry/app.Dg6iMka9.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/entry/app.Dg6iMka9.js.gz b/apps/dashboard/build/_app/immutable/entry/app.Dg6iMka9.js.gz deleted file mode 100644 index 54e99b8..0000000 Binary files a/apps/dashboard/build/_app/immutable/entry/app.Dg6iMka9.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/entry/start.BLzz4N6-.js b/apps/dashboard/build/_app/immutable/entry/start.BLzz4N6-.js new file mode 100644 index 0000000..3b50d66 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/entry/start.BLzz4N6-.js @@ -0,0 +1 @@ +import{a as r}from"../chunks/EM_PBt2C.js";import{w as t}from"../chunks/RBGf_S-E.js";export{t as load_css,r as start}; diff --git a/apps/dashboard/build/_app/immutable/entry/start.BLzz4N6-.js.br b/apps/dashboard/build/_app/immutable/entry/start.BLzz4N6-.js.br new file mode 100644 index 0000000..0dff7d1 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/entry/start.BLzz4N6-.js.br differ diff --git a/apps/dashboard/build/_app/immutable/entry/start.BLzz4N6-.js.gz b/apps/dashboard/build/_app/immutable/entry/start.BLzz4N6-.js.gz new file mode 100644 index 0000000..f9b4f0d Binary files /dev/null and b/apps/dashboard/build/_app/immutable/entry/start.BLzz4N6-.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/entry/start.BvFEq078.js b/apps/dashboard/build/_app/immutable/entry/start.BvFEq078.js deleted file mode 100644 index c225395..0000000 --- a/apps/dashboard/build/_app/immutable/entry/start.BvFEq078.js +++ /dev/null @@ -1 +0,0 @@ -import{a as r}from"../chunks/dCAmqaEc.js";import{w as t}from"../chunks/Bxs5UR9-.js";export{t as load_css,r as start}; diff --git a/apps/dashboard/build/_app/immutable/entry/start.BvFEq078.js.br b/apps/dashboard/build/_app/immutable/entry/start.BvFEq078.js.br deleted file mode 100644 index bed1076..0000000 Binary files a/apps/dashboard/build/_app/immutable/entry/start.BvFEq078.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/entry/start.BvFEq078.js.gz b/apps/dashboard/build/_app/immutable/entry/start.BvFEq078.js.gz deleted file mode 100644 index dcc3604..0000000 Binary files a/apps/dashboard/build/_app/immutable/entry/start.BvFEq078.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/0.BmfNagK9.js b/apps/dashboard/build/_app/immutable/nodes/0.BmfNagK9.js deleted file mode 100644 index 4397bdd..0000000 --- a/apps/dashboard/build/_app/immutable/nodes/0.BmfNagK9.js +++ /dev/null @@ -1,86 +0,0 @@ -import"../chunks/Bzak7iHL.js";import{o as et}from"../chunks/TZu9D97Z.js";import{c as tt,f as ve,a as v,h as s,e as a,r as t,t as F,j as h,p as Ye,n as ce,g as e,b as Qe,s as le,d as kt,i as f,u as Y,m as st}from"../chunks/wpu9U-D0.js";import{s as g,d as Ue,a as ie,e as Ge,b as _t,w as ct,g as vt}from"../chunks/D8mhvFt8.js";import{i as z}from"../chunks/DKve45Wd.js";import{e as Fe,a as Ae,i as Be,s as ee,r as yt}from"../chunks/60_R_Vbt.js";import{s as rt}from"../chunks/LDOJP_6N.js";import{b as wt}from"../chunks/CnZzd20v.js";import{b as $t}from"../chunks/g5OnrUYZ.js";import{s as pe,a as He}from"../chunks/ByYB047u.js";import{s as At,o as Ct,g as nt}from"../chunks/dCAmqaEc.js";import{b as _e}from"../chunks/Bxs5UR9-.js";import{s as pt,b as ut,a as mt,e as Mt,w as it,u as Tt,i as Et,f as St}from"../chunks/BhIgFntf.js";import{i as jt}from"../chunks/BLadwbF7.js";import{s as ft}from"../chunks/EqHb-9AZ.js";import{t as Ie}from"../chunks/CqMQEF-F.js";import{a as qe}from"../chunks/CZfHMhLI.js";import{I as Le}from"../chunks/D7A-gG4Z.js";const Dt=()=>{const n=At;return{page:{subscribe:n.page.subscribe},navigating:{subscribe:n.navigating.subscribe},updated:n.updated}},It={subscribe(n){return Dt().page.subscribe(n)}};var Ft=h('
');function Nt(n){const l=()=>pe(pt,"$suppressedCount",o),[o,i]=He();var b=tt(),M=ve(b);{var k=N=>{var w=Ft(),y=s(a(w),2),p=a(y);t(y),t(w),F(()=>g(p,`Actively forgetting ${l()??""} ${l()===1?"memory":"memories"}`)),v(N,w)};z(M,N=>{l()>0&&N(k)})}v(n,b),i()}var Lt=h(''),Bt=h('
');function Vt(n,l){Ye(l,!1);const o=()=>pe(Ie,"$toasts",i),[i,b]=He(),M={DreamCompleted:"✦",ConsolidationCompleted:"◉",ConnectionDiscovered:"⟷",MemoryPromoted:"↑",MemoryDemoted:"↓",MemorySuppressed:"◬",MemoryUnsuppressed:"◉",Rac1CascadeSwept:"✺",MemoryDeleted:"✕",HookVerdictRecorded:"⚑"};function k(p){return M[p]??"◆"}function N(p){Ie.dismiss(p.id)}function w(p,c){(p.key==="Enter"||p.key===" ")&&(p.preventDefault(),Ie.dismiss(c.id))}jt();var y=Bt();Fe(y,5,o,p=>p.id,(p,c)=>{var T=Lt(),L=s(a(T),2),B=a(L),Z=a(B),Q=a(Z,!0);t(Z);var te=s(Z,2),ue=a(te,!0);t(te),t(B);var u=s(B,2),m=a(u,!0);t(u),t(L),ce(2),t(T),F($=>{Ae(T,"aria-label",`${e(c).title??""}: ${e(c).body??""}. Click to dismiss.`),ft(T,`--toast-color: ${e(c).color??""}; --toast-dwell: ${e(c).dwellMs??""}ms;`),g(Q,$),g(ue,e(c).title),g(m,e(c).body)},[()=>k(e(c).type)]),ie("click",T,()=>N(e(c))),ie("keydown",T,$=>w($,e(c))),Ge("mouseenter",T,()=>Ie.pauseDwell(e(c).id,e(c).dwellMs)),Ge("mouseleave",T,()=>Ie.resumeDwell(e(c).id)),Ge("focus",T,()=>Ie.pauseDwell(e(c).id,e(c).dwellMs)),Ge("blur",T,()=>Ie.resumeDwell(e(c).id)),v(p,T)}),t(y),v(n,y),Qe(),b()}Ue(["click","keydown"]);function Ve(n){const l=n.data;if(!l||typeof l!="object")return null;const o=l.timestamp??l.at??l.occurred_at;if(o==null)return null;if(typeof o=="number")return Number.isFinite(o)?o>1e12?o:o*1e3:null;if(typeof o!="string")return null;const i=Date.parse(o);return Number.isFinite(i)?i:null}const Ze=10,ht=3e4,Rt=Ze*ht;function Ot(n,l){const o=l-Rt,i=new Array(Ze).fill(0);for(const M of n){if(M.type==="Heartbeat")continue;const k=Ve(M);if(k===null||kl)continue;const N=Math.min(Ze-1,Math.floor((k-o)/ht));i[N]+=1}const b=Math.max(1,...i);return i.map(M=>({count:M,ratio:M/b}))}function Pt(n,l){const o=l-864e5;for(const i of n){if(i.type!=="DreamCompleted")continue;return(Ve(i)??l)>=o?i:null}return null}function zt(n){if(!n||!n.data)return null;const l=n.data,o=typeof l.insights_generated=="number"?l.insights_generated:typeof l.insightsGenerated=="number"?l.insightsGenerated:null;return o!==null&&Number.isFinite(o)?o:null}function Kt(n,l){let o=null,i=null;for(const N of n)if(!o&&N.type==="DreamStarted"&&(o=N),!i&&N.type==="DreamCompleted"&&(i=N),o&&i)break;if(!o)return!1;const b=Ve(o)??l,M=l-300*1e3;return b=i}return!1}var Wt=h(' at risk',1),Gt=h('0 at risk',1),Yt=h(' at risk',1),Qt=h(' intentions',1),Ut=h('— intentions'),Xt=h('· insights',1),Zt=h(' Last dream: ',1),Jt=h('No recent dream'),ea=h('
'),ta=h('telemetry unavailable'),aa=h('· fail-open',1),sa=h(' vetoes · appeals ',1),ra=h('
DREAMING...
',1),na=h(''),ia=h('
memories · avg retention
');function la(n,l){Ye(l,!0);const o=()=>pe(mt,"$avgRetention",M),i=()=>pe(Mt,"$eventFeed",M),b=()=>pe(ut,"$memoryCount",M),[M,k]=He(),N=Y(()=>Math.round((o()??0)*100)),w=Y(()=>(o()??0)>=.5);let y=le(null);async function p(){try{const r=await qe.retentionDistribution();if(Array.isArray(r.endangered)&&r.endangered.length>0){f(y,r.endangered.length,!0);return}const d=r.distribution??[];let _=0;for(const V of d){const G=/^(\d+)/.exec(V.range);if(!G)continue;const re=Number.parseInt(G[1],10);Number.isFinite(re)&&re<30&&(_+=V.count??0)}f(y,_,!0)}catch{f(y,null)}}let c=le(null);async function T(){var r;try{const d=await qe.intentions("active");f(c,d.total??((r=d.intentions)==null?void 0:r.length)??0,!0)}catch{f(c,null)}}let L=le(kt(Date.now()));const B=Y(()=>{const r=i(),d=Pt(r,e(L)),_=d?Ve(d)??e(L):null,V=_!==null?e(L)-_:null;return{isDreaming:Kt(r,e(L)),recent:d,recentMsAgo:V,insights:zt(d)}}),Z=Y(()=>Ot(i(),e(L)));let Q=le(null),te=le(!1);async function ue(){try{f(Q,await qe.sanhedrin.telemetry(7),!0),f(te,!1)}catch{f(Q,null),f(te,!0)}}const u=Y(()=>Ht(i(),e(L)));et(()=>{p(),T(),ue();const r=setInterval(()=>{f(L,Date.now(),!0)},1e3),d=setInterval(()=>{p(),T(),ue()},6e4);return()=>{clearInterval(r),clearInterval(d)}});var m=ia();let $;var be=a(m),xe=a(be),Ne=a(xe);let Ce;var S=s(Ne,2);let j;t(xe);var x=s(xe,2),ae=a(x,!0);t(x);var K=s(x,6);let U;var he=a(K);t(K),ce(2),t(be);var A=s(be,4),D=a(A);{var q=r=>{var d=Wt(),_=ve(d),V=a(_,!0);t(_),ce(2),F(()=>g(V,e(y))),v(r,d)},oe=r=>{var d=Gt();ce(2),v(r,d)},E=r=>{var d=Yt();ce(2),v(r,d)};z(D,r=>{e(y)!==null&&e(y)>0?r(q):e(y)===0?r(oe,1):r(E,!1)})}t(A);var X=s(A,4),se=a(X);{var de=r=>{var d=Qt(),_=ve(d);let V;var G=s(_,2);let re;var ne=a(G,!0);t(G),ce(2),F(()=>{V=ee(_,1,"inline-flex h-2 w-2 rounded-full svelte-1kk3799",null,V,{"bg-node-pattern":e(c)>5,"animate-ping-slow":e(c)>5,"bg-muted":e(c)<=5}),re=ee(G,1,"tabular-nums svelte-1kk3799",null,re,{"text-node-pattern":e(c)>5,"text-text":e(c)>0&&e(c)<=5,"text-muted":e(c)===0}),g(ne,e(c))}),v(r,d)},Se=r=>{var d=Ut();v(r,d)};z(se,r=>{e(c)!==null?r(de):r(Se,!1)})}t(X);var J=s(X,4),Me=a(J);{var me=r=>{var d=Zt(),_=s(ve(d),4),V=a(_,!0);t(_);var G=s(_,2);{var re=ne=>{var I=Xt(),C=s(ve(I),2),P=a(C,!0);t(C),ce(2),F(()=>g(P,e(B).insights)),v(ne,I)};z(G,ne=>{e(B).insights!==null&&ne(re)})}F(ne=>g(V,ne),[()=>qt(e(B).recentMsAgo)]),v(r,d)},ge=r=>{var d=Jt();v(r,d)};z(Me,r=>{e(B).recent&&e(B).recentMsAgo!==null?r(me):r(ge,!1)})}t(J);var je=s(J,4),ke=s(a(je),2);Fe(ke,21,()=>e(Z),Be,(r,d)=>{var _=ea();F(V=>ft(_,`height: ${V??""}%; opacity: ${e(d).count===0?.18:.5+e(d).ratio*.5};`),[()=>Math.max(10,e(d).ratio*100)]),v(r,_)}),t(ke),t(je);var De=s(je,4),Oe=s(a(De),2);{var Pe=r=>{var d=ta();v(r,d)},ze=r=>{var d=sa(),_=ve(d),V=a(_,!0);t(_);var G=s(_,6),re=a(G,!0);t(G);var ne=s(G,4);{var I=C=>{var P=aa(),O=s(ve(P),2),Te=a(O,!0);t(O),ce(2),F(()=>g(Te,e(Q).failOpen)),v(C,P)};z(ne,C=>{var P;(P=e(Q))!=null&&P.failOpen&&C(I)})}F(()=>{var C,P,O;g(V,((P=(C=e(Q))==null?void 0:C.byVerdict)==null?void 0:P.VETO)??"—"),g(re,((O=e(Q))==null?void 0:O.appeals)??"—")}),v(r,d)};z(Oe,r=>{e(te)?r(Pe):r(ze,!1)})}t(De);var R=s(De,2);{var H=r=>{var d=ra();ce(2),v(r,d)};z(R,r=>{e(B).isDreaming&&r(H)})}var W=s(R,4);{var fe=r=>{var d=na();v(r,d)};z(W,r=>{e(u)&&r(fe)})}t(m),F(()=>{$=ee(m,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,$,{"ambient-flash":e(u)}),Ce=ee(Ne,1,"absolute inline-flex h-full w-full animate-ping rounded-full opacity-75 svelte-1kk3799",null,Ce,{"bg-recall":e(w),"bg-warning":!e(w)}),j=ee(S,1,"relative inline-flex h-2 w-2 rounded-full svelte-1kk3799",null,j,{"bg-recall":e(w),"bg-warning":!e(w)}),g(ae,b()),U=ee(K,1,"svelte-1kk3799",null,U,{"text-recall":e(w),"text-warning":!e(w)}),g(he,`${e(N)??""}%`)}),v(n,m),Qe(),k()}var oa=h(" "),da=h('
  • '),ca=h(' ',1),va=h('

    Appeal recorded.

    '),pa=h('

    No appealable veto in this receipt.

    '),ua=h('
    Claim

    Verdict

    Precedent
      Fix

      Appeal
      '),ma=h('
      ');function fa(n,l){Ye(l,!0);const o=["PASS","NOTE","CAUTION","VETO","APPEALED"];let i=le(null),b=le(""),M=le(!1),k=le(null),N=le(null),w=Y(()=>{var u;return((u=e(i))==null?void 0:u.verdictBar)??(e(b)?"CAUTION":"NOTE")}),y=Y(()=>{var u,m;return((u=e(i))==null?void 0:u.claims.find($=>$.decision==="veto"))??((m=e(i))==null?void 0:m.claims.find($=>$.decision==="appealed"))??null}),p=Y(()=>{var u;return e(y)??((u=e(i))==null?void 0:u.claims[0])??null}),c=Y(()=>!!e(i)||!!e(b));et(()=>{T();const u=window.setInterval(T,4e3);return()=>window.clearInterval(u)});async function T(){var u;try{const m=await qe.sanhedrin.latest();f(i,m.receipt,!0),f(b,""),((u=m.receipt)==null?void 0:u.verdictBar)==="VETO"&&m.receipt.id!==e(N)&&(f(M,!0),f(N,m.receipt.id,!0))}catch(m){f(b,m instanceof Error?m.message:String(m),!0)}}async function L(u){var m;if(!(!e(y)||((m=e(i))==null?void 0:m.verdictBar)!=="VETO")){f(k,u,!0);try{const $=await qe.sanhedrin.appeal(u,void 0,e(y).id,e(i).id);f(i,$.receipt,!0),f(M,!0),f(b,"")}catch($){f(b,$ instanceof Error?$.message:String($),!0)}finally{f(k,null)}}}function B(u){if(!u)return"";const m=new Date(u);return Number.isNaN(m.getTime())?"":m.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}function Z(u){var m;return(m=u==null?void 0:u.precedent)!=null&&m.length?u.precedent.map($=>$.summary??$.command??"Precedent recorded.").slice(0,3):["No precedent attached."]}var Q=tt(),te=ve(Q);{var ue=u=>{var m=ma(),$=a(m),be=s(a($),2);Fe(be,21,()=>o,Be,(A,D)=>{var q=oa();let oe;var E=a(q,!0);t(q),F(()=>{Ae(q,"aria-current",e(D)===e(w)?"true":void 0),oe=ee(q,1,"svelte-1j425e6",null,oe,{active:e(D)===e(w)}),g(E,e(D))}),v(A,q)}),t(be);var xe=s(be,2),Ne=a(xe);t(xe);var Ce=s(xe,2),S=a(Ce);{var j=A=>{var D=st();F(()=>g(D,e(b))),v(A,D)},x=A=>{var D=st();F(()=>g(D,e(i).summary)),v(A,D)};z(S,A=>{e(b)?A(j):e(i)&&A(x,1)})}t(Ce);var ae=s(Ce,2),K=a(ae,!0);t(ae),t($);var U=s($,2);{var he=A=>{var D=ua(),q=a(D),oe=a(q),E=s(a(oe),2),X=a(E,!0);t(E),t(oe);var se=s(oe,2),de=s(a(se),2),Se=a(de);t(de),t(se);var J=s(se,2),Me=s(a(J),2);Fe(Me,21,()=>Z(e(p)),Be,(R,H)=>{var W=da(),fe=a(W,!0);t(W),F(()=>g(fe,e(H))),v(R,W)}),t(Me),t(J);var me=s(J,2),ge=s(a(me),2),je=a(ge,!0);t(ge),t(me),t(q);var ke=s(q,2),De=s(a(ke),2);{var Oe=R=>{var H=ca(),W=ve(H),fe=a(W,!0);t(W);var r=s(W,2),d=a(r,!0);t(r);var _=s(r,2),V=a(_,!0);t(_),F((G,re,ne)=>{W.disabled=G,g(fe,e(k)==="stale"?"Saving":"Stale"),r.disabled=re,g(d,e(k)==="wrong"?"Saving":"Wrong"),_.disabled=ne,g(V,e(k)==="too_strict"?"Saving":"Too strict")},[()=>!!e(k),()=>!!e(k),()=>!!e(k)]),ie("click",W,()=>L("stale")),ie("click",r,()=>L("wrong")),ie("click",_,()=>L("too_strict")),v(R,H)},Pe=R=>{var H=va();v(R,H)},ze=R=>{var H=pa();v(R,H)};z(De,R=>{e(y)&&e(i).verdictBar==="VETO"?R(Oe):e(i).verdictBar==="APPEALED"?R(Pe,1):R(ze,!1)})}t(ke),t(D),F(()=>{var R,H,W,fe;g(X,((R=e(p))==null?void 0:R.text)??e(i).draftPreview),g(Se,`${((H=e(p))==null?void 0:H.decision)??e(i).overall??""} · ${((W=e(p))==null?void 0:W.evidence_state)??e(w)??""}`),g(je,((fe=e(p))==null?void 0:fe.fix)||"No change required.")}),v(A,D)};z(U,A=>{e(M)&&e(i)&&A(he)})}t(m),F((A,D)=>{ee(m,1,A,"svelte-1j425e6"),Ae($,"aria-expanded",e(M)),g(Ne,`Current verdict: ${e(w)??""}`),g(K,D)},[()=>`verdict-bar tone-${e(w).toLowerCase()}`,()=>{var A;return B((A=e(i))==null?void 0:A.createdAt)}]),ie("click",$,()=>f(M,!e(M))),v(u,m)};z(te,u=>{e(c)&&u(ue)})}v(n,Q),Qe()}Ue(["click"]);const gt="vestige.theme",lt="vestige-theme-light",Re=ct("dark"),Je=ct(!0),ot=_t([Re,Je],([n,l])=>n==="auto"?l?"dark":"light":n);function ha(n){return n==="dark"||n==="light"||n==="auto"}function ga(n){if(ha(n)){Re.set(n);try{localStorage.setItem(gt,n)}catch{}}}function Xe(){const n=vt(Re);ga(n==="dark"?"light":n==="light"?"auto":"dark")}function ba(){if(document.getElementById(lt))return;const n=document.createElement("style");n.id=lt,n.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(n)}function dt(n){document.documentElement.dataset.theme=n}let ye=null,Ee=null,we=null,$e=null;function xa(){ye&&Ee&&ye.removeEventListener("change",Ee),$e==null||$e(),we==null||we(),ye=null,Ee=null,$e=null,we=null,ba();let n="dark";try{const l=localStorage.getItem(gt);(l==="dark"||l==="light"||l==="auto")&&(n=l)}catch{}return Re.set(n),ye=window.matchMedia("(prefers-color-scheme: dark)"),Je.set(ye.matches),Ee=l=>Je.set(l.matches),ye.addEventListener("change",Ee),dt(vt(ot)),$e=ot.subscribe(dt),we=Re.subscribe(()=>{}),()=>{ye&&Ee&&ye.removeEventListener("change",Ee),ye=null,Ee=null,$e==null||$e(),we==null||we(),$e=null,we=null}}var ka=h('');function _a(n){const l=()=>pe(Re,"$theme",o),[o,i]=He(),b={dark:"Dark",light:"Light",auto:"Auto (system)"},M={dark:"light",light:"auto",auto:"dark"};let k=Y(l),N=Y(()=>M[e(k)]),w=Y(()=>`Toggle theme: ${b[e(k)]} (click for ${b[e(N)]})`);var y=ka(),p=a(y),c=a(p);let T;var L=s(c,2);let B;var Z=s(L,2);let Q;t(p),t(y),F(()=>{Ae(y,"aria-label",e(w)),Ae(y,"title",e(w)),Ae(y,"data-mode",e(k)),T=ee(c,0,"icon svelte-1cmi4dh",null,T,{active:e(k)==="dark"}),B=ee(L,0,"icon svelte-1cmi4dh",null,B,{active:e(k)==="light"}),Q=ee(Z,0,"icon svelte-1cmi4dh",null,Q,{active:e(k)==="auto"})}),ie("click",y,function(...te){Xe==null||Xe.apply(this,te)}),v(n,y),i()}Ue(["click"]);var ya=h(' '),wa=h('
      '),$a=h(''),Aa=h(' '),Ca=h('
      ',1),Ma=h(''),Ta=h('
      No matches
      '),Ea=h('
      esc
      '),Sa=h(" ",1);function Qa(n,l){Ye(l,!0);const o=()=>pe(It,"$page",w),i=()=>pe(Et,"$isConnected",w),b=()=>pe(ut,"$memoryCount",w),M=()=>pe(mt,"$avgRetention",w),k=()=>pe(Tt,"$uptimeSeconds",w),N=()=>pe(pt,"$suppressedCount",w),[w,y]=He();let p=le(!1),c=le(""),T=le(void 0),L=Y(()=>o().url.pathname.startsWith(_e)?o().url.pathname.slice(_e.length)||"/":o().url.pathname),B=Y(()=>e(L)==="/waitlist"||e(L).startsWith("/waitlist/"));et(()=>{e(B)||it.connect();const S=xa();function j(x){if(e(B))return;if((x.metaKey||x.ctrlKey)&&x.key==="k"){x.preventDefault(),f(p,!e(p)),f(c,""),e(p)&&requestAnimationFrame(()=>{var U;return(U=e(T))==null?void 0:U.focus()});return}if(x.key==="Escape"&&e(p)){f(p,!1);return}if(x.target instanceof HTMLInputElement||x.target instanceof HTMLTextAreaElement)return;if(x.key==="/"){x.preventDefault();const U=document.querySelector('input[type="text"]');U==null||U.focus();return}const K={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"}[x.key.toLowerCase()];K&&!x.metaKey&&!x.ctrlKey&&!x.altKey&&(x.preventDefault(),nt(`${_e}${K}`))}return window.addEventListener("keydown",j),()=>{it.disconnect(),window.removeEventListener("keydown",j),S()}}),Ct(S=>{if(!(!document.startViewTransition||window.matchMedia("(prefers-reduced-motion: reduce)").matches))return new Promise(j=>{document.startViewTransition(async()=>{j(),await S.complete})})});const Z=[{href:"/blackbox",label:"Black Box",icon:"blackbox",shortcut:"B"},{href:"/memory-prs",label:"Memory PRs",icon:"memorypr",shortcut:"Q"},{href:"/graph",label:"Graph",icon:"graph",shortcut:"G"},{href:"/reasoning",label:"Reasoning",icon:"reasoning",shortcut:"R"},{href:"/memories",label:"Memories",icon:"memories",shortcut:"M"},{href:"/timeline",label:"Timeline",icon:"timeline",shortcut:"T"},{href:"/feed",label:"Feed",icon:"feed",shortcut:"F"},{href:"/explore",label:"Explore",icon:"explore",shortcut:"E"},{href:"/activation",label:"Activation",icon:"activation",shortcut:"A"},{href:"/dreams",label:"Dreams",icon:"dreams",shortcut:"D"},{href:"/schedule",label:"Schedule",icon:"schedule",shortcut:"C"},{href:"/importance",label:"Importance",icon:"importance",shortcut:"P"},{href:"/duplicates",label:"Duplicates",icon:"duplicates",shortcut:"U"},{href:"/contradictions",label:"Contradictions",icon:"contradictions",shortcut:"X"},{href:"/patterns",label:"Patterns",icon:"patterns",shortcut:"N"},{href:"/intentions",label:"Intentions",icon:"intentions",shortcut:"I"},{href:"/stats",label:"Stats",icon:"stats",shortcut:"S"},{href:"/settings",label:"Settings",icon:"settings",shortcut:","}],Q=Z.slice(0,5);function te(S,j){const x=j.startsWith(_e)?j.slice(_e.length)||"/":j;return S==="/graph"?x==="/"||x==="/graph":x.startsWith(S)}let ue=Y(()=>e(c)?Z.filter(S=>S.label.toLowerCase().includes(e(c).toLowerCase())):Z);function u(S){f(p,!1),f(c,""),nt(`${_e}${S}`)}var m=Sa(),$=ve(m);{var be=S=>{var j=tt(),x=ve(j);rt(x,()=>l.children),v(S,j)},xe=S=>{var j=Ca(),x=s(ve(j),6),ae=a(x),K=a(ae),U=a(K),he=a(U);Le(he,{name:"logo",size:18,strokeWidth:1.8}),t(U),ce(2),t(K);var A=s(K,2);Fe(A,21,()=>Z,Be,(I,C)=>{const P=Y(()=>te(e(C).href,o().url.pathname));var O=ya(),Te=a(O),We=a(Te);Le(We,{get name(){return e(C).icon},size:18}),t(Te);var Ke=s(Te,2),bt=a(Ke,!0);t(Ke);var at=s(Ke,2),xt=a(at,!0);t(at),t(O),F(()=>{Ae(O,"href",`${_e??""}${e(C).href??""}`),ee(O,1,`nav-link group 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"}`,"svelte-12qhfyh"),g(bt,e(C).label),g(xt,e(C).shortcut)}),v(I,O)}),t(A);var D=s(A,2),q=a(D),oe=a(q);Le(oe,{name:"command",size:14}),ce(4),t(q),t(D);var E=s(D,2),X=a(E),se=a(X),de=s(se,2),Se=a(de,!0);t(de);var J=s(de,2),Me=a(J);_a(Me),t(J),t(X);var me=s(X,2),ge=a(me),je=a(ge);t(ge);var ke=s(ge,2),De=a(ke);t(ke);var Oe=s(ke,2);{var Pe=I=>{var C=wa(),P=a(C);t(C),F(O=>g(P,`up ${O??""}`),[()=>St(k())]),v(I,C)};z(Oe,I=>{k()>0&&I(Pe)})}t(me);var ze=s(me,2);{var R=I=>{var C=$a(),P=a(C);Nt(P),t(C),v(I,C)};z(ze,I=>{N()>0&&I(R)})}t(E),t(ae);var H=s(ae,2),W=a(H);la(W,{});var fe=s(W,2);fa(fe,{});var r=s(fe,2),d=a(r);rt(d,()=>l.children),t(r),t(H);var _=s(H,2),V=a(_),G=a(V);Fe(G,17,()=>Q,Be,(I,C)=>{const P=Y(()=>te(e(C).href,o().url.pathname));var O=Aa(),Te=a(O);Le(Te,{get name(){return e(C).icon},size:20});var We=s(Te,2),Ke=a(We,!0);t(We),t(O),F(()=>{Ae(O,"href",`${_e??""}${e(C).href??""}`),ee(O,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"}`),g(Ke,e(C).label)}),v(I,O)});var re=s(G,2);t(V),t(_),t(x);var ne=s(x,2);Vt(ne,{}),F(I=>{Ae(K,"href",`${_e??""}/graph`),ee(se,1,`w-2 h-2 rounded-full ${i()?"bg-recall animate-pulse-glow":"bg-decay"}`),g(Se,i()?"Connected":"Offline"),g(je,`${b()??""} memories`),g(De,`${I??""}% retention`)},[()=>(M()*100).toFixed(0)]),ie("click",q,()=>{f(p,!0),f(c,""),requestAnimationFrame(()=>{var I;return(I=e(T))==null?void 0:I.focus()})}),ie("click",re,()=>{f(p,!0),f(c,""),requestAnimationFrame(()=>{var I;return(I=e(T))==null?void 0:I.focus()})}),v(S,j)};z($,S=>{e(B)?S(be):S(xe,!1)})}var Ne=s($,2);{var Ce=S=>{var j=Ea(),x=a(j),ae=a(x),K=a(ae),U=a(K);Le(U,{name:"search",size:16}),t(K);var he=s(K,2);yt(he),$t(he,E=>f(T,E),()=>e(T)),ce(2),t(ae);var A=s(ae,2),D=a(A);Fe(D,17,()=>e(ue),Be,(E,X)=>{var se=Ma(),de=a(se),Se=a(de);Le(Se,{get name(){return e(X).icon},size:17}),t(de);var J=s(de,2),Me=a(J,!0);t(J);var me=s(J,2),ge=a(me,!0);t(me),t(se),F(()=>{g(Me,e(X).label),g(ge,e(X).shortcut)}),ie("click",se,()=>u(e(X).href)),v(E,se)});var q=s(D,2);{var oe=E=>{var X=Ta();v(E,X)};z(q,E=>{e(ue).length===0&&E(oe)})}t(A),t(x),t(j),ie("keydown",j,E=>{E.key==="Escape"&&f(p,!1)}),ie("click",j,E=>{E.target===E.currentTarget&&f(p,!1)}),ie("keydown",he,E=>{E.key==="Enter"&&e(ue).length>0&&u(e(ue)[0].href)}),wt(he,()=>e(c),E=>f(c,E)),v(S,j)};z(Ne,S=>{e(p)&&!e(B)&&S(Ce)})}v(n,m),Qe(),y()}Ue(["click","keydown"]);export{Qa as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/0.BmfNagK9.js.br b/apps/dashboard/build/_app/immutable/nodes/0.BmfNagK9.js.br deleted file mode 100644 index 758b7bb..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/0.BmfNagK9.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/0.BmfNagK9.js.gz b/apps/dashboard/build/_app/immutable/nodes/0.BmfNagK9.js.gz deleted file mode 100644 index 738f852..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/0.BmfNagK9.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/0.DHxskm8N.js b/apps/dashboard/build/_app/immutable/nodes/0.DHxskm8N.js new file mode 100644 index 0000000..bb4ce7e --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/0.DHxskm8N.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/EM_PBt2C.js";import{b as U}from"../chunks/RBGf_S-E.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.DHxskm8N.js.br b/apps/dashboard/build/_app/immutable/nodes/0.DHxskm8N.js.br new file mode 100644 index 0000000..f96e50a Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/0.DHxskm8N.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/0.DHxskm8N.js.gz b/apps/dashboard/build/_app/immutable/nodes/0.DHxskm8N.js.gz new file mode 100644 index 0000000..eb166b8 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/0.DHxskm8N.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/1.BgGPnSIe.js b/apps/dashboard/build/_app/immutable/nodes/1.BgGPnSIe.js new file mode 100644 index 0000000..3b78538 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/1.BgGPnSIe.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/RBGf_S-E.js";import{s as k}from"../chunks/EM_PBt2C.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.BgGPnSIe.js.br b/apps/dashboard/build/_app/immutable/nodes/1.BgGPnSIe.js.br new file mode 100644 index 0000000..b89c6ec Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/1.BgGPnSIe.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/1.BgGPnSIe.js.gz b/apps/dashboard/build/_app/immutable/nodes/1.BgGPnSIe.js.gz new file mode 100644 index 0000000..b9bcb3f Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/1.BgGPnSIe.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/1.MT4EnKSP.js b/apps/dashboard/build/_app/immutable/nodes/1.MT4EnKSP.js deleted file mode 100644 index 2a32e99..0000000 --- a/apps/dashboard/build/_app/immutable/nodes/1.MT4EnKSP.js +++ /dev/null @@ -1 +0,0 @@ -import"../chunks/Bzak7iHL.js";import{i as u}from"../chunks/BLadwbF7.js";import{p as g,f as l,t as v,a as d,b as _,j as x,e as a,r as o,h as $}from"../chunks/wpu9U-D0.js";import{s as p}from"../chunks/D8mhvFt8.js";import{p as m}from"../chunks/Bxs5UR9-.js";import{s as b}from"../chunks/dCAmqaEc.js";const k={get error(){return m.error},get status(){return m.status}};b.updated.check;const i=k;var j=x("

      ",1);function B(f,n){g(n,!1),u();var t=j(),r=l(t),c=a(r,!0);o(r);var e=$(r,2),h=a(e,!0);o(e),v(()=>{var s;p(c,i.status),p(h,(s=i.error)==null?void 0:s.message)}),d(f,t),_()}export{B as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/1.MT4EnKSP.js.br b/apps/dashboard/build/_app/immutable/nodes/1.MT4EnKSP.js.br deleted file mode 100644 index 1af4c86..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/1.MT4EnKSP.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/1.MT4EnKSP.js.gz b/apps/dashboard/build/_app/immutable/nodes/1.MT4EnKSP.js.gz deleted file mode 100644 index 27ffe28..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/1.MT4EnKSP.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/10.DYHIt_do.js b/apps/dashboard/build/_app/immutable/nodes/10.DYHIt_do.js deleted file mode 100644 index 47efa48..0000000 --- a/apps/dashboard/build/_app/immutable/nodes/10.DYHIt_do.js +++ /dev/null @@ -1,8 +0,0 @@ -import"../chunks/Bzak7iHL.js";import{p as oe,o as ve,g as e,i as E,e as s,h as v,r,f as ne,t as C,a as u,b as ie,s as W,u as k,j as p,n as X}from"../chunks/wpu9U-D0.js";import{s as F,d as me,a as ue}from"../chunks/D8mhvFt8.js";import{i as P}from"../chunks/DKve45Wd.js";import{e as le,i as ce,s as ae,a as pe}from"../chunks/60_R_Vbt.js";import{a as re,r as se}from"../chunks/P1-U_Xsj.js";import{s as S}from"../chunks/EqHb-9AZ.js";import{p as Z,a as fe,s as ee}from"../chunks/ByYB047u.js";import{w as _e,e as ge,i as xe,c as he}from"../chunks/BhIgFntf.js";import{E as $e}from"../chunks/CcUbQ_Wl.js";import{P as be}from"../chunks/BHDZZvku.js";import{I as te}from"../chunks/D7A-gG4Z.js";import{A as ye}from"../chunks/DcKTNC6e.js";var we=p(' '),Ce=p('
      '),Fe=p('
      ',1),Se=p('
      '),De=p('
      '),ke=p('
      Cognitive Search Pipeline
      ');function Pe(J,M){oe(M,!0);let V=Z(M,"resultCount",3,0),B=Z(M,"durationMs",3,0),R=Z(M,"active",3,!1);const f=[{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 b=W(-1),y=W(!1),w=W(!1);ve(()=>{R()&&!e(y)&&Q()});function Q(){E(y,!0),E(b,-1),E(w,!1);const o=Math.max(1500,(B()||50)*2),t=o/(f.length+1);f.forEach((n,c)=>{setTimeout(()=>{E(b,c,!0)},t*(c+1))}),setTimeout(()=>{E(w,!0),E(y,!1)},o)}var _=ke(),D=s(_),q=v(s(D),2);{var U=o=>{var t=we(),n=s(t);r(t),C(()=>F(n,`${V()??""} results in ${B()??""}ms`)),u(o,t)};P(q,o=>{e(w)&&o(U)})}r(D);var T=v(D,2);le(T,21,()=>f,ce,(o,t,n)=>{const c=k(()=>n<=e(b)),A=k(()=>n===e(b)&&e(y));var x=Fe(),h=ne(x),d=s(h),O=s(d,!0);r(d);var j=v(d,2),Y=s(j,!0);r(j),r(h);var z=v(h,2);{var G=L=>{var H=Ce();C(()=>S(H,`background: ${n{n{ae(d,1,`w-8 h-8 rounded-full flex items-center justify-center text-xs transition-all duration-300 - ${e(A)?"scale-125":""}`),S(d,`background: ${e(c)?e(t).color+"25":"rgba(255,255,255,0.03)"}; - border: 1.5px solid ${(e(c)?e(t).color:"rgba(255,255,255,0.06)")??""}; - color: ${(e(c)?e(t).color:"#4a4a7a")??""}; - box-shadow: ${e(A)?"0 0 12px "+e(t).color+"40":"none"}`),pe(d,"title",e(t).desc),F(O,e(t).icon),S(j,`color: ${(e(c)?e(t).color:"#4a4a7a")??""}`),F(Y,e(t).name)}),u(o,x)}),r(T);var I=v(T,2),l=s(I);{var a=o=>{var t=Se();C(n=>S(t,`width: ${n??""}%; - background: linear-gradient(90deg, #818CF8, #00FFD1, #9D00FF); - transition-duration: ${e(y)?"300ms":"500ms"}`),[()=>e(w)?"100":((e(b)+1)/f.length*100).toFixed(0)]),u(o,t)};P(l,o=>{(e(y)||e(w))&&o(a)})}r(I);var g=v(I,2);{var i=o=>{var t=De(),n=v(s(t),2),c=s(n);r(n),r(t),C(()=>F(c,`Pipeline complete: ${V()??""} memories surfaced from ${f.length??""}-stage cognitive cascade`)),u(o,t)};P(g,o=>{e(w)&&o(i)})}r(_),u(J,_),ie()}var Me=p(' events ',1),Re=p(`

      Quiet for now.

      New memory activity will stream in here the moment it happens — the feed - wakes up the instant Vestige does.

      `),Ae=p(' '),Ne=p('
      '),Ee=p('

      '),Te=p('
      '),Ie=p('
      ');function Ke(J,M){oe(M,!0);const V=()=>ee(xe,"$isConnected",f),B=()=>ee(he,"$isReconnecting",f),R=()=>ee(ge,"$eventFeed",f),[f,b]=fe();function y(l){return new Date(l).toLocaleTimeString()}function w(l){return{MemoryCreated:"memories",MemoryUpdated:"memories",MemoryDeleted:"close",MemoryPromoted:"importance",MemoryDemoted:"importance",SearchPerformed:"search",DreamStarted:"dreams",DreamProgress:"dreams",DreamCompleted:"dreams",ConsolidationStarted:"activation",ConsolidationCompleted:"activation",RetentionDecayed:"timeline",ConnectionDiscovered:"graph",ActivationSpread:"activation",ImportanceScored:"importance",Heartbeat:"pulse"}[l]||"sparkle"}function Q(l){const a=l.data;switch(l.type){case"MemoryCreated":return`New ${a.node_type}: "${String(a.content_preview).slice(0,60)}..."`;case"SearchPerformed":return`Searched "${a.query}" → ${a.result_count} results (${a.duration_ms}ms)`;case"DreamStarted":return`Dream started with ${a.memory_count} memories`;case"DreamCompleted":return`Dream complete: ${a.connections_found} connections, ${a.insights_generated} insights (${a.duration_ms}ms)`;case"ConsolidationStarted":return"Consolidation cycle started";case"ConsolidationCompleted":return`Consolidated ${a.nodes_processed} nodes, ${a.decay_applied} decayed (${a.duration_ms}ms)`;case"ConnectionDiscovered":return`Connection: ${String(a.connection_type)} (weight: ${Number(a.weight).toFixed(2)})`;case"ImportanceScored":return`Scored ${Number(a.composite_score).toFixed(2)}: "${String(a.content_preview).slice(0,50)}..."`;case"MemoryPromoted":return`Promoted → ${(Number(a.new_retention)*100).toFixed(0)}% retention`;case"MemoryDemoted":return`Demoted → ${(Number(a.new_retention)*100).toFixed(0)}% retention`;default:return JSON.stringify(a).slice(0,100)}}let _=k(()=>V()?{color:"var(--color-recall)",label:"Live",live:!0}:B()?{color:"var(--color-importance, #f59e0b)",label:"Reconnecting",live:!1}:{color:"var(--color-decay, #8B95A5)",label:"Offline",live:!1});var D=Ie(),q=s(D);be(q,{icon:"feed",title:"Live Feed",subtitle:"Every thought as it happens — memories born, searches fired, dreams consolidating, connections discovered. Vestige, thinking out loud.",accent:"synapse",children:(l,a)=>{var g=Me(),i=ne(g);let o;var t=s(i);let n;var c=v(t,2),A=s(c,!0);r(c),r(i);var x=v(i,2),h=s(x);ye(h,{get value(){return R().length},class:"text-bright font-semibold"}),X(2),r(x);var d=v(x,2),O=s(d);te(O,{name:"close",size:13}),X(2),r(d),C(()=>{o=ae(i,1,"live-status svelte-1o4vd58",null,o,{idle:!e(_).live}),n=ae(t,1,"live-dot w-2 h-2 rounded-full svelte-1o4vd58",null,n,{"ping-host":e(_).live,breathe:!e(_).live}),S(t,`color:${e(_).color??""};background:${e(_).color??""}`),F(A,e(_).label),d.disabled=R().length===0}),ue("click",d,()=>_e.clearEvents()),u(l,g)},$$slots:{default:!0}});var U=v(q,2);{var T=l=>{var a=Re(),g=s(a),i=s(g);te(i,{name:"feed",size:52,draw:!0}),r(g),X(4),r(a),re(a,o=>{var t;return(t=se)==null?void 0:t(o)}),u(l,a)},I=l=>{var a=Te();le(a,5,R,ce,(g,i,o)=>{const t=k(()=>$e[e(i).type]||"#8B95A5");var n=Ee(),c=s(n),A=s(c);{let m=k(()=>w(e(i).type));te(A,{get name(){return e(m)},size:15})}r(c);var x=v(c,2),h=s(x),d=s(h),O=s(d,!0);r(d);var j=v(d,2);{var Y=m=>{var $=Ae(),N=s($,!0);r($),C(K=>F(N,K),[()=>y(String(e(i).data.timestamp))]),u(m,$)};P(j,m=>{e(i).data.timestamp&&m(Y)})}r(h);var z=v(h,2),G=s(z,!0);r(z);var L=v(z,2);{var H=m=>{var $=Ne(),N=s($);{let K=k(()=>Number(e(i).data.result_count)||0),de=k(()=>Number(e(i).data.duration_ms)||0);Pe(N,{get resultCount(){return e(K)},get durationMs(){return e(de)},active:!0})}r($),u(m,$)};P(L,m=>{e(i).type==="SearchPerformed"&&m(H)})}r(x),r(n),re(n,(m,$)=>{var N;return(N=se)==null?void 0:N(m,$)},()=>({delay:Math.min(o*30,300),y:10})),C(m=>{S(n,`border-left: 3px solid ${e(t)??""}; --evt: ${e(t)??""};`),S(c,`background: ${e(t)??""}1a; color: ${e(t)??""}`),S(d,`color: ${e(t)??""}`),F(O,e(i).type),F(G,m)},[()=>Q(e(i))]),u(g,n)}),r(a),u(l,a)};P(U,l=>{R().length===0?l(T):l(I,!1)})}r(D),u(J,D),ie(),b()}me(["click"]);export{Ke as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/10.DYHIt_do.js.br b/apps/dashboard/build/_app/immutable/nodes/10.DYHIt_do.js.br deleted file mode 100644 index 24213d5..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/10.DYHIt_do.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/10.DYHIt_do.js.gz b/apps/dashboard/build/_app/immutable/nodes/10.DYHIt_do.js.gz deleted file mode 100644 index 7b5fa64..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/10.DYHIt_do.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/10.Dp-knJux.js b/apps/dashboard/build/_app/immutable/nodes/10.Dp-knJux.js new file mode 100644 index 0000000..7ee36c5 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/10.Dp-knJux.js @@ -0,0 +1,4115 @@ +var Bc=Object.defineProperty;var zc=(i,t,e)=>t in i?Bc(i,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):i[t]=e;var kt=(i,t,e)=>zc(i,typeof t!="symbol"?t+"":t,e);import"../chunks/Bzak7iHL.js";import{o as jl,a as Zl}from"../chunks/CNjeV5xa.js";import{s as me,c as va,h as zt,g as B,p as ys,aB as kc,a as Es,e as yt,d as bt,n as Hc,r as xt,t as Ke,u as Gn,f as Kl,j as Vc}from"../chunks/CvjSAYrz.js";import{s as fe,d as $l,a as Fe}from"../chunks/FzvEaXMa.js";import{i as kn}from"../chunks/ciN1mm2W.js";import{e as _s,i as hr}from"../chunks/DTnG8poT.js";import{a as _e,f as Se,c as Gc}from"../chunks/BsvCUYx-.js";import{s as ve,r as xa}from"../chunks/CNfQDikv.js";import{s as Us}from"../chunks/DPl3NjBv.js";import{s as Sr}from"../chunks/Bhad70Ss.js";import{b as Ma}from"../chunks/CVpUe0w3.js";import{b as Jl}from"../chunks/DMu1Byux.js";import{s as Wc,a as Xc}from"../chunks/D81f-o_I.js";import{b as Do}from"../chunks/RBGf_S-E.js";import{b as Yc}from"../chunks/D3XWCg9-.js";import{p as vs}from"../chunks/B_YDQCB6.js";import{N as Sa}from"../chunks/DzfRjky4.js";import{i as qc}from"../chunks/Bz1l2A_1.js";import{a as gi}from"../chunks/DNjM5a-l.js";import{e as jc}from"../chunks/CtkE7HV2.js";/** + * @license + * Copyright 2010-2024 Three.js Authors + * SPDX-License-Identifier: MIT + */const vo="172",Hi={ROTATE:0,DOLLY:1,PAN:2},Oi={ROTATE:0,PAN:1,DOLLY_PAN:2,DOLLY_ROTATE:3},Zc=0,Lo=1,Kc=2,Ql=1,$c=2,An=3,Yn=0,Xe=1,gn=2,Cn=0,Vi=1,Le=2,Uo=3,Io=4,Jc=5,ii=100,Qc=101,th=102,eh=103,nh=104,ih=200,sh=201,rh=202,ah=203,ya=204,Ea=205,oh=206,lh=207,ch=208,hh=209,uh=210,dh=211,fh=212,ph=213,mh=214,ba=0,Ta=1,wa=2,qi=3,Aa=4,Ra=5,Ca=6,Pa=7,tc=0,gh=1,_h=2,Wn=0,vh=1,xh=2,Mh=3,ec=4,Sh=5,yh=6,Eh=7,nc=300,ji=301,Zi=302,Da=303,La=304,Pr=306,Ua=1e3,ri=1001,Ia=1002,Je=1003,bh=1004,Is=1005,vn=1006,Br=1007,ai=1008,Ln=1009,ic=1010,sc=1011,Ms=1012,xo=1013,li=1014,xn=1015,Pn=1016,Mo=1017,So=1018,Ki=1020,rc=35902,ac=1021,oc=1022,dn=1023,lc=1024,cc=1025,Gi=1026,$i=1027,yo=1028,Eo=1029,hc=1030,bo=1031,To=1033,ur=33776,dr=33777,fr=33778,pr=33779,Na=35840,Fa=35841,Oa=35842,Ba=35843,za=36196,ka=37492,Ha=37496,Va=37808,Ga=37809,Wa=37810,Xa=37811,Ya=37812,qa=37813,ja=37814,Za=37815,Ka=37816,$a=37817,Ja=37818,Qa=37819,to=37820,eo=37821,mr=36492,no=36494,io=36495,uc=36283,so=36284,ro=36285,ao=36286,Th=3200,wh=3201,dc=0,Ah=1,Vn="",an="srgb",Ji="srgb-linear",yr="linear",se="srgb",_i=7680,No=519,Rh=512,Ch=513,Ph=514,fc=515,Dh=516,Lh=517,Uh=518,Ih=519,oo=35044,Fo="300 es",Rn=2e3,Er=2001;class hi{addEventListener(t,e){this._listeners===void 0&&(this._listeners={});const n=this._listeners;n[t]===void 0&&(n[t]=[]),n[t].indexOf(e)===-1&&n[t].push(e)}hasEventListener(t,e){if(this._listeners===void 0)return!1;const n=this._listeners;return n[t]!==void 0&&n[t].indexOf(e)!==-1}removeEventListener(t,e){if(this._listeners===void 0)return;const s=this._listeners[t];if(s!==void 0){const r=s.indexOf(e);r!==-1&&s.splice(r,1)}}dispatchEvent(t){if(this._listeners===void 0)return;const n=this._listeners[t.type];if(n!==void 0){t.target=this;const s=n.slice(0);for(let r=0,a=s.length;r>8&255]+Ie[i>>16&255]+Ie[i>>24&255]+"-"+Ie[t&255]+Ie[t>>8&255]+"-"+Ie[t>>16&15|64]+Ie[t>>24&255]+"-"+Ie[e&63|128]+Ie[e>>8&255]+"-"+Ie[e>>16&255]+Ie[e>>24&255]+Ie[n&255]+Ie[n>>8&255]+Ie[n>>16&255]+Ie[n>>24&255]).toLowerCase()}function Yt(i,t,e){return Math.max(t,Math.min(e,i))}function Nh(i,t){return(i%t+t)%t}function zr(i,t,e){return(1-e)*i+e*t}function _n(i,t){switch(t.constructor){case Float32Array:return i;case Uint32Array:return i/4294967295;case Uint16Array:return i/65535;case Uint8Array:return i/255;case Int32Array:return Math.max(i/2147483647,-1);case Int16Array:return Math.max(i/32767,-1);case Int8Array:return Math.max(i/127,-1);default:throw new Error("Invalid component type.")}}function re(i,t){switch(t.constructor){case Float32Array:return i;case Uint32Array:return Math.round(i*4294967295);case Uint16Array:return Math.round(i*65535);case Uint8Array:return Math.round(i*255);case Int32Array:return Math.round(i*2147483647);case Int16Array:return Math.round(i*32767);case Int8Array:return Math.round(i*127);default:throw new Error("Invalid component type.")}}const Fh={DEG2RAD:gr};class St{constructor(t=0,e=0){St.prototype.isVector2=!0,this.x=t,this.y=e}get width(){return this.x}set width(t){this.x=t}get height(){return this.y}set height(t){this.y=t}set(t,e){return this.x=t,this.y=e,this}setScalar(t){return this.x=t,this.y=t,this}setX(t){return this.x=t,this}setY(t){return this.y=t,this}setComponent(t,e){switch(t){case 0:this.x=e;break;case 1:this.y=e;break;default:throw new Error("index is out of range: "+t)}return this}getComponent(t){switch(t){case 0:return this.x;case 1:return this.y;default:throw new Error("index is out of range: "+t)}}clone(){return new this.constructor(this.x,this.y)}copy(t){return this.x=t.x,this.y=t.y,this}add(t){return this.x+=t.x,this.y+=t.y,this}addScalar(t){return this.x+=t,this.y+=t,this}addVectors(t,e){return this.x=t.x+e.x,this.y=t.y+e.y,this}addScaledVector(t,e){return this.x+=t.x*e,this.y+=t.y*e,this}sub(t){return this.x-=t.x,this.y-=t.y,this}subScalar(t){return this.x-=t,this.y-=t,this}subVectors(t,e){return this.x=t.x-e.x,this.y=t.y-e.y,this}multiply(t){return this.x*=t.x,this.y*=t.y,this}multiplyScalar(t){return this.x*=t,this.y*=t,this}divide(t){return this.x/=t.x,this.y/=t.y,this}divideScalar(t){return this.multiplyScalar(1/t)}applyMatrix3(t){const e=this.x,n=this.y,s=t.elements;return this.x=s[0]*e+s[3]*n+s[6],this.y=s[1]*e+s[4]*n+s[7],this}min(t){return this.x=Math.min(this.x,t.x),this.y=Math.min(this.y,t.y),this}max(t){return this.x=Math.max(this.x,t.x),this.y=Math.max(this.y,t.y),this}clamp(t,e){return this.x=Yt(this.x,t.x,e.x),this.y=Yt(this.y,t.y,e.y),this}clampScalar(t,e){return this.x=Yt(this.x,t,e),this.y=Yt(this.y,t,e),this}clampLength(t,e){const n=this.length();return this.divideScalar(n||1).multiplyScalar(Yt(n,t,e))}floor(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this}ceil(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this}round(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this}roundToZero(){return this.x=Math.trunc(this.x),this.y=Math.trunc(this.y),this}negate(){return this.x=-this.x,this.y=-this.y,this}dot(t){return this.x*t.x+this.y*t.y}cross(t){return this.x*t.y-this.y*t.x}lengthSq(){return this.x*this.x+this.y*this.y}length(){return Math.sqrt(this.x*this.x+this.y*this.y)}manhattanLength(){return Math.abs(this.x)+Math.abs(this.y)}normalize(){return this.divideScalar(this.length()||1)}angle(){return Math.atan2(-this.y,-this.x)+Math.PI}angleTo(t){const e=Math.sqrt(this.lengthSq()*t.lengthSq());if(e===0)return Math.PI/2;const n=this.dot(t)/e;return Math.acos(Yt(n,-1,1))}distanceTo(t){return Math.sqrt(this.distanceToSquared(t))}distanceToSquared(t){const e=this.x-t.x,n=this.y-t.y;return e*e+n*n}manhattanDistanceTo(t){return Math.abs(this.x-t.x)+Math.abs(this.y-t.y)}setLength(t){return this.normalize().multiplyScalar(t)}lerp(t,e){return this.x+=(t.x-this.x)*e,this.y+=(t.y-this.y)*e,this}lerpVectors(t,e,n){return this.x=t.x+(e.x-t.x)*n,this.y=t.y+(e.y-t.y)*n,this}equals(t){return t.x===this.x&&t.y===this.y}fromArray(t,e=0){return this.x=t[e],this.y=t[e+1],this}toArray(t=[],e=0){return t[e]=this.x,t[e+1]=this.y,t}fromBufferAttribute(t,e){return this.x=t.getX(e),this.y=t.getY(e),this}rotateAround(t,e){const n=Math.cos(e),s=Math.sin(e),r=this.x-t.x,a=this.y-t.y;return this.x=r*n-a*s+t.x,this.y=r*s+a*n+t.y,this}random(){return this.x=Math.random(),this.y=Math.random(),this}*[Symbol.iterator](){yield this.x,yield this.y}}class Ht{constructor(t,e,n,s,r,a,o,l,c){Ht.prototype.isMatrix3=!0,this.elements=[1,0,0,0,1,0,0,0,1],t!==void 0&&this.set(t,e,n,s,r,a,o,l,c)}set(t,e,n,s,r,a,o,l,c){const h=this.elements;return h[0]=t,h[1]=s,h[2]=o,h[3]=e,h[4]=r,h[5]=l,h[6]=n,h[7]=a,h[8]=c,this}identity(){return this.set(1,0,0,0,1,0,0,0,1),this}copy(t){const e=this.elements,n=t.elements;return e[0]=n[0],e[1]=n[1],e[2]=n[2],e[3]=n[3],e[4]=n[4],e[5]=n[5],e[6]=n[6],e[7]=n[7],e[8]=n[8],this}extractBasis(t,e,n){return t.setFromMatrix3Column(this,0),e.setFromMatrix3Column(this,1),n.setFromMatrix3Column(this,2),this}setFromMatrix4(t){const e=t.elements;return this.set(e[0],e[4],e[8],e[1],e[5],e[9],e[2],e[6],e[10]),this}multiply(t){return this.multiplyMatrices(this,t)}premultiply(t){return this.multiplyMatrices(t,this)}multiplyMatrices(t,e){const n=t.elements,s=e.elements,r=this.elements,a=n[0],o=n[3],l=n[6],c=n[1],h=n[4],d=n[7],p=n[2],u=n[5],g=n[8],_=s[0],m=s[3],f=s[6],T=s[1],E=s[4],y=s[7],D=s[2],w=s[5],C=s[8];return r[0]=a*_+o*T+l*D,r[3]=a*m+o*E+l*w,r[6]=a*f+o*y+l*C,r[1]=c*_+h*T+d*D,r[4]=c*m+h*E+d*w,r[7]=c*f+h*y+d*C,r[2]=p*_+u*T+g*D,r[5]=p*m+u*E+g*w,r[8]=p*f+u*y+g*C,this}multiplyScalar(t){const e=this.elements;return e[0]*=t,e[3]*=t,e[6]*=t,e[1]*=t,e[4]*=t,e[7]*=t,e[2]*=t,e[5]*=t,e[8]*=t,this}determinant(){const t=this.elements,e=t[0],n=t[1],s=t[2],r=t[3],a=t[4],o=t[5],l=t[6],c=t[7],h=t[8];return e*a*h-e*o*c-n*r*h+n*o*l+s*r*c-s*a*l}invert(){const t=this.elements,e=t[0],n=t[1],s=t[2],r=t[3],a=t[4],o=t[5],l=t[6],c=t[7],h=t[8],d=h*a-o*c,p=o*l-h*r,u=c*r-a*l,g=e*d+n*p+s*u;if(g===0)return this.set(0,0,0,0,0,0,0,0,0);const _=1/g;return t[0]=d*_,t[1]=(s*c-h*n)*_,t[2]=(o*n-s*a)*_,t[3]=p*_,t[4]=(h*e-s*l)*_,t[5]=(s*r-o*e)*_,t[6]=u*_,t[7]=(n*l-c*e)*_,t[8]=(a*e-n*r)*_,this}transpose(){let t;const e=this.elements;return t=e[1],e[1]=e[3],e[3]=t,t=e[2],e[2]=e[6],e[6]=t,t=e[5],e[5]=e[7],e[7]=t,this}getNormalMatrix(t){return this.setFromMatrix4(t).invert().transpose()}transposeIntoArray(t){const e=this.elements;return t[0]=e[0],t[1]=e[3],t[2]=e[6],t[3]=e[1],t[4]=e[4],t[5]=e[7],t[6]=e[2],t[7]=e[5],t[8]=e[8],this}setUvTransform(t,e,n,s,r,a,o){const l=Math.cos(r),c=Math.sin(r);return this.set(n*l,n*c,-n*(l*a+c*o)+a+t,-s*c,s*l,-s*(-c*a+l*o)+o+e,0,0,1),this}scale(t,e){return this.premultiply(kr.makeScale(t,e)),this}rotate(t){return this.premultiply(kr.makeRotation(-t)),this}translate(t,e){return this.premultiply(kr.makeTranslation(t,e)),this}makeTranslation(t,e){return t.isVector2?this.set(1,0,t.x,0,1,t.y,0,0,1):this.set(1,0,t,0,1,e,0,0,1),this}makeRotation(t){const e=Math.cos(t),n=Math.sin(t);return this.set(e,-n,0,n,e,0,0,0,1),this}makeScale(t,e){return this.set(t,0,0,0,e,0,0,0,1),this}equals(t){const e=this.elements,n=t.elements;for(let s=0;s<9;s++)if(e[s]!==n[s])return!1;return!0}fromArray(t,e=0){for(let n=0;n<9;n++)this.elements[n]=t[n+e];return this}toArray(t=[],e=0){const n=this.elements;return t[e]=n[0],t[e+1]=n[1],t[e+2]=n[2],t[e+3]=n[3],t[e+4]=n[4],t[e+5]=n[5],t[e+6]=n[6],t[e+7]=n[7],t[e+8]=n[8],t}clone(){return new this.constructor().fromArray(this.elements)}}const kr=new Ht;function pc(i){for(let t=i.length-1;t>=0;--t)if(i[t]>=65535)return!0;return!1}function br(i){return document.createElementNS("http://www.w3.org/1999/xhtml",i)}function Oh(){const i=br("canvas");return i.style.display="block",i}const Oo={};function Fi(i){i in Oo||(Oo[i]=!0,console.warn(i))}function Bh(i,t,e){return new Promise(function(n,s){function r(){switch(i.clientWaitSync(t,i.SYNC_FLUSH_COMMANDS_BIT,0)){case i.WAIT_FAILED:s();break;case i.TIMEOUT_EXPIRED:setTimeout(r,e);break;default:n()}}setTimeout(r,e)})}function zh(i){const t=i.elements;t[2]=.5*t[2]+.5*t[3],t[6]=.5*t[6]+.5*t[7],t[10]=.5*t[10]+.5*t[11],t[14]=.5*t[14]+.5*t[15]}function kh(i){const t=i.elements;t[11]===-1?(t[10]=-t[10]-1,t[14]=-t[14]):(t[10]=-t[10],t[14]=-t[14]+1)}const Bo=new Ht().set(.4123908,.3575843,.1804808,.212639,.7151687,.0721923,.0193308,.1191948,.9505322),zo=new Ht().set(3.2409699,-1.5373832,-.4986108,-.9692436,1.8759675,.0415551,.0556301,-.203977,1.0569715);function Hh(){const i={enabled:!0,workingColorSpace:Ji,spaces:{},convert:function(s,r,a){return this.enabled===!1||r===a||!r||!a||(this.spaces[r].transfer===se&&(s.r=Dn(s.r),s.g=Dn(s.g),s.b=Dn(s.b)),this.spaces[r].primaries!==this.spaces[a].primaries&&(s.applyMatrix3(this.spaces[r].toXYZ),s.applyMatrix3(this.spaces[a].fromXYZ)),this.spaces[a].transfer===se&&(s.r=Wi(s.r),s.g=Wi(s.g),s.b=Wi(s.b))),s},fromWorkingColorSpace:function(s,r){return this.convert(s,this.workingColorSpace,r)},toWorkingColorSpace:function(s,r){return this.convert(s,r,this.workingColorSpace)},getPrimaries:function(s){return this.spaces[s].primaries},getTransfer:function(s){return s===Vn?yr:this.spaces[s].transfer},getLuminanceCoefficients:function(s,r=this.workingColorSpace){return s.fromArray(this.spaces[r].luminanceCoefficients)},define:function(s){Object.assign(this.spaces,s)},_getMatrix:function(s,r,a){return s.copy(this.spaces[r].toXYZ).multiply(this.spaces[a].fromXYZ)},_getDrawingBufferColorSpace:function(s){return this.spaces[s].outputColorSpaceConfig.drawingBufferColorSpace},_getUnpackColorSpace:function(s=this.workingColorSpace){return this.spaces[s].workingColorSpaceConfig.unpackColorSpace}},t=[.64,.33,.3,.6,.15,.06],e=[.2126,.7152,.0722],n=[.3127,.329];return i.define({[Ji]:{primaries:t,whitePoint:n,transfer:yr,toXYZ:Bo,fromXYZ:zo,luminanceCoefficients:e,workingColorSpaceConfig:{unpackColorSpace:an},outputColorSpaceConfig:{drawingBufferColorSpace:an}},[an]:{primaries:t,whitePoint:n,transfer:se,toXYZ:Bo,fromXYZ:zo,luminanceCoefficients:e,outputColorSpaceConfig:{drawingBufferColorSpace:an}}}),i}const Qt=Hh();function Dn(i){return i<.04045?i*.0773993808:Math.pow(i*.9478672986+.0521327014,2.4)}function Wi(i){return i<.0031308?i*12.92:1.055*Math.pow(i,.41666)-.055}let vi;class Vh{static getDataURL(t){if(/^data:/i.test(t.src)||typeof HTMLCanvasElement>"u")return t.src;let e;if(t instanceof HTMLCanvasElement)e=t;else{vi===void 0&&(vi=br("canvas")),vi.width=t.width,vi.height=t.height;const n=vi.getContext("2d");t instanceof ImageData?n.putImageData(t,0,0):n.drawImage(t,0,0,t.width,t.height),e=vi}return e.width>2048||e.height>2048?(console.warn("THREE.ImageUtils.getDataURL: Image converted to jpg for performance reasons",t),e.toDataURL("image/jpeg",.6)):e.toDataURL("image/png")}static sRGBToLinear(t){if(typeof HTMLImageElement<"u"&&t instanceof HTMLImageElement||typeof HTMLCanvasElement<"u"&&t instanceof HTMLCanvasElement||typeof ImageBitmap<"u"&&t instanceof ImageBitmap){const e=br("canvas");e.width=t.width,e.height=t.height;const n=e.getContext("2d");n.drawImage(t,0,0,t.width,t.height);const s=n.getImageData(0,0,t.width,t.height),r=s.data;for(let a=0;a0&&(n.userData=this.userData),e||(t.textures[this.uuid]=n),n}dispose(){this.dispatchEvent({type:"dispose"})}transformUv(t){if(this.mapping!==nc)return t;if(t.applyMatrix3(this.matrix),t.x<0||t.x>1)switch(this.wrapS){case Ua:t.x=t.x-Math.floor(t.x);break;case ri:t.x=t.x<0?0:1;break;case Ia:Math.abs(Math.floor(t.x)%2)===1?t.x=Math.ceil(t.x)-t.x:t.x=t.x-Math.floor(t.x);break}if(t.y<0||t.y>1)switch(this.wrapT){case Ua:t.y=t.y-Math.floor(t.y);break;case ri:t.y=t.y<0?0:1;break;case Ia:Math.abs(Math.floor(t.y)%2)===1?t.y=Math.ceil(t.y)-t.y:t.y=t.y-Math.floor(t.y);break}return this.flipY&&(t.y=1-t.y),t}set needsUpdate(t){t===!0&&(this.version++,this.source.needsUpdate=!0)}set needsPMREMUpdate(t){t===!0&&this.pmremVersion++}}Pe.DEFAULT_IMAGE=null;Pe.DEFAULT_MAPPING=nc;Pe.DEFAULT_ANISOTROPY=1;class oe{constructor(t=0,e=0,n=0,s=1){oe.prototype.isVector4=!0,this.x=t,this.y=e,this.z=n,this.w=s}get width(){return this.z}set width(t){this.z=t}get height(){return this.w}set height(t){this.w=t}set(t,e,n,s){return this.x=t,this.y=e,this.z=n,this.w=s,this}setScalar(t){return this.x=t,this.y=t,this.z=t,this.w=t,this}setX(t){return this.x=t,this}setY(t){return this.y=t,this}setZ(t){return this.z=t,this}setW(t){return this.w=t,this}setComponent(t,e){switch(t){case 0:this.x=e;break;case 1:this.y=e;break;case 2:this.z=e;break;case 3:this.w=e;break;default:throw new Error("index is out of range: "+t)}return this}getComponent(t){switch(t){case 0:return this.x;case 1:return this.y;case 2:return this.z;case 3:return this.w;default:throw new Error("index is out of range: "+t)}}clone(){return new this.constructor(this.x,this.y,this.z,this.w)}copy(t){return this.x=t.x,this.y=t.y,this.z=t.z,this.w=t.w!==void 0?t.w:1,this}add(t){return this.x+=t.x,this.y+=t.y,this.z+=t.z,this.w+=t.w,this}addScalar(t){return this.x+=t,this.y+=t,this.z+=t,this.w+=t,this}addVectors(t,e){return this.x=t.x+e.x,this.y=t.y+e.y,this.z=t.z+e.z,this.w=t.w+e.w,this}addScaledVector(t,e){return this.x+=t.x*e,this.y+=t.y*e,this.z+=t.z*e,this.w+=t.w*e,this}sub(t){return this.x-=t.x,this.y-=t.y,this.z-=t.z,this.w-=t.w,this}subScalar(t){return this.x-=t,this.y-=t,this.z-=t,this.w-=t,this}subVectors(t,e){return this.x=t.x-e.x,this.y=t.y-e.y,this.z=t.z-e.z,this.w=t.w-e.w,this}multiply(t){return this.x*=t.x,this.y*=t.y,this.z*=t.z,this.w*=t.w,this}multiplyScalar(t){return this.x*=t,this.y*=t,this.z*=t,this.w*=t,this}applyMatrix4(t){const e=this.x,n=this.y,s=this.z,r=this.w,a=t.elements;return this.x=a[0]*e+a[4]*n+a[8]*s+a[12]*r,this.y=a[1]*e+a[5]*n+a[9]*s+a[13]*r,this.z=a[2]*e+a[6]*n+a[10]*s+a[14]*r,this.w=a[3]*e+a[7]*n+a[11]*s+a[15]*r,this}divide(t){return this.x/=t.x,this.y/=t.y,this.z/=t.z,this.w/=t.w,this}divideScalar(t){return this.multiplyScalar(1/t)}setAxisAngleFromQuaternion(t){this.w=2*Math.acos(t.w);const e=Math.sqrt(1-t.w*t.w);return e<1e-4?(this.x=1,this.y=0,this.z=0):(this.x=t.x/e,this.y=t.y/e,this.z=t.z/e),this}setAxisAngleFromRotationMatrix(t){let e,n,s,r;const l=t.elements,c=l[0],h=l[4],d=l[8],p=l[1],u=l[5],g=l[9],_=l[2],m=l[6],f=l[10];if(Math.abs(h-p)<.01&&Math.abs(d-_)<.01&&Math.abs(g-m)<.01){if(Math.abs(h+p)<.1&&Math.abs(d+_)<.1&&Math.abs(g+m)<.1&&Math.abs(c+u+f-3)<.1)return this.set(1,0,0,0),this;e=Math.PI;const E=(c+1)/2,y=(u+1)/2,D=(f+1)/2,w=(h+p)/4,C=(d+_)/4,I=(g+m)/4;return E>y&&E>D?E<.01?(n=0,s=.707106781,r=.707106781):(n=Math.sqrt(E),s=w/n,r=C/n):y>D?y<.01?(n=.707106781,s=0,r=.707106781):(s=Math.sqrt(y),n=w/s,r=I/s):D<.01?(n=.707106781,s=.707106781,r=0):(r=Math.sqrt(D),n=C/r,s=I/r),this.set(n,s,r,e),this}let T=Math.sqrt((m-g)*(m-g)+(d-_)*(d-_)+(p-h)*(p-h));return Math.abs(T)<.001&&(T=1),this.x=(m-g)/T,this.y=(d-_)/T,this.z=(p-h)/T,this.w=Math.acos((c+u+f-1)/2),this}setFromMatrixPosition(t){const e=t.elements;return this.x=e[12],this.y=e[13],this.z=e[14],this.w=e[15],this}min(t){return this.x=Math.min(this.x,t.x),this.y=Math.min(this.y,t.y),this.z=Math.min(this.z,t.z),this.w=Math.min(this.w,t.w),this}max(t){return this.x=Math.max(this.x,t.x),this.y=Math.max(this.y,t.y),this.z=Math.max(this.z,t.z),this.w=Math.max(this.w,t.w),this}clamp(t,e){return this.x=Yt(this.x,t.x,e.x),this.y=Yt(this.y,t.y,e.y),this.z=Yt(this.z,t.z,e.z),this.w=Yt(this.w,t.w,e.w),this}clampScalar(t,e){return this.x=Yt(this.x,t,e),this.y=Yt(this.y,t,e),this.z=Yt(this.z,t,e),this.w=Yt(this.w,t,e),this}clampLength(t,e){const n=this.length();return this.divideScalar(n||1).multiplyScalar(Yt(n,t,e))}floor(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this.z=Math.floor(this.z),this.w=Math.floor(this.w),this}ceil(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this.z=Math.ceil(this.z),this.w=Math.ceil(this.w),this}round(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this.z=Math.round(this.z),this.w=Math.round(this.w),this}roundToZero(){return this.x=Math.trunc(this.x),this.y=Math.trunc(this.y),this.z=Math.trunc(this.z),this.w=Math.trunc(this.w),this}negate(){return this.x=-this.x,this.y=-this.y,this.z=-this.z,this.w=-this.w,this}dot(t){return this.x*t.x+this.y*t.y+this.z*t.z+this.w*t.w}lengthSq(){return this.x*this.x+this.y*this.y+this.z*this.z+this.w*this.w}length(){return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z+this.w*this.w)}manhattanLength(){return Math.abs(this.x)+Math.abs(this.y)+Math.abs(this.z)+Math.abs(this.w)}normalize(){return this.divideScalar(this.length()||1)}setLength(t){return this.normalize().multiplyScalar(t)}lerp(t,e){return this.x+=(t.x-this.x)*e,this.y+=(t.y-this.y)*e,this.z+=(t.z-this.z)*e,this.w+=(t.w-this.w)*e,this}lerpVectors(t,e,n){return this.x=t.x+(e.x-t.x)*n,this.y=t.y+(e.y-t.y)*n,this.z=t.z+(e.z-t.z)*n,this.w=t.w+(e.w-t.w)*n,this}equals(t){return t.x===this.x&&t.y===this.y&&t.z===this.z&&t.w===this.w}fromArray(t,e=0){return this.x=t[e],this.y=t[e+1],this.z=t[e+2],this.w=t[e+3],this}toArray(t=[],e=0){return t[e]=this.x,t[e+1]=this.y,t[e+2]=this.z,t[e+3]=this.w,t}fromBufferAttribute(t,e){return this.x=t.getX(e),this.y=t.getY(e),this.z=t.getZ(e),this.w=t.getW(e),this}random(){return this.x=Math.random(),this.y=Math.random(),this.z=Math.random(),this.w=Math.random(),this}*[Symbol.iterator](){yield this.x,yield this.y,yield this.z,yield this.w}}class Xh extends hi{constructor(t=1,e=1,n={}){super(),this.isRenderTarget=!0,this.width=t,this.height=e,this.depth=1,this.scissor=new oe(0,0,t,e),this.scissorTest=!1,this.viewport=new oe(0,0,t,e);const s={width:t,height:e,depth:1};n=Object.assign({generateMipmaps:!1,internalFormat:null,minFilter:vn,depthBuffer:!0,stencilBuffer:!1,resolveDepthBuffer:!0,resolveStencilBuffer:!0,depthTexture:null,samples:0,count:1},n);const r=new Pe(s,n.mapping,n.wrapS,n.wrapT,n.magFilter,n.minFilter,n.format,n.type,n.anisotropy,n.colorSpace);r.flipY=!1,r.generateMipmaps=n.generateMipmaps,r.internalFormat=n.internalFormat,this.textures=[];const a=n.count;for(let o=0;o=0?1:-1,E=1-f*f;if(E>Number.EPSILON){const D=Math.sqrt(E),w=Math.atan2(D,f*T);m=Math.sin(m*w)/D,o=Math.sin(o*w)/D}const y=o*T;if(l=l*m+p*y,c=c*m+u*y,h=h*m+g*y,d=d*m+_*y,m===1-o){const D=1/Math.sqrt(l*l+c*c+h*h+d*d);l*=D,c*=D,h*=D,d*=D}}t[e]=l,t[e+1]=c,t[e+2]=h,t[e+3]=d}static multiplyQuaternionsFlat(t,e,n,s,r,a){const o=n[s],l=n[s+1],c=n[s+2],h=n[s+3],d=r[a],p=r[a+1],u=r[a+2],g=r[a+3];return t[e]=o*g+h*d+l*u-c*p,t[e+1]=l*g+h*p+c*d-o*u,t[e+2]=c*g+h*u+o*p-l*d,t[e+3]=h*g-o*d-l*p-c*u,t}get x(){return this._x}set x(t){this._x=t,this._onChangeCallback()}get y(){return this._y}set y(t){this._y=t,this._onChangeCallback()}get z(){return this._z}set z(t){this._z=t,this._onChangeCallback()}get w(){return this._w}set w(t){this._w=t,this._onChangeCallback()}set(t,e,n,s){return this._x=t,this._y=e,this._z=n,this._w=s,this._onChangeCallback(),this}clone(){return new this.constructor(this._x,this._y,this._z,this._w)}copy(t){return this._x=t.x,this._y=t.y,this._z=t.z,this._w=t.w,this._onChangeCallback(),this}setFromEuler(t,e=!0){const n=t._x,s=t._y,r=t._z,a=t._order,o=Math.cos,l=Math.sin,c=o(n/2),h=o(s/2),d=o(r/2),p=l(n/2),u=l(s/2),g=l(r/2);switch(a){case"XYZ":this._x=p*h*d+c*u*g,this._y=c*u*d-p*h*g,this._z=c*h*g+p*u*d,this._w=c*h*d-p*u*g;break;case"YXZ":this._x=p*h*d+c*u*g,this._y=c*u*d-p*h*g,this._z=c*h*g-p*u*d,this._w=c*h*d+p*u*g;break;case"ZXY":this._x=p*h*d-c*u*g,this._y=c*u*d+p*h*g,this._z=c*h*g+p*u*d,this._w=c*h*d-p*u*g;break;case"ZYX":this._x=p*h*d-c*u*g,this._y=c*u*d+p*h*g,this._z=c*h*g-p*u*d,this._w=c*h*d+p*u*g;break;case"YZX":this._x=p*h*d+c*u*g,this._y=c*u*d+p*h*g,this._z=c*h*g-p*u*d,this._w=c*h*d-p*u*g;break;case"XZY":this._x=p*h*d-c*u*g,this._y=c*u*d-p*h*g,this._z=c*h*g+p*u*d,this._w=c*h*d+p*u*g;break;default:console.warn("THREE.Quaternion: .setFromEuler() encountered an unknown order: "+a)}return e===!0&&this._onChangeCallback(),this}setFromAxisAngle(t,e){const n=e/2,s=Math.sin(n);return this._x=t.x*s,this._y=t.y*s,this._z=t.z*s,this._w=Math.cos(n),this._onChangeCallback(),this}setFromRotationMatrix(t){const e=t.elements,n=e[0],s=e[4],r=e[8],a=e[1],o=e[5],l=e[9],c=e[2],h=e[6],d=e[10],p=n+o+d;if(p>0){const u=.5/Math.sqrt(p+1);this._w=.25/u,this._x=(h-l)*u,this._y=(r-c)*u,this._z=(a-s)*u}else if(n>o&&n>d){const u=2*Math.sqrt(1+n-o-d);this._w=(h-l)/u,this._x=.25*u,this._y=(s+a)/u,this._z=(r+c)/u}else if(o>d){const u=2*Math.sqrt(1+o-n-d);this._w=(r-c)/u,this._x=(s+a)/u,this._y=.25*u,this._z=(l+h)/u}else{const u=2*Math.sqrt(1+d-n-o);this._w=(a-s)/u,this._x=(r+c)/u,this._y=(l+h)/u,this._z=.25*u}return this._onChangeCallback(),this}setFromUnitVectors(t,e){let n=t.dot(e)+1;return nMath.abs(t.z)?(this._x=-t.y,this._y=t.x,this._z=0,this._w=n):(this._x=0,this._y=-t.z,this._z=t.y,this._w=n)):(this._x=t.y*e.z-t.z*e.y,this._y=t.z*e.x-t.x*e.z,this._z=t.x*e.y-t.y*e.x,this._w=n),this.normalize()}angleTo(t){return 2*Math.acos(Math.abs(Yt(this.dot(t),-1,1)))}rotateTowards(t,e){const n=this.angleTo(t);if(n===0)return this;const s=Math.min(1,e/n);return this.slerp(t,s),this}identity(){return this.set(0,0,0,1)}invert(){return this.conjugate()}conjugate(){return this._x*=-1,this._y*=-1,this._z*=-1,this._onChangeCallback(),this}dot(t){return this._x*t._x+this._y*t._y+this._z*t._z+this._w*t._w}lengthSq(){return this._x*this._x+this._y*this._y+this._z*this._z+this._w*this._w}length(){return Math.sqrt(this._x*this._x+this._y*this._y+this._z*this._z+this._w*this._w)}normalize(){let t=this.length();return t===0?(this._x=0,this._y=0,this._z=0,this._w=1):(t=1/t,this._x=this._x*t,this._y=this._y*t,this._z=this._z*t,this._w=this._w*t),this._onChangeCallback(),this}multiply(t){return this.multiplyQuaternions(this,t)}premultiply(t){return this.multiplyQuaternions(t,this)}multiplyQuaternions(t,e){const n=t._x,s=t._y,r=t._z,a=t._w,o=e._x,l=e._y,c=e._z,h=e._w;return this._x=n*h+a*o+s*c-r*l,this._y=s*h+a*l+r*o-n*c,this._z=r*h+a*c+n*l-s*o,this._w=a*h-n*o-s*l-r*c,this._onChangeCallback(),this}slerp(t,e){if(e===0)return this;if(e===1)return this.copy(t);const n=this._x,s=this._y,r=this._z,a=this._w;let o=a*t._w+n*t._x+s*t._y+r*t._z;if(o<0?(this._w=-t._w,this._x=-t._x,this._y=-t._y,this._z=-t._z,o=-o):this.copy(t),o>=1)return this._w=a,this._x=n,this._y=s,this._z=r,this;const l=1-o*o;if(l<=Number.EPSILON){const u=1-e;return this._w=u*a+e*this._w,this._x=u*n+e*this._x,this._y=u*s+e*this._y,this._z=u*r+e*this._z,this.normalize(),this}const c=Math.sqrt(l),h=Math.atan2(c,o),d=Math.sin((1-e)*h)/c,p=Math.sin(e*h)/c;return this._w=a*d+this._w*p,this._x=n*d+this._x*p,this._y=s*d+this._y*p,this._z=r*d+this._z*p,this._onChangeCallback(),this}slerpQuaternions(t,e,n){return this.copy(t).slerp(e,n)}random(){const t=2*Math.PI*Math.random(),e=2*Math.PI*Math.random(),n=Math.random(),s=Math.sqrt(1-n),r=Math.sqrt(n);return this.set(s*Math.sin(t),s*Math.cos(t),r*Math.sin(e),r*Math.cos(e))}equals(t){return t._x===this._x&&t._y===this._y&&t._z===this._z&&t._w===this._w}fromArray(t,e=0){return this._x=t[e],this._y=t[e+1],this._z=t[e+2],this._w=t[e+3],this._onChangeCallback(),this}toArray(t=[],e=0){return t[e]=this._x,t[e+1]=this._y,t[e+2]=this._z,t[e+3]=this._w,t}fromBufferAttribute(t,e){return this._x=t.getX(e),this._y=t.getY(e),this._z=t.getZ(e),this._w=t.getW(e),this._onChangeCallback(),this}toJSON(){return this.toArray()}_onChange(t){return this._onChangeCallback=t,this}_onChangeCallback(){}*[Symbol.iterator](){yield this._x,yield this._y,yield this._z,yield this._w}}class P{constructor(t=0,e=0,n=0){P.prototype.isVector3=!0,this.x=t,this.y=e,this.z=n}set(t,e,n){return n===void 0&&(n=this.z),this.x=t,this.y=e,this.z=n,this}setScalar(t){return this.x=t,this.y=t,this.z=t,this}setX(t){return this.x=t,this}setY(t){return this.y=t,this}setZ(t){return this.z=t,this}setComponent(t,e){switch(t){case 0:this.x=e;break;case 1:this.y=e;break;case 2:this.z=e;break;default:throw new Error("index is out of range: "+t)}return this}getComponent(t){switch(t){case 0:return this.x;case 1:return this.y;case 2:return this.z;default:throw new Error("index is out of range: "+t)}}clone(){return new this.constructor(this.x,this.y,this.z)}copy(t){return this.x=t.x,this.y=t.y,this.z=t.z,this}add(t){return this.x+=t.x,this.y+=t.y,this.z+=t.z,this}addScalar(t){return this.x+=t,this.y+=t,this.z+=t,this}addVectors(t,e){return this.x=t.x+e.x,this.y=t.y+e.y,this.z=t.z+e.z,this}addScaledVector(t,e){return this.x+=t.x*e,this.y+=t.y*e,this.z+=t.z*e,this}sub(t){return this.x-=t.x,this.y-=t.y,this.z-=t.z,this}subScalar(t){return this.x-=t,this.y-=t,this.z-=t,this}subVectors(t,e){return this.x=t.x-e.x,this.y=t.y-e.y,this.z=t.z-e.z,this}multiply(t){return this.x*=t.x,this.y*=t.y,this.z*=t.z,this}multiplyScalar(t){return this.x*=t,this.y*=t,this.z*=t,this}multiplyVectors(t,e){return this.x=t.x*e.x,this.y=t.y*e.y,this.z=t.z*e.z,this}applyEuler(t){return this.applyQuaternion(ko.setFromEuler(t))}applyAxisAngle(t,e){return this.applyQuaternion(ko.setFromAxisAngle(t,e))}applyMatrix3(t){const e=this.x,n=this.y,s=this.z,r=t.elements;return this.x=r[0]*e+r[3]*n+r[6]*s,this.y=r[1]*e+r[4]*n+r[7]*s,this.z=r[2]*e+r[5]*n+r[8]*s,this}applyNormalMatrix(t){return this.applyMatrix3(t).normalize()}applyMatrix4(t){const e=this.x,n=this.y,s=this.z,r=t.elements,a=1/(r[3]*e+r[7]*n+r[11]*s+r[15]);return this.x=(r[0]*e+r[4]*n+r[8]*s+r[12])*a,this.y=(r[1]*e+r[5]*n+r[9]*s+r[13])*a,this.z=(r[2]*e+r[6]*n+r[10]*s+r[14])*a,this}applyQuaternion(t){const e=this.x,n=this.y,s=this.z,r=t.x,a=t.y,o=t.z,l=t.w,c=2*(a*s-o*n),h=2*(o*e-r*s),d=2*(r*n-a*e);return this.x=e+l*c+a*d-o*h,this.y=n+l*h+o*c-r*d,this.z=s+l*d+r*h-a*c,this}project(t){return this.applyMatrix4(t.matrixWorldInverse).applyMatrix4(t.projectionMatrix)}unproject(t){return this.applyMatrix4(t.projectionMatrixInverse).applyMatrix4(t.matrixWorld)}transformDirection(t){const e=this.x,n=this.y,s=this.z,r=t.elements;return this.x=r[0]*e+r[4]*n+r[8]*s,this.y=r[1]*e+r[5]*n+r[9]*s,this.z=r[2]*e+r[6]*n+r[10]*s,this.normalize()}divide(t){return this.x/=t.x,this.y/=t.y,this.z/=t.z,this}divideScalar(t){return this.multiplyScalar(1/t)}min(t){return this.x=Math.min(this.x,t.x),this.y=Math.min(this.y,t.y),this.z=Math.min(this.z,t.z),this}max(t){return this.x=Math.max(this.x,t.x),this.y=Math.max(this.y,t.y),this.z=Math.max(this.z,t.z),this}clamp(t,e){return this.x=Yt(this.x,t.x,e.x),this.y=Yt(this.y,t.y,e.y),this.z=Yt(this.z,t.z,e.z),this}clampScalar(t,e){return this.x=Yt(this.x,t,e),this.y=Yt(this.y,t,e),this.z=Yt(this.z,t,e),this}clampLength(t,e){const n=this.length();return this.divideScalar(n||1).multiplyScalar(Yt(n,t,e))}floor(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this.z=Math.floor(this.z),this}ceil(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this.z=Math.ceil(this.z),this}round(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this.z=Math.round(this.z),this}roundToZero(){return this.x=Math.trunc(this.x),this.y=Math.trunc(this.y),this.z=Math.trunc(this.z),this}negate(){return this.x=-this.x,this.y=-this.y,this.z=-this.z,this}dot(t){return this.x*t.x+this.y*t.y+this.z*t.z}lengthSq(){return this.x*this.x+this.y*this.y+this.z*this.z}length(){return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z)}manhattanLength(){return Math.abs(this.x)+Math.abs(this.y)+Math.abs(this.z)}normalize(){return this.divideScalar(this.length()||1)}setLength(t){return this.normalize().multiplyScalar(t)}lerp(t,e){return this.x+=(t.x-this.x)*e,this.y+=(t.y-this.y)*e,this.z+=(t.z-this.z)*e,this}lerpVectors(t,e,n){return this.x=t.x+(e.x-t.x)*n,this.y=t.y+(e.y-t.y)*n,this.z=t.z+(e.z-t.z)*n,this}cross(t){return this.crossVectors(this,t)}crossVectors(t,e){const n=t.x,s=t.y,r=t.z,a=e.x,o=e.y,l=e.z;return this.x=s*l-r*o,this.y=r*a-n*l,this.z=n*o-s*a,this}projectOnVector(t){const e=t.lengthSq();if(e===0)return this.set(0,0,0);const n=t.dot(this)/e;return this.copy(t).multiplyScalar(n)}projectOnPlane(t){return Vr.copy(this).projectOnVector(t),this.sub(Vr)}reflect(t){return this.sub(Vr.copy(t).multiplyScalar(2*this.dot(t)))}angleTo(t){const e=Math.sqrt(this.lengthSq()*t.lengthSq());if(e===0)return Math.PI/2;const n=this.dot(t)/e;return Math.acos(Yt(n,-1,1))}distanceTo(t){return Math.sqrt(this.distanceToSquared(t))}distanceToSquared(t){const e=this.x-t.x,n=this.y-t.y,s=this.z-t.z;return e*e+n*n+s*s}manhattanDistanceTo(t){return Math.abs(this.x-t.x)+Math.abs(this.y-t.y)+Math.abs(this.z-t.z)}setFromSpherical(t){return this.setFromSphericalCoords(t.radius,t.phi,t.theta)}setFromSphericalCoords(t,e,n){const s=Math.sin(e)*t;return this.x=s*Math.sin(n),this.y=Math.cos(e)*t,this.z=s*Math.cos(n),this}setFromCylindrical(t){return this.setFromCylindricalCoords(t.radius,t.theta,t.y)}setFromCylindricalCoords(t,e,n){return this.x=t*Math.sin(e),this.y=n,this.z=t*Math.cos(e),this}setFromMatrixPosition(t){const e=t.elements;return this.x=e[12],this.y=e[13],this.z=e[14],this}setFromMatrixScale(t){const e=this.setFromMatrixColumn(t,0).length(),n=this.setFromMatrixColumn(t,1).length(),s=this.setFromMatrixColumn(t,2).length();return this.x=e,this.y=n,this.z=s,this}setFromMatrixColumn(t,e){return this.fromArray(t.elements,e*4)}setFromMatrix3Column(t,e){return this.fromArray(t.elements,e*3)}setFromEuler(t){return this.x=t._x,this.y=t._y,this.z=t._z,this}setFromColor(t){return this.x=t.r,this.y=t.g,this.z=t.b,this}equals(t){return t.x===this.x&&t.y===this.y&&t.z===this.z}fromArray(t,e=0){return this.x=t[e],this.y=t[e+1],this.z=t[e+2],this}toArray(t=[],e=0){return t[e]=this.x,t[e+1]=this.y,t[e+2]=this.z,t}fromBufferAttribute(t,e){return this.x=t.getX(e),this.y=t.getY(e),this.z=t.getZ(e),this}random(){return this.x=Math.random(),this.y=Math.random(),this.z=Math.random(),this}randomDirection(){const t=Math.random()*Math.PI*2,e=Math.random()*2-1,n=Math.sqrt(1-e*e);return this.x=n*Math.cos(t),this.y=e,this.z=n*Math.sin(t),this}*[Symbol.iterator](){yield this.x,yield this.y,yield this.z}}const Vr=new P,ko=new ci;class ui{constructor(t=new P(1/0,1/0,1/0),e=new P(-1/0,-1/0,-1/0)){this.isBox3=!0,this.min=t,this.max=e}set(t,e){return this.min.copy(t),this.max.copy(e),this}setFromArray(t){this.makeEmpty();for(let e=0,n=t.length;e=this.min.x&&t.x<=this.max.x&&t.y>=this.min.y&&t.y<=this.max.y&&t.z>=this.min.z&&t.z<=this.max.z}containsBox(t){return this.min.x<=t.min.x&&t.max.x<=this.max.x&&this.min.y<=t.min.y&&t.max.y<=this.max.y&&this.min.z<=t.min.z&&t.max.z<=this.max.z}getParameter(t,e){return e.set((t.x-this.min.x)/(this.max.x-this.min.x),(t.y-this.min.y)/(this.max.y-this.min.y),(t.z-this.min.z)/(this.max.z-this.min.z))}intersectsBox(t){return t.max.x>=this.min.x&&t.min.x<=this.max.x&&t.max.y>=this.min.y&&t.min.y<=this.max.y&&t.max.z>=this.min.z&&t.min.z<=this.max.z}intersectsSphere(t){return this.clampPoint(t.center,cn),cn.distanceToSquared(t.center)<=t.radius*t.radius}intersectsPlane(t){let e,n;return t.normal.x>0?(e=t.normal.x*this.min.x,n=t.normal.x*this.max.x):(e=t.normal.x*this.max.x,n=t.normal.x*this.min.x),t.normal.y>0?(e+=t.normal.y*this.min.y,n+=t.normal.y*this.max.y):(e+=t.normal.y*this.max.y,n+=t.normal.y*this.min.y),t.normal.z>0?(e+=t.normal.z*this.min.z,n+=t.normal.z*this.max.z):(e+=t.normal.z*this.max.z,n+=t.normal.z*this.min.z),e<=-t.constant&&n>=-t.constant}intersectsTriangle(t){if(this.isEmpty())return!1;this.getCenter(ss),Fs.subVectors(this.max,ss),xi.subVectors(t.a,ss),Mi.subVectors(t.b,ss),Si.subVectors(t.c,ss),Un.subVectors(Mi,xi),In.subVectors(Si,Mi),Zn.subVectors(xi,Si);let e=[0,-Un.z,Un.y,0,-In.z,In.y,0,-Zn.z,Zn.y,Un.z,0,-Un.x,In.z,0,-In.x,Zn.z,0,-Zn.x,-Un.y,Un.x,0,-In.y,In.x,0,-Zn.y,Zn.x,0];return!Gr(e,xi,Mi,Si,Fs)||(e=[1,0,0,0,1,0,0,0,1],!Gr(e,xi,Mi,Si,Fs))?!1:(Os.crossVectors(Un,In),e=[Os.x,Os.y,Os.z],Gr(e,xi,Mi,Si,Fs))}clampPoint(t,e){return e.copy(t).clamp(this.min,this.max)}distanceToPoint(t){return this.clampPoint(t,cn).distanceTo(t)}getBoundingSphere(t){return this.isEmpty()?t.makeEmpty():(this.getCenter(t.center),t.radius=this.getSize(cn).length()*.5),t}intersect(t){return this.min.max(t.min),this.max.min(t.max),this.isEmpty()&&this.makeEmpty(),this}union(t){return this.min.min(t.min),this.max.max(t.max),this}applyMatrix4(t){return this.isEmpty()?this:(Sn[0].set(this.min.x,this.min.y,this.min.z).applyMatrix4(t),Sn[1].set(this.min.x,this.min.y,this.max.z).applyMatrix4(t),Sn[2].set(this.min.x,this.max.y,this.min.z).applyMatrix4(t),Sn[3].set(this.min.x,this.max.y,this.max.z).applyMatrix4(t),Sn[4].set(this.max.x,this.min.y,this.min.z).applyMatrix4(t),Sn[5].set(this.max.x,this.min.y,this.max.z).applyMatrix4(t),Sn[6].set(this.max.x,this.max.y,this.min.z).applyMatrix4(t),Sn[7].set(this.max.x,this.max.y,this.max.z).applyMatrix4(t),this.setFromPoints(Sn),this)}translate(t){return this.min.add(t),this.max.add(t),this}equals(t){return t.min.equals(this.min)&&t.max.equals(this.max)}}const Sn=[new P,new P,new P,new P,new P,new P,new P,new P],cn=new P,Ns=new ui,xi=new P,Mi=new P,Si=new P,Un=new P,In=new P,Zn=new P,ss=new P,Fs=new P,Os=new P,Kn=new P;function Gr(i,t,e,n,s){for(let r=0,a=i.length-3;r<=a;r+=3){Kn.fromArray(i,r);const o=s.x*Math.abs(Kn.x)+s.y*Math.abs(Kn.y)+s.z*Math.abs(Kn.z),l=t.dot(Kn),c=e.dot(Kn),h=n.dot(Kn);if(Math.max(-Math.max(l,c,h),Math.min(l,c,h))>o)return!1}return!0}const qh=new ui,rs=new P,Wr=new P;class di{constructor(t=new P,e=-1){this.isSphere=!0,this.center=t,this.radius=e}set(t,e){return this.center.copy(t),this.radius=e,this}setFromPoints(t,e){const n=this.center;e!==void 0?n.copy(e):qh.setFromPoints(t).getCenter(n);let s=0;for(let r=0,a=t.length;rthis.radius*this.radius&&(e.sub(this.center).normalize(),e.multiplyScalar(this.radius).add(this.center)),e}getBoundingBox(t){return this.isEmpty()?(t.makeEmpty(),t):(t.set(this.center,this.center),t.expandByScalar(this.radius),t)}applyMatrix4(t){return this.center.applyMatrix4(t),this.radius=this.radius*t.getMaxScaleOnAxis(),this}translate(t){return this.center.add(t),this}expandByPoint(t){if(this.isEmpty())return this.center.copy(t),this.radius=0,this;rs.subVectors(t,this.center);const e=rs.lengthSq();if(e>this.radius*this.radius){const n=Math.sqrt(e),s=(n-this.radius)*.5;this.center.addScaledVector(rs,s/n),this.radius+=s}return this}union(t){return t.isEmpty()?this:this.isEmpty()?(this.copy(t),this):(this.center.equals(t.center)===!0?this.radius=Math.max(this.radius,t.radius):(Wr.subVectors(t.center,this.center).setLength(t.radius),this.expandByPoint(rs.copy(t.center).add(Wr)),this.expandByPoint(rs.copy(t.center).sub(Wr))),this)}equals(t){return t.center.equals(this.center)&&t.radius===this.radius}clone(){return new this.constructor().copy(this)}}const yn=new P,Xr=new P,Bs=new P,Nn=new P,Yr=new P,zs=new P,qr=new P;class bs{constructor(t=new P,e=new P(0,0,-1)){this.origin=t,this.direction=e}set(t,e){return this.origin.copy(t),this.direction.copy(e),this}copy(t){return this.origin.copy(t.origin),this.direction.copy(t.direction),this}at(t,e){return e.copy(this.origin).addScaledVector(this.direction,t)}lookAt(t){return this.direction.copy(t).sub(this.origin).normalize(),this}recast(t){return this.origin.copy(this.at(t,yn)),this}closestPointToPoint(t,e){e.subVectors(t,this.origin);const n=e.dot(this.direction);return n<0?e.copy(this.origin):e.copy(this.origin).addScaledVector(this.direction,n)}distanceToPoint(t){return Math.sqrt(this.distanceSqToPoint(t))}distanceSqToPoint(t){const e=yn.subVectors(t,this.origin).dot(this.direction);return e<0?this.origin.distanceToSquared(t):(yn.copy(this.origin).addScaledVector(this.direction,e),yn.distanceToSquared(t))}distanceSqToSegment(t,e,n,s){Xr.copy(t).add(e).multiplyScalar(.5),Bs.copy(e).sub(t).normalize(),Nn.copy(this.origin).sub(Xr);const r=t.distanceTo(e)*.5,a=-this.direction.dot(Bs),o=Nn.dot(this.direction),l=-Nn.dot(Bs),c=Nn.lengthSq(),h=Math.abs(1-a*a);let d,p,u,g;if(h>0)if(d=a*l-o,p=a*o-l,g=r*h,d>=0)if(p>=-g)if(p<=g){const _=1/h;d*=_,p*=_,u=d*(d+a*p+2*o)+p*(a*d+p+2*l)+c}else p=r,d=Math.max(0,-(a*p+o)),u=-d*d+p*(p+2*l)+c;else p=-r,d=Math.max(0,-(a*p+o)),u=-d*d+p*(p+2*l)+c;else p<=-g?(d=Math.max(0,-(-a*r+o)),p=d>0?-r:Math.min(Math.max(-r,-l),r),u=-d*d+p*(p+2*l)+c):p<=g?(d=0,p=Math.min(Math.max(-r,-l),r),u=p*(p+2*l)+c):(d=Math.max(0,-(a*r+o)),p=d>0?r:Math.min(Math.max(-r,-l),r),u=-d*d+p*(p+2*l)+c);else p=a>0?-r:r,d=Math.max(0,-(a*p+o)),u=-d*d+p*(p+2*l)+c;return n&&n.copy(this.origin).addScaledVector(this.direction,d),s&&s.copy(Xr).addScaledVector(Bs,p),u}intersectSphere(t,e){yn.subVectors(t.center,this.origin);const n=yn.dot(this.direction),s=yn.dot(yn)-n*n,r=t.radius*t.radius;if(s>r)return null;const a=Math.sqrt(r-s),o=n-a,l=n+a;return l<0?null:o<0?this.at(l,e):this.at(o,e)}intersectsSphere(t){return this.distanceSqToPoint(t.center)<=t.radius*t.radius}distanceToPlane(t){const e=t.normal.dot(this.direction);if(e===0)return t.distanceToPoint(this.origin)===0?0:null;const n=-(this.origin.dot(t.normal)+t.constant)/e;return n>=0?n:null}intersectPlane(t,e){const n=this.distanceToPlane(t);return n===null?null:this.at(n,e)}intersectsPlane(t){const e=t.distanceToPoint(this.origin);return e===0||t.normal.dot(this.direction)*e<0}intersectBox(t,e){let n,s,r,a,o,l;const c=1/this.direction.x,h=1/this.direction.y,d=1/this.direction.z,p=this.origin;return c>=0?(n=(t.min.x-p.x)*c,s=(t.max.x-p.x)*c):(n=(t.max.x-p.x)*c,s=(t.min.x-p.x)*c),h>=0?(r=(t.min.y-p.y)*h,a=(t.max.y-p.y)*h):(r=(t.max.y-p.y)*h,a=(t.min.y-p.y)*h),n>a||r>s||((r>n||isNaN(n))&&(n=r),(a=0?(o=(t.min.z-p.z)*d,l=(t.max.z-p.z)*d):(o=(t.max.z-p.z)*d,l=(t.min.z-p.z)*d),n>l||o>s)||((o>n||n!==n)&&(n=o),(l=0?n:s,e)}intersectsBox(t){return this.intersectBox(t,yn)!==null}intersectTriangle(t,e,n,s,r){Yr.subVectors(e,t),zs.subVectors(n,t),qr.crossVectors(Yr,zs);let a=this.direction.dot(qr),o;if(a>0){if(s)return null;o=1}else if(a<0)o=-1,a=-a;else return null;Nn.subVectors(this.origin,t);const l=o*this.direction.dot(zs.crossVectors(Nn,zs));if(l<0)return null;const c=o*this.direction.dot(Yr.cross(Nn));if(c<0||l+c>a)return null;const h=-o*Nn.dot(qr);return h<0?null:this.at(h/a,r)}applyMatrix4(t){return this.origin.applyMatrix4(t),this.direction.transformDirection(t),this}equals(t){return t.origin.equals(this.origin)&&t.direction.equals(this.direction)}clone(){return new this.constructor().copy(this)}}class ne{constructor(t,e,n,s,r,a,o,l,c,h,d,p,u,g,_,m){ne.prototype.isMatrix4=!0,this.elements=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],t!==void 0&&this.set(t,e,n,s,r,a,o,l,c,h,d,p,u,g,_,m)}set(t,e,n,s,r,a,o,l,c,h,d,p,u,g,_,m){const f=this.elements;return f[0]=t,f[4]=e,f[8]=n,f[12]=s,f[1]=r,f[5]=a,f[9]=o,f[13]=l,f[2]=c,f[6]=h,f[10]=d,f[14]=p,f[3]=u,f[7]=g,f[11]=_,f[15]=m,this}identity(){return this.set(1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1),this}clone(){return new ne().fromArray(this.elements)}copy(t){const e=this.elements,n=t.elements;return e[0]=n[0],e[1]=n[1],e[2]=n[2],e[3]=n[3],e[4]=n[4],e[5]=n[5],e[6]=n[6],e[7]=n[7],e[8]=n[8],e[9]=n[9],e[10]=n[10],e[11]=n[11],e[12]=n[12],e[13]=n[13],e[14]=n[14],e[15]=n[15],this}copyPosition(t){const e=this.elements,n=t.elements;return e[12]=n[12],e[13]=n[13],e[14]=n[14],this}setFromMatrix3(t){const e=t.elements;return this.set(e[0],e[3],e[6],0,e[1],e[4],e[7],0,e[2],e[5],e[8],0,0,0,0,1),this}extractBasis(t,e,n){return t.setFromMatrixColumn(this,0),e.setFromMatrixColumn(this,1),n.setFromMatrixColumn(this,2),this}makeBasis(t,e,n){return this.set(t.x,e.x,n.x,0,t.y,e.y,n.y,0,t.z,e.z,n.z,0,0,0,0,1),this}extractRotation(t){const e=this.elements,n=t.elements,s=1/yi.setFromMatrixColumn(t,0).length(),r=1/yi.setFromMatrixColumn(t,1).length(),a=1/yi.setFromMatrixColumn(t,2).length();return e[0]=n[0]*s,e[1]=n[1]*s,e[2]=n[2]*s,e[3]=0,e[4]=n[4]*r,e[5]=n[5]*r,e[6]=n[6]*r,e[7]=0,e[8]=n[8]*a,e[9]=n[9]*a,e[10]=n[10]*a,e[11]=0,e[12]=0,e[13]=0,e[14]=0,e[15]=1,this}makeRotationFromEuler(t){const e=this.elements,n=t.x,s=t.y,r=t.z,a=Math.cos(n),o=Math.sin(n),l=Math.cos(s),c=Math.sin(s),h=Math.cos(r),d=Math.sin(r);if(t.order==="XYZ"){const p=a*h,u=a*d,g=o*h,_=o*d;e[0]=l*h,e[4]=-l*d,e[8]=c,e[1]=u+g*c,e[5]=p-_*c,e[9]=-o*l,e[2]=_-p*c,e[6]=g+u*c,e[10]=a*l}else if(t.order==="YXZ"){const p=l*h,u=l*d,g=c*h,_=c*d;e[0]=p+_*o,e[4]=g*o-u,e[8]=a*c,e[1]=a*d,e[5]=a*h,e[9]=-o,e[2]=u*o-g,e[6]=_+p*o,e[10]=a*l}else if(t.order==="ZXY"){const p=l*h,u=l*d,g=c*h,_=c*d;e[0]=p-_*o,e[4]=-a*d,e[8]=g+u*o,e[1]=u+g*o,e[5]=a*h,e[9]=_-p*o,e[2]=-a*c,e[6]=o,e[10]=a*l}else if(t.order==="ZYX"){const p=a*h,u=a*d,g=o*h,_=o*d;e[0]=l*h,e[4]=g*c-u,e[8]=p*c+_,e[1]=l*d,e[5]=_*c+p,e[9]=u*c-g,e[2]=-c,e[6]=o*l,e[10]=a*l}else if(t.order==="YZX"){const p=a*l,u=a*c,g=o*l,_=o*c;e[0]=l*h,e[4]=_-p*d,e[8]=g*d+u,e[1]=d,e[5]=a*h,e[9]=-o*h,e[2]=-c*h,e[6]=u*d+g,e[10]=p-_*d}else if(t.order==="XZY"){const p=a*l,u=a*c,g=o*l,_=o*c;e[0]=l*h,e[4]=-d,e[8]=c*h,e[1]=p*d+_,e[5]=a*h,e[9]=u*d-g,e[2]=g*d-u,e[6]=o*h,e[10]=_*d+p}return e[3]=0,e[7]=0,e[11]=0,e[12]=0,e[13]=0,e[14]=0,e[15]=1,this}makeRotationFromQuaternion(t){return this.compose(jh,t,Zh)}lookAt(t,e,n){const s=this.elements;return je.subVectors(t,e),je.lengthSq()===0&&(je.z=1),je.normalize(),Fn.crossVectors(n,je),Fn.lengthSq()===0&&(Math.abs(n.z)===1?je.x+=1e-4:je.z+=1e-4,je.normalize(),Fn.crossVectors(n,je)),Fn.normalize(),ks.crossVectors(je,Fn),s[0]=Fn.x,s[4]=ks.x,s[8]=je.x,s[1]=Fn.y,s[5]=ks.y,s[9]=je.y,s[2]=Fn.z,s[6]=ks.z,s[10]=je.z,this}multiply(t){return this.multiplyMatrices(this,t)}premultiply(t){return this.multiplyMatrices(t,this)}multiplyMatrices(t,e){const n=t.elements,s=e.elements,r=this.elements,a=n[0],o=n[4],l=n[8],c=n[12],h=n[1],d=n[5],p=n[9],u=n[13],g=n[2],_=n[6],m=n[10],f=n[14],T=n[3],E=n[7],y=n[11],D=n[15],w=s[0],C=s[4],I=s[8],S=s[12],M=s[1],A=s[5],W=s[9],k=s[13],q=s[2],Q=s[6],X=s[10],tt=s[14],H=s[3],st=s[7],gt=s[11],Et=s[15];return r[0]=a*w+o*M+l*q+c*H,r[4]=a*C+o*A+l*Q+c*st,r[8]=a*I+o*W+l*X+c*gt,r[12]=a*S+o*k+l*tt+c*Et,r[1]=h*w+d*M+p*q+u*H,r[5]=h*C+d*A+p*Q+u*st,r[9]=h*I+d*W+p*X+u*gt,r[13]=h*S+d*k+p*tt+u*Et,r[2]=g*w+_*M+m*q+f*H,r[6]=g*C+_*A+m*Q+f*st,r[10]=g*I+_*W+m*X+f*gt,r[14]=g*S+_*k+m*tt+f*Et,r[3]=T*w+E*M+y*q+D*H,r[7]=T*C+E*A+y*Q+D*st,r[11]=T*I+E*W+y*X+D*gt,r[15]=T*S+E*k+y*tt+D*Et,this}multiplyScalar(t){const e=this.elements;return e[0]*=t,e[4]*=t,e[8]*=t,e[12]*=t,e[1]*=t,e[5]*=t,e[9]*=t,e[13]*=t,e[2]*=t,e[6]*=t,e[10]*=t,e[14]*=t,e[3]*=t,e[7]*=t,e[11]*=t,e[15]*=t,this}determinant(){const t=this.elements,e=t[0],n=t[4],s=t[8],r=t[12],a=t[1],o=t[5],l=t[9],c=t[13],h=t[2],d=t[6],p=t[10],u=t[14],g=t[3],_=t[7],m=t[11],f=t[15];return g*(+r*l*d-s*c*d-r*o*p+n*c*p+s*o*u-n*l*u)+_*(+e*l*u-e*c*p+r*a*p-s*a*u+s*c*h-r*l*h)+m*(+e*c*d-e*o*u-r*a*d+n*a*u+r*o*h-n*c*h)+f*(-s*o*h-e*l*d+e*o*p+s*a*d-n*a*p+n*l*h)}transpose(){const t=this.elements;let e;return e=t[1],t[1]=t[4],t[4]=e,e=t[2],t[2]=t[8],t[8]=e,e=t[6],t[6]=t[9],t[9]=e,e=t[3],t[3]=t[12],t[12]=e,e=t[7],t[7]=t[13],t[13]=e,e=t[11],t[11]=t[14],t[14]=e,this}setPosition(t,e,n){const s=this.elements;return t.isVector3?(s[12]=t.x,s[13]=t.y,s[14]=t.z):(s[12]=t,s[13]=e,s[14]=n),this}invert(){const t=this.elements,e=t[0],n=t[1],s=t[2],r=t[3],a=t[4],o=t[5],l=t[6],c=t[7],h=t[8],d=t[9],p=t[10],u=t[11],g=t[12],_=t[13],m=t[14],f=t[15],T=d*m*c-_*p*c+_*l*u-o*m*u-d*l*f+o*p*f,E=g*p*c-h*m*c-g*l*u+a*m*u+h*l*f-a*p*f,y=h*_*c-g*d*c+g*o*u-a*_*u-h*o*f+a*d*f,D=g*d*l-h*_*l-g*o*p+a*_*p+h*o*m-a*d*m,w=e*T+n*E+s*y+r*D;if(w===0)return this.set(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);const C=1/w;return t[0]=T*C,t[1]=(_*p*r-d*m*r-_*s*u+n*m*u+d*s*f-n*p*f)*C,t[2]=(o*m*r-_*l*r+_*s*c-n*m*c-o*s*f+n*l*f)*C,t[3]=(d*l*r-o*p*r-d*s*c+n*p*c+o*s*u-n*l*u)*C,t[4]=E*C,t[5]=(h*m*r-g*p*r+g*s*u-e*m*u-h*s*f+e*p*f)*C,t[6]=(g*l*r-a*m*r-g*s*c+e*m*c+a*s*f-e*l*f)*C,t[7]=(a*p*r-h*l*r+h*s*c-e*p*c-a*s*u+e*l*u)*C,t[8]=y*C,t[9]=(g*d*r-h*_*r-g*n*u+e*_*u+h*n*f-e*d*f)*C,t[10]=(a*_*r-g*o*r+g*n*c-e*_*c-a*n*f+e*o*f)*C,t[11]=(h*o*r-a*d*r-h*n*c+e*d*c+a*n*u-e*o*u)*C,t[12]=D*C,t[13]=(h*_*s-g*d*s+g*n*p-e*_*p-h*n*m+e*d*m)*C,t[14]=(g*o*s-a*_*s-g*n*l+e*_*l+a*n*m-e*o*m)*C,t[15]=(a*d*s-h*o*s+h*n*l-e*d*l-a*n*p+e*o*p)*C,this}scale(t){const e=this.elements,n=t.x,s=t.y,r=t.z;return e[0]*=n,e[4]*=s,e[8]*=r,e[1]*=n,e[5]*=s,e[9]*=r,e[2]*=n,e[6]*=s,e[10]*=r,e[3]*=n,e[7]*=s,e[11]*=r,this}getMaxScaleOnAxis(){const t=this.elements,e=t[0]*t[0]+t[1]*t[1]+t[2]*t[2],n=t[4]*t[4]+t[5]*t[5]+t[6]*t[6],s=t[8]*t[8]+t[9]*t[9]+t[10]*t[10];return Math.sqrt(Math.max(e,n,s))}makeTranslation(t,e,n){return t.isVector3?this.set(1,0,0,t.x,0,1,0,t.y,0,0,1,t.z,0,0,0,1):this.set(1,0,0,t,0,1,0,e,0,0,1,n,0,0,0,1),this}makeRotationX(t){const e=Math.cos(t),n=Math.sin(t);return this.set(1,0,0,0,0,e,-n,0,0,n,e,0,0,0,0,1),this}makeRotationY(t){const e=Math.cos(t),n=Math.sin(t);return this.set(e,0,n,0,0,1,0,0,-n,0,e,0,0,0,0,1),this}makeRotationZ(t){const e=Math.cos(t),n=Math.sin(t);return this.set(e,-n,0,0,n,e,0,0,0,0,1,0,0,0,0,1),this}makeRotationAxis(t,e){const n=Math.cos(e),s=Math.sin(e),r=1-n,a=t.x,o=t.y,l=t.z,c=r*a,h=r*o;return this.set(c*a+n,c*o-s*l,c*l+s*o,0,c*o+s*l,h*o+n,h*l-s*a,0,c*l-s*o,h*l+s*a,r*l*l+n,0,0,0,0,1),this}makeScale(t,e,n){return this.set(t,0,0,0,0,e,0,0,0,0,n,0,0,0,0,1),this}makeShear(t,e,n,s,r,a){return this.set(1,n,r,0,t,1,a,0,e,s,1,0,0,0,0,1),this}compose(t,e,n){const s=this.elements,r=e._x,a=e._y,o=e._z,l=e._w,c=r+r,h=a+a,d=o+o,p=r*c,u=r*h,g=r*d,_=a*h,m=a*d,f=o*d,T=l*c,E=l*h,y=l*d,D=n.x,w=n.y,C=n.z;return s[0]=(1-(_+f))*D,s[1]=(u+y)*D,s[2]=(g-E)*D,s[3]=0,s[4]=(u-y)*w,s[5]=(1-(p+f))*w,s[6]=(m+T)*w,s[7]=0,s[8]=(g+E)*C,s[9]=(m-T)*C,s[10]=(1-(p+_))*C,s[11]=0,s[12]=t.x,s[13]=t.y,s[14]=t.z,s[15]=1,this}decompose(t,e,n){const s=this.elements;let r=yi.set(s[0],s[1],s[2]).length();const a=yi.set(s[4],s[5],s[6]).length(),o=yi.set(s[8],s[9],s[10]).length();this.determinant()<0&&(r=-r),t.x=s[12],t.y=s[13],t.z=s[14],hn.copy(this);const c=1/r,h=1/a,d=1/o;return hn.elements[0]*=c,hn.elements[1]*=c,hn.elements[2]*=c,hn.elements[4]*=h,hn.elements[5]*=h,hn.elements[6]*=h,hn.elements[8]*=d,hn.elements[9]*=d,hn.elements[10]*=d,e.setFromRotationMatrix(hn),n.x=r,n.y=a,n.z=o,this}makePerspective(t,e,n,s,r,a,o=Rn){const l=this.elements,c=2*r/(e-t),h=2*r/(n-s),d=(e+t)/(e-t),p=(n+s)/(n-s);let u,g;if(o===Rn)u=-(a+r)/(a-r),g=-2*a*r/(a-r);else if(o===Er)u=-a/(a-r),g=-a*r/(a-r);else throw new Error("THREE.Matrix4.makePerspective(): Invalid coordinate system: "+o);return l[0]=c,l[4]=0,l[8]=d,l[12]=0,l[1]=0,l[5]=h,l[9]=p,l[13]=0,l[2]=0,l[6]=0,l[10]=u,l[14]=g,l[3]=0,l[7]=0,l[11]=-1,l[15]=0,this}makeOrthographic(t,e,n,s,r,a,o=Rn){const l=this.elements,c=1/(e-t),h=1/(n-s),d=1/(a-r),p=(e+t)*c,u=(n+s)*h;let g,_;if(o===Rn)g=(a+r)*d,_=-2*d;else if(o===Er)g=r*d,_=-1*d;else throw new Error("THREE.Matrix4.makeOrthographic(): Invalid coordinate system: "+o);return l[0]=2*c,l[4]=0,l[8]=0,l[12]=-p,l[1]=0,l[5]=2*h,l[9]=0,l[13]=-u,l[2]=0,l[6]=0,l[10]=_,l[14]=-g,l[3]=0,l[7]=0,l[11]=0,l[15]=1,this}equals(t){const e=this.elements,n=t.elements;for(let s=0;s<16;s++)if(e[s]!==n[s])return!1;return!0}fromArray(t,e=0){for(let n=0;n<16;n++)this.elements[n]=t[n+e];return this}toArray(t=[],e=0){const n=this.elements;return t[e]=n[0],t[e+1]=n[1],t[e+2]=n[2],t[e+3]=n[3],t[e+4]=n[4],t[e+5]=n[5],t[e+6]=n[6],t[e+7]=n[7],t[e+8]=n[8],t[e+9]=n[9],t[e+10]=n[10],t[e+11]=n[11],t[e+12]=n[12],t[e+13]=n[13],t[e+14]=n[14],t[e+15]=n[15],t}}const yi=new P,hn=new ne,jh=new P(0,0,0),Zh=new P(1,1,1),Fn=new P,ks=new P,je=new P,Ho=new ne,Vo=new ci;class Mn{constructor(t=0,e=0,n=0,s=Mn.DEFAULT_ORDER){this.isEuler=!0,this._x=t,this._y=e,this._z=n,this._order=s}get x(){return this._x}set x(t){this._x=t,this._onChangeCallback()}get y(){return this._y}set y(t){this._y=t,this._onChangeCallback()}get z(){return this._z}set z(t){this._z=t,this._onChangeCallback()}get order(){return this._order}set order(t){this._order=t,this._onChangeCallback()}set(t,e,n,s=this._order){return this._x=t,this._y=e,this._z=n,this._order=s,this._onChangeCallback(),this}clone(){return new this.constructor(this._x,this._y,this._z,this._order)}copy(t){return this._x=t._x,this._y=t._y,this._z=t._z,this._order=t._order,this._onChangeCallback(),this}setFromRotationMatrix(t,e=this._order,n=!0){const s=t.elements,r=s[0],a=s[4],o=s[8],l=s[1],c=s[5],h=s[9],d=s[2],p=s[6],u=s[10];switch(e){case"XYZ":this._y=Math.asin(Yt(o,-1,1)),Math.abs(o)<.9999999?(this._x=Math.atan2(-h,u),this._z=Math.atan2(-a,r)):(this._x=Math.atan2(p,c),this._z=0);break;case"YXZ":this._x=Math.asin(-Yt(h,-1,1)),Math.abs(h)<.9999999?(this._y=Math.atan2(o,u),this._z=Math.atan2(l,c)):(this._y=Math.atan2(-d,r),this._z=0);break;case"ZXY":this._x=Math.asin(Yt(p,-1,1)),Math.abs(p)<.9999999?(this._y=Math.atan2(-d,u),this._z=Math.atan2(-a,c)):(this._y=0,this._z=Math.atan2(l,r));break;case"ZYX":this._y=Math.asin(-Yt(d,-1,1)),Math.abs(d)<.9999999?(this._x=Math.atan2(p,u),this._z=Math.atan2(l,r)):(this._x=0,this._z=Math.atan2(-a,c));break;case"YZX":this._z=Math.asin(Yt(l,-1,1)),Math.abs(l)<.9999999?(this._x=Math.atan2(-h,c),this._y=Math.atan2(-d,r)):(this._x=0,this._y=Math.atan2(o,u));break;case"XZY":this._z=Math.asin(-Yt(a,-1,1)),Math.abs(a)<.9999999?(this._x=Math.atan2(p,c),this._y=Math.atan2(o,r)):(this._x=Math.atan2(-h,u),this._y=0);break;default:console.warn("THREE.Euler: .setFromRotationMatrix() encountered an unknown order: "+e)}return this._order=e,n===!0&&this._onChangeCallback(),this}setFromQuaternion(t,e,n){return Ho.makeRotationFromQuaternion(t),this.setFromRotationMatrix(Ho,e,n)}setFromVector3(t,e=this._order){return this.set(t.x,t.y,t.z,e)}reorder(t){return Vo.setFromEuler(this),this.setFromQuaternion(Vo,t)}equals(t){return t._x===this._x&&t._y===this._y&&t._z===this._z&&t._order===this._order}fromArray(t){return this._x=t[0],this._y=t[1],this._z=t[2],t[3]!==void 0&&(this._order=t[3]),this._onChangeCallback(),this}toArray(t=[],e=0){return t[e]=this._x,t[e+1]=this._y,t[e+2]=this._z,t[e+3]=this._order,t}_onChange(t){return this._onChangeCallback=t,this}_onChangeCallback(){}*[Symbol.iterator](){yield this._x,yield this._y,yield this._z,yield this._order}}Mn.DEFAULT_ORDER="XYZ";class wo{constructor(){this.mask=1}set(t){this.mask=(1<>>0}enable(t){this.mask|=1<1){for(let e=0;e1){for(let n=0;n0&&(s.userData=this.userData),s.layers=this.layers.mask,s.matrix=this.matrix.toArray(),s.up=this.up.toArray(),this.matrixAutoUpdate===!1&&(s.matrixAutoUpdate=!1),this.isInstancedMesh&&(s.type="InstancedMesh",s.count=this.count,s.instanceMatrix=this.instanceMatrix.toJSON(),this.instanceColor!==null&&(s.instanceColor=this.instanceColor.toJSON())),this.isBatchedMesh&&(s.type="BatchedMesh",s.perObjectFrustumCulled=this.perObjectFrustumCulled,s.sortObjects=this.sortObjects,s.drawRanges=this._drawRanges,s.reservedRanges=this._reservedRanges,s.visibility=this._visibility,s.active=this._active,s.bounds=this._bounds.map(o=>({boxInitialized:o.boxInitialized,boxMin:o.box.min.toArray(),boxMax:o.box.max.toArray(),sphereInitialized:o.sphereInitialized,sphereRadius:o.sphere.radius,sphereCenter:o.sphere.center.toArray()})),s.maxInstanceCount=this._maxInstanceCount,s.maxVertexCount=this._maxVertexCount,s.maxIndexCount=this._maxIndexCount,s.geometryInitialized=this._geometryInitialized,s.geometryCount=this._geometryCount,s.matricesTexture=this._matricesTexture.toJSON(t),this._colorsTexture!==null&&(s.colorsTexture=this._colorsTexture.toJSON(t)),this.boundingSphere!==null&&(s.boundingSphere={center:s.boundingSphere.center.toArray(),radius:s.boundingSphere.radius}),this.boundingBox!==null&&(s.boundingBox={min:s.boundingBox.min.toArray(),max:s.boundingBox.max.toArray()}));function r(o,l){return o[l.uuid]===void 0&&(o[l.uuid]=l.toJSON(t)),l.uuid}if(this.isScene)this.background&&(this.background.isColor?s.background=this.background.toJSON():this.background.isTexture&&(s.background=this.background.toJSON(t).uuid)),this.environment&&this.environment.isTexture&&this.environment.isRenderTargetTexture!==!0&&(s.environment=this.environment.toJSON(t).uuid);else if(this.isMesh||this.isLine||this.isPoints){s.geometry=r(t.geometries,this.geometry);const o=this.geometry.parameters;if(o!==void 0&&o.shapes!==void 0){const l=o.shapes;if(Array.isArray(l))for(let c=0,h=l.length;c0){s.children=[];for(let o=0;o0){s.animations=[];for(let o=0;o0&&(n.geometries=o),l.length>0&&(n.materials=l),c.length>0&&(n.textures=c),h.length>0&&(n.images=h),d.length>0&&(n.shapes=d),p.length>0&&(n.skeletons=p),u.length>0&&(n.animations=u),g.length>0&&(n.nodes=g)}return n.object=s,n;function a(o){const l=[];for(const c in o){const h=o[c];delete h.metadata,l.push(h)}return l}}clone(t){return new this.constructor().copy(this,t)}copy(t,e=!0){if(this.name=t.name,this.up.copy(t.up),this.position.copy(t.position),this.rotation.order=t.rotation.order,this.quaternion.copy(t.quaternion),this.scale.copy(t.scale),this.matrix.copy(t.matrix),this.matrixWorld.copy(t.matrixWorld),this.matrixAutoUpdate=t.matrixAutoUpdate,this.matrixWorldAutoUpdate=t.matrixWorldAutoUpdate,this.matrixWorldNeedsUpdate=t.matrixWorldNeedsUpdate,this.layers.mask=t.layers.mask,this.visible=t.visible,this.castShadow=t.castShadow,this.receiveShadow=t.receiveShadow,this.frustumCulled=t.frustumCulled,this.renderOrder=t.renderOrder,this.animations=t.animations.slice(),this.userData=JSON.parse(JSON.stringify(t.userData)),e===!0)for(let n=0;n0?s.multiplyScalar(1/Math.sqrt(r)):s.set(0,0,0)}static getBarycoord(t,e,n,s,r){un.subVectors(s,e),bn.subVectors(n,e),Zr.subVectors(t,e);const a=un.dot(un),o=un.dot(bn),l=un.dot(Zr),c=bn.dot(bn),h=bn.dot(Zr),d=a*c-o*o;if(d===0)return r.set(0,0,0),null;const p=1/d,u=(c*l-o*h)*p,g=(a*h-o*l)*p;return r.set(1-u-g,g,u)}static containsPoint(t,e,n,s){return this.getBarycoord(t,e,n,s,Tn)===null?!1:Tn.x>=0&&Tn.y>=0&&Tn.x+Tn.y<=1}static getInterpolation(t,e,n,s,r,a,o,l){return this.getBarycoord(t,e,n,s,Tn)===null?(l.x=0,l.y=0,"z"in l&&(l.z=0),"w"in l&&(l.w=0),null):(l.setScalar(0),l.addScaledVector(r,Tn.x),l.addScaledVector(a,Tn.y),l.addScaledVector(o,Tn.z),l)}static getInterpolatedAttribute(t,e,n,s,r,a){return Qr.setScalar(0),ta.setScalar(0),ea.setScalar(0),Qr.fromBufferAttribute(t,e),ta.fromBufferAttribute(t,n),ea.fromBufferAttribute(t,s),a.setScalar(0),a.addScaledVector(Qr,r.x),a.addScaledVector(ta,r.y),a.addScaledVector(ea,r.z),a}static isFrontFacing(t,e,n,s){return un.subVectors(n,e),bn.subVectors(t,e),un.cross(bn).dot(s)<0}set(t,e,n){return this.a.copy(t),this.b.copy(e),this.c.copy(n),this}setFromPointsAndIndices(t,e,n,s){return this.a.copy(t[e]),this.b.copy(t[n]),this.c.copy(t[s]),this}setFromAttributeAndIndices(t,e,n,s){return this.a.fromBufferAttribute(t,e),this.b.fromBufferAttribute(t,n),this.c.fromBufferAttribute(t,s),this}clone(){return new this.constructor().copy(this)}copy(t){return this.a.copy(t.a),this.b.copy(t.b),this.c.copy(t.c),this}getArea(){return un.subVectors(this.c,this.b),bn.subVectors(this.a,this.b),un.cross(bn).length()*.5}getMidpoint(t){return t.addVectors(this.a,this.b).add(this.c).multiplyScalar(1/3)}getNormal(t){return on.getNormal(this.a,this.b,this.c,t)}getPlane(t){return t.setFromCoplanarPoints(this.a,this.b,this.c)}getBarycoord(t,e){return on.getBarycoord(t,this.a,this.b,this.c,e)}getInterpolation(t,e,n,s,r){return on.getInterpolation(t,this.a,this.b,this.c,e,n,s,r)}containsPoint(t){return on.containsPoint(t,this.a,this.b,this.c)}isFrontFacing(t){return on.isFrontFacing(this.a,this.b,this.c,t)}intersectsBox(t){return t.intersectsTriangle(this)}closestPointToPoint(t,e){const n=this.a,s=this.b,r=this.c;let a,o;Ti.subVectors(s,n),wi.subVectors(r,n),Kr.subVectors(t,n);const l=Ti.dot(Kr),c=wi.dot(Kr);if(l<=0&&c<=0)return e.copy(n);$r.subVectors(t,s);const h=Ti.dot($r),d=wi.dot($r);if(h>=0&&d<=h)return e.copy(s);const p=l*d-h*c;if(p<=0&&l>=0&&h<=0)return a=l/(l-h),e.copy(n).addScaledVector(Ti,a);Jr.subVectors(t,r);const u=Ti.dot(Jr),g=wi.dot(Jr);if(g>=0&&u<=g)return e.copy(r);const _=u*c-l*g;if(_<=0&&c>=0&&g<=0)return o=c/(c-g),e.copy(n).addScaledVector(wi,o);const m=h*g-u*d;if(m<=0&&d-h>=0&&u-g>=0)return jo.subVectors(r,s),o=(d-h)/(d-h+(u-g)),e.copy(s).addScaledVector(jo,o);const f=1/(m+_+p);return a=_*f,o=p*f,e.copy(n).addScaledVector(Ti,a).addScaledVector(wi,o)}equals(t){return t.a.equals(this.a)&&t.b.equals(this.b)&&t.c.equals(this.c)}}const _c={aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074},On={h:0,s:0,l:0},Vs={h:0,s:0,l:0};function na(i,t,e){return e<0&&(e+=1),e>1&&(e-=1),e<1/6?i+(t-i)*6*e:e<1/2?t:e<2/3?i+(t-i)*6*(2/3-e):i}class ot{constructor(t,e,n){return this.isColor=!0,this.r=1,this.g=1,this.b=1,this.set(t,e,n)}set(t,e,n){if(e===void 0&&n===void 0){const s=t;s&&s.isColor?this.copy(s):typeof s=="number"?this.setHex(s):typeof s=="string"&&this.setStyle(s)}else this.setRGB(t,e,n);return this}setScalar(t){return this.r=t,this.g=t,this.b=t,this}setHex(t,e=an){return t=Math.floor(t),this.r=(t>>16&255)/255,this.g=(t>>8&255)/255,this.b=(t&255)/255,Qt.toWorkingColorSpace(this,e),this}setRGB(t,e,n,s=Qt.workingColorSpace){return this.r=t,this.g=e,this.b=n,Qt.toWorkingColorSpace(this,s),this}setHSL(t,e,n,s=Qt.workingColorSpace){if(t=Nh(t,1),e=Yt(e,0,1),n=Yt(n,0,1),e===0)this.r=this.g=this.b=n;else{const r=n<=.5?n*(1+e):n+e-n*e,a=2*n-r;this.r=na(a,r,t+1/3),this.g=na(a,r,t),this.b=na(a,r,t-1/3)}return Qt.toWorkingColorSpace(this,s),this}setStyle(t,e=an){function n(r){r!==void 0&&parseFloat(r)<1&&console.warn("THREE.Color: Alpha component of "+t+" will be ignored.")}let s;if(s=/^(\w+)\(([^\)]*)\)/.exec(t)){let r;const a=s[1],o=s[2];switch(a){case"rgb":case"rgba":if(r=/^\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(o))return n(r[4]),this.setRGB(Math.min(255,parseInt(r[1],10))/255,Math.min(255,parseInt(r[2],10))/255,Math.min(255,parseInt(r[3],10))/255,e);if(r=/^\s*(\d+)\%\s*,\s*(\d+)\%\s*,\s*(\d+)\%\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(o))return n(r[4]),this.setRGB(Math.min(100,parseInt(r[1],10))/100,Math.min(100,parseInt(r[2],10))/100,Math.min(100,parseInt(r[3],10))/100,e);break;case"hsl":case"hsla":if(r=/^\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\%\s*,\s*(\d*\.?\d+)\%\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(o))return n(r[4]),this.setHSL(parseFloat(r[1])/360,parseFloat(r[2])/100,parseFloat(r[3])/100,e);break;default:console.warn("THREE.Color: Unknown color model "+t)}}else if(s=/^\#([A-Fa-f\d]+)$/.exec(t)){const r=s[1],a=r.length;if(a===3)return this.setRGB(parseInt(r.charAt(0),16)/15,parseInt(r.charAt(1),16)/15,parseInt(r.charAt(2),16)/15,e);if(a===6)return this.setHex(parseInt(r,16),e);console.warn("THREE.Color: Invalid hex color "+t)}else if(t&&t.length>0)return this.setColorName(t,e);return this}setColorName(t,e=an){const n=_c[t.toLowerCase()];return n!==void 0?this.setHex(n,e):console.warn("THREE.Color: Unknown color "+t),this}clone(){return new this.constructor(this.r,this.g,this.b)}copy(t){return this.r=t.r,this.g=t.g,this.b=t.b,this}copySRGBToLinear(t){return this.r=Dn(t.r),this.g=Dn(t.g),this.b=Dn(t.b),this}copyLinearToSRGB(t){return this.r=Wi(t.r),this.g=Wi(t.g),this.b=Wi(t.b),this}convertSRGBToLinear(){return this.copySRGBToLinear(this),this}convertLinearToSRGB(){return this.copyLinearToSRGB(this),this}getHex(t=an){return Qt.fromWorkingColorSpace(Ne.copy(this),t),Math.round(Yt(Ne.r*255,0,255))*65536+Math.round(Yt(Ne.g*255,0,255))*256+Math.round(Yt(Ne.b*255,0,255))}getHexString(t=an){return("000000"+this.getHex(t).toString(16)).slice(-6)}getHSL(t,e=Qt.workingColorSpace){Qt.fromWorkingColorSpace(Ne.copy(this),e);const n=Ne.r,s=Ne.g,r=Ne.b,a=Math.max(n,s,r),o=Math.min(n,s,r);let l,c;const h=(o+a)/2;if(o===a)l=0,c=0;else{const d=a-o;switch(c=h<=.5?d/(a+o):d/(2-a-o),a){case n:l=(s-r)/d+(s0!=t>0&&this.version++,this._alphaTest=t}onBeforeRender(){}onBeforeCompile(){}customProgramCacheKey(){return this.onBeforeCompile.toString()}setValues(t){if(t!==void 0)for(const e in t){const n=t[e];if(n===void 0){console.warn(`THREE.Material: parameter '${e}' has value of undefined.`);continue}const s=this[e];if(s===void 0){console.warn(`THREE.Material: '${e}' is not a property of THREE.${this.type}.`);continue}s&&s.isColor?s.set(n):s&&s.isVector3&&n&&n.isVector3?s.copy(n):this[e]=n}}toJSON(t){const e=t===void 0||typeof t=="string";e&&(t={textures:{},images:{}});const n={metadata:{version:4.6,type:"Material",generator:"Material.toJSON"}};n.uuid=this.uuid,n.type=this.type,this.name!==""&&(n.name=this.name),this.color&&this.color.isColor&&(n.color=this.color.getHex()),this.roughness!==void 0&&(n.roughness=this.roughness),this.metalness!==void 0&&(n.metalness=this.metalness),this.sheen!==void 0&&(n.sheen=this.sheen),this.sheenColor&&this.sheenColor.isColor&&(n.sheenColor=this.sheenColor.getHex()),this.sheenRoughness!==void 0&&(n.sheenRoughness=this.sheenRoughness),this.emissive&&this.emissive.isColor&&(n.emissive=this.emissive.getHex()),this.emissiveIntensity!==void 0&&this.emissiveIntensity!==1&&(n.emissiveIntensity=this.emissiveIntensity),this.specular&&this.specular.isColor&&(n.specular=this.specular.getHex()),this.specularIntensity!==void 0&&(n.specularIntensity=this.specularIntensity),this.specularColor&&this.specularColor.isColor&&(n.specularColor=this.specularColor.getHex()),this.shininess!==void 0&&(n.shininess=this.shininess),this.clearcoat!==void 0&&(n.clearcoat=this.clearcoat),this.clearcoatRoughness!==void 0&&(n.clearcoatRoughness=this.clearcoatRoughness),this.clearcoatMap&&this.clearcoatMap.isTexture&&(n.clearcoatMap=this.clearcoatMap.toJSON(t).uuid),this.clearcoatRoughnessMap&&this.clearcoatRoughnessMap.isTexture&&(n.clearcoatRoughnessMap=this.clearcoatRoughnessMap.toJSON(t).uuid),this.clearcoatNormalMap&&this.clearcoatNormalMap.isTexture&&(n.clearcoatNormalMap=this.clearcoatNormalMap.toJSON(t).uuid,n.clearcoatNormalScale=this.clearcoatNormalScale.toArray()),this.dispersion!==void 0&&(n.dispersion=this.dispersion),this.iridescence!==void 0&&(n.iridescence=this.iridescence),this.iridescenceIOR!==void 0&&(n.iridescenceIOR=this.iridescenceIOR),this.iridescenceThicknessRange!==void 0&&(n.iridescenceThicknessRange=this.iridescenceThicknessRange),this.iridescenceMap&&this.iridescenceMap.isTexture&&(n.iridescenceMap=this.iridescenceMap.toJSON(t).uuid),this.iridescenceThicknessMap&&this.iridescenceThicknessMap.isTexture&&(n.iridescenceThicknessMap=this.iridescenceThicknessMap.toJSON(t).uuid),this.anisotropy!==void 0&&(n.anisotropy=this.anisotropy),this.anisotropyRotation!==void 0&&(n.anisotropyRotation=this.anisotropyRotation),this.anisotropyMap&&this.anisotropyMap.isTexture&&(n.anisotropyMap=this.anisotropyMap.toJSON(t).uuid),this.map&&this.map.isTexture&&(n.map=this.map.toJSON(t).uuid),this.matcap&&this.matcap.isTexture&&(n.matcap=this.matcap.toJSON(t).uuid),this.alphaMap&&this.alphaMap.isTexture&&(n.alphaMap=this.alphaMap.toJSON(t).uuid),this.lightMap&&this.lightMap.isTexture&&(n.lightMap=this.lightMap.toJSON(t).uuid,n.lightMapIntensity=this.lightMapIntensity),this.aoMap&&this.aoMap.isTexture&&(n.aoMap=this.aoMap.toJSON(t).uuid,n.aoMapIntensity=this.aoMapIntensity),this.bumpMap&&this.bumpMap.isTexture&&(n.bumpMap=this.bumpMap.toJSON(t).uuid,n.bumpScale=this.bumpScale),this.normalMap&&this.normalMap.isTexture&&(n.normalMap=this.normalMap.toJSON(t).uuid,n.normalMapType=this.normalMapType,n.normalScale=this.normalScale.toArray()),this.displacementMap&&this.displacementMap.isTexture&&(n.displacementMap=this.displacementMap.toJSON(t).uuid,n.displacementScale=this.displacementScale,n.displacementBias=this.displacementBias),this.roughnessMap&&this.roughnessMap.isTexture&&(n.roughnessMap=this.roughnessMap.toJSON(t).uuid),this.metalnessMap&&this.metalnessMap.isTexture&&(n.metalnessMap=this.metalnessMap.toJSON(t).uuid),this.emissiveMap&&this.emissiveMap.isTexture&&(n.emissiveMap=this.emissiveMap.toJSON(t).uuid),this.specularMap&&this.specularMap.isTexture&&(n.specularMap=this.specularMap.toJSON(t).uuid),this.specularIntensityMap&&this.specularIntensityMap.isTexture&&(n.specularIntensityMap=this.specularIntensityMap.toJSON(t).uuid),this.specularColorMap&&this.specularColorMap.isTexture&&(n.specularColorMap=this.specularColorMap.toJSON(t).uuid),this.envMap&&this.envMap.isTexture&&(n.envMap=this.envMap.toJSON(t).uuid,this.combine!==void 0&&(n.combine=this.combine)),this.envMapRotation!==void 0&&(n.envMapRotation=this.envMapRotation.toArray()),this.envMapIntensity!==void 0&&(n.envMapIntensity=this.envMapIntensity),this.reflectivity!==void 0&&(n.reflectivity=this.reflectivity),this.refractionRatio!==void 0&&(n.refractionRatio=this.refractionRatio),this.gradientMap&&this.gradientMap.isTexture&&(n.gradientMap=this.gradientMap.toJSON(t).uuid),this.transmission!==void 0&&(n.transmission=this.transmission),this.transmissionMap&&this.transmissionMap.isTexture&&(n.transmissionMap=this.transmissionMap.toJSON(t).uuid),this.thickness!==void 0&&(n.thickness=this.thickness),this.thicknessMap&&this.thicknessMap.isTexture&&(n.thicknessMap=this.thicknessMap.toJSON(t).uuid),this.attenuationDistance!==void 0&&this.attenuationDistance!==1/0&&(n.attenuationDistance=this.attenuationDistance),this.attenuationColor!==void 0&&(n.attenuationColor=this.attenuationColor.getHex()),this.size!==void 0&&(n.size=this.size),this.shadowSide!==null&&(n.shadowSide=this.shadowSide),this.sizeAttenuation!==void 0&&(n.sizeAttenuation=this.sizeAttenuation),this.blending!==Vi&&(n.blending=this.blending),this.side!==Yn&&(n.side=this.side),this.vertexColors===!0&&(n.vertexColors=!0),this.opacity<1&&(n.opacity=this.opacity),this.transparent===!0&&(n.transparent=!0),this.blendSrc!==ya&&(n.blendSrc=this.blendSrc),this.blendDst!==Ea&&(n.blendDst=this.blendDst),this.blendEquation!==ii&&(n.blendEquation=this.blendEquation),this.blendSrcAlpha!==null&&(n.blendSrcAlpha=this.blendSrcAlpha),this.blendDstAlpha!==null&&(n.blendDstAlpha=this.blendDstAlpha),this.blendEquationAlpha!==null&&(n.blendEquationAlpha=this.blendEquationAlpha),this.blendColor&&this.blendColor.isColor&&(n.blendColor=this.blendColor.getHex()),this.blendAlpha!==0&&(n.blendAlpha=this.blendAlpha),this.depthFunc!==qi&&(n.depthFunc=this.depthFunc),this.depthTest===!1&&(n.depthTest=this.depthTest),this.depthWrite===!1&&(n.depthWrite=this.depthWrite),this.colorWrite===!1&&(n.colorWrite=this.colorWrite),this.stencilWriteMask!==255&&(n.stencilWriteMask=this.stencilWriteMask),this.stencilFunc!==No&&(n.stencilFunc=this.stencilFunc),this.stencilRef!==0&&(n.stencilRef=this.stencilRef),this.stencilFuncMask!==255&&(n.stencilFuncMask=this.stencilFuncMask),this.stencilFail!==_i&&(n.stencilFail=this.stencilFail),this.stencilZFail!==_i&&(n.stencilZFail=this.stencilZFail),this.stencilZPass!==_i&&(n.stencilZPass=this.stencilZPass),this.stencilWrite===!0&&(n.stencilWrite=this.stencilWrite),this.rotation!==void 0&&this.rotation!==0&&(n.rotation=this.rotation),this.polygonOffset===!0&&(n.polygonOffset=!0),this.polygonOffsetFactor!==0&&(n.polygonOffsetFactor=this.polygonOffsetFactor),this.polygonOffsetUnits!==0&&(n.polygonOffsetUnits=this.polygonOffsetUnits),this.linewidth!==void 0&&this.linewidth!==1&&(n.linewidth=this.linewidth),this.dashSize!==void 0&&(n.dashSize=this.dashSize),this.gapSize!==void 0&&(n.gapSize=this.gapSize),this.scale!==void 0&&(n.scale=this.scale),this.dithering===!0&&(n.dithering=!0),this.alphaTest>0&&(n.alphaTest=this.alphaTest),this.alphaHash===!0&&(n.alphaHash=!0),this.alphaToCoverage===!0&&(n.alphaToCoverage=!0),this.premultipliedAlpha===!0&&(n.premultipliedAlpha=!0),this.forceSinglePass===!0&&(n.forceSinglePass=!0),this.wireframe===!0&&(n.wireframe=!0),this.wireframeLinewidth>1&&(n.wireframeLinewidth=this.wireframeLinewidth),this.wireframeLinecap!=="round"&&(n.wireframeLinecap=this.wireframeLinecap),this.wireframeLinejoin!=="round"&&(n.wireframeLinejoin=this.wireframeLinejoin),this.flatShading===!0&&(n.flatShading=!0),this.visible===!1&&(n.visible=!1),this.toneMapped===!1&&(n.toneMapped=!1),this.fog===!1&&(n.fog=!1),Object.keys(this.userData).length>0&&(n.userData=this.userData);function s(r){const a=[];for(const o in r){const l=r[o];delete l.metadata,a.push(l)}return a}if(e){const r=s(t.textures),a=s(t.images);r.length>0&&(n.textures=r),a.length>0&&(n.images=a)}return n}clone(){return new this.constructor().copy(this)}copy(t){this.name=t.name,this.blending=t.blending,this.side=t.side,this.vertexColors=t.vertexColors,this.opacity=t.opacity,this.transparent=t.transparent,this.blendSrc=t.blendSrc,this.blendDst=t.blendDst,this.blendEquation=t.blendEquation,this.blendSrcAlpha=t.blendSrcAlpha,this.blendDstAlpha=t.blendDstAlpha,this.blendEquationAlpha=t.blendEquationAlpha,this.blendColor.copy(t.blendColor),this.blendAlpha=t.blendAlpha,this.depthFunc=t.depthFunc,this.depthTest=t.depthTest,this.depthWrite=t.depthWrite,this.stencilWriteMask=t.stencilWriteMask,this.stencilFunc=t.stencilFunc,this.stencilRef=t.stencilRef,this.stencilFuncMask=t.stencilFuncMask,this.stencilFail=t.stencilFail,this.stencilZFail=t.stencilZFail,this.stencilZPass=t.stencilZPass,this.stencilWrite=t.stencilWrite;const e=t.clippingPlanes;let n=null;if(e!==null){const s=e.length;n=new Array(s);for(let r=0;r!==s;++r)n[r]=e[r].clone()}return this.clippingPlanes=n,this.clipIntersection=t.clipIntersection,this.clipShadows=t.clipShadows,this.shadowSide=t.shadowSide,this.colorWrite=t.colorWrite,this.precision=t.precision,this.polygonOffset=t.polygonOffset,this.polygonOffsetFactor=t.polygonOffsetFactor,this.polygonOffsetUnits=t.polygonOffsetUnits,this.dithering=t.dithering,this.alphaTest=t.alphaTest,this.alphaHash=t.alphaHash,this.alphaToCoverage=t.alphaToCoverage,this.premultipliedAlpha=t.premultipliedAlpha,this.forceSinglePass=t.forceSinglePass,this.visible=t.visible,this.toneMapped=t.toneMapped,this.userData=JSON.parse(JSON.stringify(t.userData)),this}dispose(){this.dispatchEvent({type:"dispose"})}set needsUpdate(t){t===!0&&this.version++}onBuild(){console.warn("Material: onBuild() has been removed.")}}class Ss extends qn{constructor(t){super(),this.isMeshBasicMaterial=!0,this.type="MeshBasicMaterial",this.color=new ot(16777215),this.map=null,this.lightMap=null,this.lightMapIntensity=1,this.aoMap=null,this.aoMapIntensity=1,this.specularMap=null,this.alphaMap=null,this.envMap=null,this.envMapRotation=new Mn,this.combine=tc,this.reflectivity=1,this.refractionRatio=.98,this.wireframe=!1,this.wireframeLinewidth=1,this.wireframeLinecap="round",this.wireframeLinejoin="round",this.fog=!0,this.setValues(t)}copy(t){return super.copy(t),this.color.copy(t.color),this.map=t.map,this.lightMap=t.lightMap,this.lightMapIntensity=t.lightMapIntensity,this.aoMap=t.aoMap,this.aoMapIntensity=t.aoMapIntensity,this.specularMap=t.specularMap,this.alphaMap=t.alphaMap,this.envMap=t.envMap,this.envMapRotation.copy(t.envMapRotation),this.combine=t.combine,this.reflectivity=t.reflectivity,this.refractionRatio=t.refractionRatio,this.wireframe=t.wireframe,this.wireframeLinewidth=t.wireframeLinewidth,this.wireframeLinecap=t.wireframeLinecap,this.wireframeLinejoin=t.wireframeLinejoin,this.fog=t.fog,this}}const Me=new P,Gs=new St;class he{constructor(t,e,n=!1){if(Array.isArray(t))throw new TypeError("THREE.BufferAttribute: array should be a Typed Array.");this.isBufferAttribute=!0,this.name="",this.array=t,this.itemSize=e,this.count=t!==void 0?t.length/e:0,this.normalized=n,this.usage=oo,this.updateRanges=[],this.gpuType=xn,this.version=0}onUploadCallback(){}set needsUpdate(t){t===!0&&this.version++}setUsage(t){return this.usage=t,this}addUpdateRange(t,e){this.updateRanges.push({start:t,count:e})}clearUpdateRanges(){this.updateRanges.length=0}copy(t){return this.name=t.name,this.array=new t.array.constructor(t.array),this.itemSize=t.itemSize,this.count=t.count,this.normalized=t.normalized,this.usage=t.usage,this.gpuType=t.gpuType,this}copyAt(t,e,n){t*=this.itemSize,n*=e.itemSize;for(let s=0,r=this.itemSize;se.count&&console.warn("THREE.BufferGeometry: Buffer size too small for points data. Use .dispose() and create a new geometry."),e.needsUpdate=!0}return this}computeBoundingBox(){this.boundingBox===null&&(this.boundingBox=new ui);const t=this.attributes.position,e=this.morphAttributes.position;if(t&&t.isGLBufferAttribute){console.error("THREE.BufferGeometry.computeBoundingBox(): GLBufferAttribute requires a manual bounding box.",this),this.boundingBox.set(new P(-1/0,-1/0,-1/0),new P(1/0,1/0,1/0));return}if(t!==void 0){if(this.boundingBox.setFromBufferAttribute(t),e)for(let n=0,s=e.length;n0&&(t.userData=this.userData),this.parameters!==void 0){const l=this.parameters;for(const c in l)l[c]!==void 0&&(t[c]=l[c]);return t}t.data={attributes:{}};const e=this.index;e!==null&&(t.data.index={type:e.array.constructor.name,array:Array.prototype.slice.call(e.array)});const n=this.attributes;for(const l in n){const c=n[l];t.data.attributes[l]=c.toJSON(t.data)}const s={};let r=!1;for(const l in this.morphAttributes){const c=this.morphAttributes[l],h=[];for(let d=0,p=c.length;d0&&(s[l]=h,r=!0)}r&&(t.data.morphAttributes=s,t.data.morphTargetsRelative=this.morphTargetsRelative);const a=this.groups;a.length>0&&(t.data.groups=JSON.parse(JSON.stringify(a)));const o=this.boundingSphere;return o!==null&&(t.data.boundingSphere={center:o.center.toArray(),radius:o.radius}),t}clone(){return new this.constructor().copy(this)}copy(t){this.index=null,this.attributes={},this.morphAttributes={},this.groups=[],this.boundingBox=null,this.boundingSphere=null;const e={};this.name=t.name;const n=t.index;n!==null&&this.setIndex(n.clone(e));const s=t.attributes;for(const c in s){const h=s[c];this.setAttribute(c,h.clone(e))}const r=t.morphAttributes;for(const c in r){const h=[],d=r[c];for(let p=0,u=d.length;p0){const s=e[n[0]];if(s!==void 0){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let r=0,a=s.length;r(t.far-t.near)**2))&&(Zo.copy(r).invert(),$n.copy(t.ray).applyMatrix4(Zo),!(n.boundingBox!==null&&$n.intersectsBox(n.boundingBox)===!1)&&this._computeIntersections(t,e,$n)))}_computeIntersections(t,e,n){let s;const r=this.geometry,a=this.material,o=r.index,l=r.attributes.position,c=r.attributes.uv,h=r.attributes.uv1,d=r.attributes.normal,p=r.groups,u=r.drawRange;if(o!==null)if(Array.isArray(a))for(let g=0,_=p.length;g<_;g++){const m=p[g],f=a[m.materialIndex],T=Math.max(m.start,u.start),E=Math.min(o.count,Math.min(m.start+m.count,u.start+u.count));for(let y=T,D=E;ye.far?null:{distance:c,point:Zs.clone(),object:i}}function Ks(i,t,e,n,s,r,a,o,l,c){i.getVertexPosition(o,Xs),i.getVertexPosition(l,Ys),i.getVertexPosition(c,qs);const h=nu(i,t,e,n,Xs,Ys,qs,$o);if(h){const d=new P;on.getBarycoord($o,Xs,Ys,qs,d),s&&(h.uv=on.getInterpolatedAttribute(s,o,l,c,d,new St)),r&&(h.uv1=on.getInterpolatedAttribute(r,o,l,c,d,new St)),a&&(h.normal=on.getInterpolatedAttribute(a,o,l,c,d,new P),h.normal.dot(n.direction)>0&&h.normal.multiplyScalar(-1));const p={a:o,b:l,c,normal:new P,materialIndex:0};on.getNormal(Xs,Ys,qs,p.normal),h.face=p,h.barycoord=d}return h}class Ts extends ge{constructor(t=1,e=1,n=1,s=1,r=1,a=1){super(),this.type="BoxGeometry",this.parameters={width:t,height:e,depth:n,widthSegments:s,heightSegments:r,depthSegments:a};const o=this;s=Math.floor(s),r=Math.floor(r),a=Math.floor(a);const l=[],c=[],h=[],d=[];let p=0,u=0;g("z","y","x",-1,-1,n,e,t,a,r,0),g("z","y","x",1,-1,n,e,-t,a,r,1),g("x","z","y",1,1,t,n,e,s,a,2),g("x","z","y",1,-1,t,n,-e,s,a,3),g("x","y","z",1,-1,t,e,n,s,r,4),g("x","y","z",-1,-1,t,e,-n,s,r,5),this.setIndex(l),this.setAttribute("position",new Oe(c,3)),this.setAttribute("normal",new Oe(h,3)),this.setAttribute("uv",new Oe(d,2));function g(_,m,f,T,E,y,D,w,C,I,S){const M=y/C,A=D/I,W=y/2,k=D/2,q=w/2,Q=C+1,X=I+1;let tt=0,H=0;const st=new P;for(let gt=0;gt0?1:-1,h.push(st.x,st.y,st.z),d.push(Ft/C),d.push(1-gt/I),tt+=1}}for(let gt=0;gt0&&(e.defines=this.defines),e.vertexShader=this.vertexShader,e.fragmentShader=this.fragmentShader,e.lights=this.lights,e.clipping=this.clipping;const n={};for(const s in this.extensions)this.extensions[s]===!0&&(n[s]=!0);return Object.keys(n).length>0&&(e.extensions=n),e}}class Sc extends Ue{constructor(){super(),this.isCamera=!0,this.type="Camera",this.matrixWorldInverse=new ne,this.projectionMatrix=new ne,this.projectionMatrixInverse=new ne,this.coordinateSystem=Rn}copy(t,e){return super.copy(t,e),this.matrixWorldInverse.copy(t.matrixWorldInverse),this.projectionMatrix.copy(t.projectionMatrix),this.projectionMatrixInverse.copy(t.projectionMatrixInverse),this.coordinateSystem=t.coordinateSystem,this}getWorldDirection(t){return super.getWorldDirection(t).negate()}updateMatrixWorld(t){super.updateMatrixWorld(t),this.matrixWorldInverse.copy(this.matrixWorld).invert()}updateWorldMatrix(t,e){super.updateWorldMatrix(t,e),this.matrixWorldInverse.copy(this.matrixWorld).invert()}clone(){return new this.constructor().copy(this)}}const Bn=new P,Jo=new St,Qo=new St;class $e extends Sc{constructor(t=50,e=1,n=.1,s=2e3){super(),this.isPerspectiveCamera=!0,this.type="PerspectiveCamera",this.fov=t,this.zoom=1,this.near=n,this.far=s,this.focus=10,this.aspect=e,this.view=null,this.filmGauge=35,this.filmOffset=0,this.updateProjectionMatrix()}copy(t,e){return super.copy(t,e),this.fov=t.fov,this.zoom=t.zoom,this.near=t.near,this.far=t.far,this.focus=t.focus,this.aspect=t.aspect,this.view=t.view===null?null:Object.assign({},t.view),this.filmGauge=t.filmGauge,this.filmOffset=t.filmOffset,this}setFocalLength(t){const e=.5*this.getFilmHeight()/t;this.fov=lo*2*Math.atan(e),this.updateProjectionMatrix()}getFocalLength(){const t=Math.tan(gr*.5*this.fov);return .5*this.getFilmHeight()/t}getEffectiveFOV(){return lo*2*Math.atan(Math.tan(gr*.5*this.fov)/this.zoom)}getFilmWidth(){return this.filmGauge*Math.min(this.aspect,1)}getFilmHeight(){return this.filmGauge/Math.max(this.aspect,1)}getViewBounds(t,e,n){Bn.set(-1,-1,.5).applyMatrix4(this.projectionMatrixInverse),e.set(Bn.x,Bn.y).multiplyScalar(-t/Bn.z),Bn.set(1,1,.5).applyMatrix4(this.projectionMatrixInverse),n.set(Bn.x,Bn.y).multiplyScalar(-t/Bn.z)}getViewSize(t,e){return this.getViewBounds(t,Jo,Qo),e.subVectors(Qo,Jo)}setViewOffset(t,e,n,s,r,a){this.aspect=t/e,this.view===null&&(this.view={enabled:!0,fullWidth:1,fullHeight:1,offsetX:0,offsetY:0,width:1,height:1}),this.view.enabled=!0,this.view.fullWidth=t,this.view.fullHeight=e,this.view.offsetX=n,this.view.offsetY=s,this.view.width=r,this.view.height=a,this.updateProjectionMatrix()}clearViewOffset(){this.view!==null&&(this.view.enabled=!1),this.updateProjectionMatrix()}updateProjectionMatrix(){const t=this.near;let e=t*Math.tan(gr*.5*this.fov)/this.zoom,n=2*e,s=this.aspect*n,r=-.5*s;const a=this.view;if(this.view!==null&&this.view.enabled){const l=a.fullWidth,c=a.fullHeight;r+=a.offsetX*s/l,e-=a.offsetY*n/c,s*=a.width/l,n*=a.height/c}const o=this.filmOffset;o!==0&&(r+=t*o/this.getFilmWidth()),this.projectionMatrix.makePerspective(r,r+s,e,e-n,t,this.far,this.coordinateSystem),this.projectionMatrixInverse.copy(this.projectionMatrix).invert()}toJSON(t){const e=super.toJSON(t);return e.object.fov=this.fov,e.object.zoom=this.zoom,e.object.near=this.near,e.object.far=this.far,e.object.focus=this.focus,e.object.aspect=this.aspect,this.view!==null&&(e.object.view=Object.assign({},this.view)),e.object.filmGauge=this.filmGauge,e.object.filmOffset=this.filmOffset,e}}const Ri=-90,Ci=1;class au extends Ue{constructor(t,e,n){super(),this.type="CubeCamera",this.renderTarget=n,this.coordinateSystem=null,this.activeMipmapLevel=0;const s=new $e(Ri,Ci,t,e);s.layers=this.layers,this.add(s);const r=new $e(Ri,Ci,t,e);r.layers=this.layers,this.add(r);const a=new $e(Ri,Ci,t,e);a.layers=this.layers,this.add(a);const o=new $e(Ri,Ci,t,e);o.layers=this.layers,this.add(o);const l=new $e(Ri,Ci,t,e);l.layers=this.layers,this.add(l);const c=new $e(Ri,Ci,t,e);c.layers=this.layers,this.add(c)}updateCoordinateSystem(){const t=this.coordinateSystem,e=this.children.concat(),[n,s,r,a,o,l]=e;for(const c of e)this.remove(c);if(t===Rn)n.up.set(0,1,0),n.lookAt(1,0,0),s.up.set(0,1,0),s.lookAt(-1,0,0),r.up.set(0,0,-1),r.lookAt(0,1,0),a.up.set(0,0,1),a.lookAt(0,-1,0),o.up.set(0,1,0),o.lookAt(0,0,1),l.up.set(0,1,0),l.lookAt(0,0,-1);else if(t===Er)n.up.set(0,-1,0),n.lookAt(-1,0,0),s.up.set(0,-1,0),s.lookAt(1,0,0),r.up.set(0,0,1),r.lookAt(0,1,0),a.up.set(0,0,-1),a.lookAt(0,-1,0),o.up.set(0,-1,0),o.lookAt(0,0,1),l.up.set(0,-1,0),l.lookAt(0,0,-1);else throw new Error("THREE.CubeCamera.updateCoordinateSystem(): Invalid coordinate system: "+t);for(const c of e)this.add(c),c.updateMatrixWorld()}update(t,e){this.parent===null&&this.updateMatrixWorld();const{renderTarget:n,activeMipmapLevel:s}=this;this.coordinateSystem!==t.coordinateSystem&&(this.coordinateSystem=t.coordinateSystem,this.updateCoordinateSystem());const[r,a,o,l,c,h]=this.children,d=t.getRenderTarget(),p=t.getActiveCubeFace(),u=t.getActiveMipmapLevel(),g=t.xr.enabled;t.xr.enabled=!1;const _=n.texture.generateMipmaps;n.texture.generateMipmaps=!1,t.setRenderTarget(n,0,s),t.render(e,r),t.setRenderTarget(n,1,s),t.render(e,a),t.setRenderTarget(n,2,s),t.render(e,o),t.setRenderTarget(n,3,s),t.render(e,l),t.setRenderTarget(n,4,s),t.render(e,c),n.texture.generateMipmaps=_,t.setRenderTarget(n,5,s),t.render(e,h),t.setRenderTarget(d,p,u),t.xr.enabled=g,n.texture.needsPMREMUpdate=!0}}class yc extends Pe{constructor(t,e,n,s,r,a,o,l,c,h){t=t!==void 0?t:[],e=e!==void 0?e:ji,super(t,e,n,s,r,a,o,l,c,h),this.isCubeTexture=!0,this.flipY=!1}get images(){return this.image}set images(t){this.image=t}}class ou extends fn{constructor(t=1,e={}){super(t,t,e),this.isWebGLCubeRenderTarget=!0;const n={width:t,height:t,depth:1},s=[n,n,n,n,n,n];this.texture=new yc(s,e.mapping,e.wrapS,e.wrapT,e.magFilter,e.minFilter,e.format,e.type,e.anisotropy,e.colorSpace),this.texture.isRenderTargetTexture=!0,this.texture.generateMipmaps=e.generateMipmaps!==void 0?e.generateMipmaps:!1,this.texture.minFilter=e.minFilter!==void 0?e.minFilter:vn}fromEquirectangularTexture(t,e){this.texture.type=e.type,this.texture.colorSpace=e.colorSpace,this.texture.generateMipmaps=e.generateMipmaps,this.texture.minFilter=e.minFilter,this.texture.magFilter=e.magFilter;const n={uniforms:{tEquirect:{value:null}},vertexShader:` + + varying vec3 vWorldDirection; + + vec3 transformDirection( in vec3 dir, in mat4 matrix ) { + + return normalize( ( matrix * vec4( dir, 0.0 ) ).xyz ); + + } + + void main() { + + vWorldDirection = transformDirection( position, modelMatrix ); + + #include + #include + + } + `,fragmentShader:` + + uniform sampler2D tEquirect; + + varying vec3 vWorldDirection; + + #include + + void main() { + + vec3 direction = normalize( vWorldDirection ); + + vec2 sampleUV = equirectUv( direction ); + + gl_FragColor = texture2D( tEquirect, sampleUV ); + + } + `},s=new Ts(5,5,5),r=new He({name:"CubemapFromEquirect",uniforms:Qi(n.uniforms),vertexShader:n.vertexShader,fragmentShader:n.fragmentShader,side:Xe,blending:Cn});r.uniforms.tEquirect.value=e;const a=new be(s,r),o=e.minFilter;return e.minFilter===ai&&(e.minFilter=vn),new au(1,10,this).update(t,a),e.minFilter=o,a.geometry.dispose(),a.material.dispose(),this}clear(t,e,n,s){const r=t.getRenderTarget();for(let a=0;a<6;a++)t.setRenderTarget(this,a),t.clear(e,n,s);t.setRenderTarget(r)}}class Dr{constructor(t,e=25e-5){this.isFogExp2=!0,this.name="",this.color=new ot(t),this.density=e}clone(){return new Dr(this.color,this.density)}toJSON(){return{type:"FogExp2",name:this.name,color:this.color.getHex(),density:this.density}}}class lu extends Ue{constructor(){super(),this.isScene=!0,this.type="Scene",this.background=null,this.environment=null,this.fog=null,this.backgroundBlurriness=0,this.backgroundIntensity=1,this.backgroundRotation=new Mn,this.environmentIntensity=1,this.environmentRotation=new Mn,this.overrideMaterial=null,typeof __THREE_DEVTOOLS__<"u"&&__THREE_DEVTOOLS__.dispatchEvent(new CustomEvent("observe",{detail:this}))}copy(t,e){return super.copy(t,e),t.background!==null&&(this.background=t.background.clone()),t.environment!==null&&(this.environment=t.environment.clone()),t.fog!==null&&(this.fog=t.fog.clone()),this.backgroundBlurriness=t.backgroundBlurriness,this.backgroundIntensity=t.backgroundIntensity,this.backgroundRotation.copy(t.backgroundRotation),this.environmentIntensity=t.environmentIntensity,this.environmentRotation.copy(t.environmentRotation),t.overrideMaterial!==null&&(this.overrideMaterial=t.overrideMaterial.clone()),this.matrixAutoUpdate=t.matrixAutoUpdate,this}toJSON(t){const e=super.toJSON(t);return this.fog!==null&&(e.object.fog=this.fog.toJSON()),this.backgroundBlurriness>0&&(e.object.backgroundBlurriness=this.backgroundBlurriness),this.backgroundIntensity!==1&&(e.object.backgroundIntensity=this.backgroundIntensity),e.object.backgroundRotation=this.backgroundRotation.toArray(),this.environmentIntensity!==1&&(e.object.environmentIntensity=this.environmentIntensity),e.object.environmentRotation=this.environmentRotation.toArray(),e}}class cu{constructor(t,e){this.isInterleavedBuffer=!0,this.array=t,this.stride=e,this.count=t!==void 0?t.length/e:0,this.usage=oo,this.updateRanges=[],this.version=0,this.uuid=Xn()}onUploadCallback(){}set needsUpdate(t){t===!0&&this.version++}setUsage(t){return this.usage=t,this}addUpdateRange(t,e){this.updateRanges.push({start:t,count:e})}clearUpdateRanges(){this.updateRanges.length=0}copy(t){return this.array=new t.array.constructor(t.array),this.count=t.count,this.stride=t.stride,this.usage=t.usage,this}copyAt(t,e,n){t*=this.stride,n*=e.stride;for(let s=0,r=this.stride;st.far||e.push({distance:l,point:ls.clone(),uv:on.getInterpolation(ls,$s,hs,Js,tl,ra,el,new St),face:null,object:this})}copy(t,e){return super.copy(t,e),t.center!==void 0&&this.center.copy(t.center),this.material=t.material,this}}function Qs(i,t,e,n,s,r){Ui.subVectors(i,e).addScalar(.5).multiply(n),s!==void 0?(cs.x=r*Ui.x-s*Ui.y,cs.y=s*Ui.x+r*Ui.y):cs.copy(Ui),i.copy(t),i.x+=cs.x,i.y+=cs.y,i.applyMatrix4(Ec)}class hu extends Pe{constructor(t=null,e=1,n=1,s,r,a,o,l,c=Je,h=Je,d,p){super(null,a,o,l,c,h,s,r,d,p),this.isDataTexture=!0,this.image={data:t,width:e,height:n},this.generateMipmaps=!1,this.flipY=!1,this.unpackAlignment=1}}class nl extends he{constructor(t,e,n,s=1){super(t,e,n),this.isInstancedBufferAttribute=!0,this.meshPerAttribute=s}copy(t){return super.copy(t),this.meshPerAttribute=t.meshPerAttribute,this}toJSON(){const t=super.toJSON();return t.meshPerAttribute=this.meshPerAttribute,t.isInstancedBufferAttribute=!0,t}}const Ii=new ne,il=new ne,tr=[],sl=new ui,uu=new ne,us=new be,ds=new di;class du extends be{constructor(t,e,n){super(t,e),this.isInstancedMesh=!0,this.instanceMatrix=new nl(new Float32Array(n*16),16),this.instanceColor=null,this.morphTexture=null,this.count=n,this.boundingBox=null,this.boundingSphere=null;for(let s=0;s1?null:e.copy(t.start).addScaledVector(n,r)}intersectsLine(t){const e=this.distanceToPoint(t.start),n=this.distanceToPoint(t.end);return e<0&&n>0||n<0&&e>0}intersectsBox(t){return t.intersectsPlane(this)}intersectsSphere(t){return t.intersectsPlane(this)}coplanarPoint(t){return t.copy(this.normal).multiplyScalar(-this.constant)}applyMatrix4(t,e){const n=e||pu.getNormalMatrix(t),s=this.coplanarPoint(aa).applyMatrix4(t),r=this.normal.applyMatrix3(n).normalize();return this.constant=-s.dot(r),this}translate(t){return this.constant-=t.dot(this.normal),this}equals(t){return t.normal.equals(this.normal)&&t.constant===this.constant}clone(){return new this.constructor().copy(this)}}const Jn=new di,er=new P;class Ao{constructor(t=new Hn,e=new Hn,n=new Hn,s=new Hn,r=new Hn,a=new Hn){this.planes=[t,e,n,s,r,a]}set(t,e,n,s,r,a){const o=this.planes;return o[0].copy(t),o[1].copy(e),o[2].copy(n),o[3].copy(s),o[4].copy(r),o[5].copy(a),this}copy(t){const e=this.planes;for(let n=0;n<6;n++)e[n].copy(t.planes[n]);return this}setFromProjectionMatrix(t,e=Rn){const n=this.planes,s=t.elements,r=s[0],a=s[1],o=s[2],l=s[3],c=s[4],h=s[5],d=s[6],p=s[7],u=s[8],g=s[9],_=s[10],m=s[11],f=s[12],T=s[13],E=s[14],y=s[15];if(n[0].setComponents(l-r,p-c,m-u,y-f).normalize(),n[1].setComponents(l+r,p+c,m+u,y+f).normalize(),n[2].setComponents(l+a,p+h,m+g,y+T).normalize(),n[3].setComponents(l-a,p-h,m-g,y-T).normalize(),n[4].setComponents(l-o,p-d,m-_,y-E).normalize(),e===Rn)n[5].setComponents(l+o,p+d,m+_,y+E).normalize();else if(e===Er)n[5].setComponents(o,d,_,E).normalize();else throw new Error("THREE.Frustum.setFromProjectionMatrix(): Invalid coordinate system: "+e);return this}intersectsObject(t){if(t.boundingSphere!==void 0)t.boundingSphere===null&&t.computeBoundingSphere(),Jn.copy(t.boundingSphere).applyMatrix4(t.matrixWorld);else{const e=t.geometry;e.boundingSphere===null&&e.computeBoundingSphere(),Jn.copy(e.boundingSphere).applyMatrix4(t.matrixWorld)}return this.intersectsSphere(Jn)}intersectsSprite(t){return Jn.center.set(0,0,0),Jn.radius=.7071067811865476,Jn.applyMatrix4(t.matrixWorld),this.intersectsSphere(Jn)}intersectsSphere(t){const e=this.planes,n=t.center,s=-t.radius;for(let r=0;r<6;r++)if(e[r].distanceToPoint(n)0?t.max.x:t.min.x,er.y=s.normal.y>0?t.max.y:t.min.y,er.z=s.normal.z>0?t.max.z:t.min.z,s.distanceToPoint(er)<0)return!1}return!0}containsPoint(t){const e=this.planes;for(let n=0;n<6;n++)if(e[n].distanceToPoint(t)<0)return!1;return!0}clone(){return new this.constructor().copy(this)}}class Ar extends qn{constructor(t){super(),this.isLineBasicMaterial=!0,this.type="LineBasicMaterial",this.color=new ot(16777215),this.map=null,this.linewidth=1,this.linecap="round",this.linejoin="round",this.fog=!0,this.setValues(t)}copy(t){return super.copy(t),this.color.copy(t.color),this.map=t.map,this.linewidth=t.linewidth,this.linecap=t.linecap,this.linejoin=t.linejoin,this.fog=t.fog,this}}const Rr=new P,Cr=new P,rl=new ne,fs=new bs,nr=new di,oa=new P,al=new P;class co extends Ue{constructor(t=new ge,e=new Ar){super(),this.isLine=!0,this.type="Line",this.geometry=t,this.material=e,this.updateMorphTargets()}copy(t,e){return super.copy(t,e),this.material=Array.isArray(t.material)?t.material.slice():t.material,this.geometry=t.geometry,this}computeLineDistances(){const t=this.geometry;if(t.index===null){const e=t.attributes.position,n=[0];for(let s=1,r=e.count;s0){const s=e[n[0]];if(s!==void 0){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let r=0,a=s.length;rn)return;oa.applyMatrix4(i.matrixWorld);const l=t.ray.origin.distanceTo(oa);if(!(lt.far))return{distance:l,point:al.clone().applyMatrix4(i.matrixWorld),index:s,face:null,faceIndex:null,barycoord:null,object:i}}class oi extends qn{constructor(t){super(),this.isPointsMaterial=!0,this.type="PointsMaterial",this.color=new ot(16777215),this.map=null,this.alphaMap=null,this.size=1,this.sizeAttenuation=!0,this.fog=!0,this.setValues(t)}copy(t){return super.copy(t),this.color.copy(t.color),this.map=t.map,this.alphaMap=t.alphaMap,this.size=t.size,this.sizeAttenuation=t.sizeAttenuation,this.fog=t.fog,this}}const ol=new ne,ho=new bs,sr=new di,rr=new P;class Yi extends Ue{constructor(t=new ge,e=new oi){super(),this.isPoints=!0,this.type="Points",this.geometry=t,this.material=e,this.updateMorphTargets()}copy(t,e){return super.copy(t,e),this.material=Array.isArray(t.material)?t.material.slice():t.material,this.geometry=t.geometry,this}raycast(t,e){const n=this.geometry,s=this.matrixWorld,r=t.params.Points.threshold,a=n.drawRange;if(n.boundingSphere===null&&n.computeBoundingSphere(),sr.copy(n.boundingSphere),sr.applyMatrix4(s),sr.radius+=r,t.ray.intersectsSphere(sr)===!1)return;ol.copy(s).invert(),ho.copy(t.ray).applyMatrix4(ol);const o=r/((this.scale.x+this.scale.y+this.scale.z)/3),l=o*o,c=n.index,d=n.attributes.position;if(c!==null){const p=Math.max(0,a.start),u=Math.min(c.count,a.start+a.count);for(let g=p,_=u;g<_;g++){const m=c.getX(g);rr.fromBufferAttribute(d,m),ll(rr,m,l,s,t,e,this)}}else{const p=Math.max(0,a.start),u=Math.min(d.count,a.start+a.count);for(let g=p,_=u;g<_;g++)rr.fromBufferAttribute(d,g),ll(rr,g,l,s,t,e,this)}}updateMorphTargets(){const e=this.geometry.morphAttributes,n=Object.keys(e);if(n.length>0){const s=e[n[0]];if(s!==void 0){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let r=0,a=s.length;rs.far)return;r.push({distance:c,distanceToRay:Math.sqrt(o),point:l,index:t,face:null,faceIndex:null,barycoord:null,object:a})}}class zi extends Ue{constructor(){super(),this.isGroup=!0,this.type="Group"}}class bc extends Pe{constructor(t,e,n,s,r,a,o,l,c){super(t,e,n,s,r,a,o,l,c),this.isCanvasTexture=!0,this.needsUpdate=!0}}class Tc extends Pe{constructor(t,e,n,s,r,a,o,l,c,h=Gi){if(h!==Gi&&h!==$i)throw new Error("DepthTexture format must be either THREE.DepthFormat or THREE.DepthStencilFormat");n===void 0&&h===Gi&&(n=li),n===void 0&&h===$i&&(n=Ki),super(null,s,r,a,o,l,h,n,c),this.isDepthTexture=!0,this.image={width:t,height:e},this.magFilter=o!==void 0?o:Je,this.minFilter=l!==void 0?l:Je,this.flipY=!1,this.generateMipmaps=!1,this.compareFunction=null}copy(t){return super.copy(t),this.compareFunction=t.compareFunction,this}toJSON(t){const e=super.toJSON(t);return this.compareFunction!==null&&(e.compareFunction=this.compareFunction),e}}class ws extends ge{constructor(t=1,e=1,n=1,s=1){super(),this.type="PlaneGeometry",this.parameters={width:t,height:e,widthSegments:n,heightSegments:s};const r=t/2,a=e/2,o=Math.floor(n),l=Math.floor(s),c=o+1,h=l+1,d=t/o,p=e/l,u=[],g=[],_=[],m=[];for(let f=0;f0)&&u.push(E,y,w),(f!==n-1||lu.start-g.start);let p=0;for(let u=1;u 0 + vec4 plane; + #ifdef ALPHA_TO_COVERAGE + float distanceToPlane, distanceGradient; + float clipOpacity = 1.0; + #pragma unroll_loop_start + for ( int i = 0; i < UNION_CLIPPING_PLANES; i ++ ) { + plane = clippingPlanes[ i ]; + distanceToPlane = - dot( vClipPosition, plane.xyz ) + plane.w; + distanceGradient = fwidth( distanceToPlane ) / 2.0; + clipOpacity *= smoothstep( - distanceGradient, distanceGradient, distanceToPlane ); + if ( clipOpacity == 0.0 ) discard; + } + #pragma unroll_loop_end + #if UNION_CLIPPING_PLANES < NUM_CLIPPING_PLANES + float unionClipOpacity = 1.0; + #pragma unroll_loop_start + for ( int i = UNION_CLIPPING_PLANES; i < NUM_CLIPPING_PLANES; i ++ ) { + plane = clippingPlanes[ i ]; + distanceToPlane = - dot( vClipPosition, plane.xyz ) + plane.w; + distanceGradient = fwidth( distanceToPlane ) / 2.0; + unionClipOpacity *= 1.0 - smoothstep( - distanceGradient, distanceGradient, distanceToPlane ); + } + #pragma unroll_loop_end + clipOpacity *= 1.0 - unionClipOpacity; + #endif + diffuseColor.a *= clipOpacity; + if ( diffuseColor.a == 0.0 ) discard; + #else + #pragma unroll_loop_start + for ( int i = 0; i < UNION_CLIPPING_PLANES; i ++ ) { + plane = clippingPlanes[ i ]; + if ( dot( vClipPosition, plane.xyz ) > plane.w ) discard; + } + #pragma unroll_loop_end + #if UNION_CLIPPING_PLANES < NUM_CLIPPING_PLANES + bool clipped = true; + #pragma unroll_loop_start + for ( int i = UNION_CLIPPING_PLANES; i < NUM_CLIPPING_PLANES; i ++ ) { + plane = clippingPlanes[ i ]; + clipped = ( dot( vClipPosition, plane.xyz ) > plane.w ) && clipped; + } + #pragma unroll_loop_end + if ( clipped ) discard; + #endif + #endif +#endif`,Gu=`#if NUM_CLIPPING_PLANES > 0 + varying vec3 vClipPosition; + uniform vec4 clippingPlanes[ NUM_CLIPPING_PLANES ]; +#endif`,Wu=`#if NUM_CLIPPING_PLANES > 0 + varying vec3 vClipPosition; +#endif`,Xu=`#if NUM_CLIPPING_PLANES > 0 + vClipPosition = - mvPosition.xyz; +#endif`,Yu=`#if defined( USE_COLOR_ALPHA ) + diffuseColor *= vColor; +#elif defined( USE_COLOR ) + diffuseColor.rgb *= vColor; +#endif`,qu=`#if defined( USE_COLOR_ALPHA ) + varying vec4 vColor; +#elif defined( USE_COLOR ) + varying vec3 vColor; +#endif`,ju=`#if defined( USE_COLOR_ALPHA ) + varying vec4 vColor; +#elif defined( USE_COLOR ) || defined( USE_INSTANCING_COLOR ) || defined( USE_BATCHING_COLOR ) + varying vec3 vColor; +#endif`,Zu=`#if defined( USE_COLOR_ALPHA ) + vColor = vec4( 1.0 ); +#elif defined( USE_COLOR ) || defined( USE_INSTANCING_COLOR ) || defined( USE_BATCHING_COLOR ) + vColor = vec3( 1.0 ); +#endif +#ifdef USE_COLOR + vColor *= color; +#endif +#ifdef USE_INSTANCING_COLOR + vColor.xyz *= instanceColor.xyz; +#endif +#ifdef USE_BATCHING_COLOR + vec3 batchingColor = getBatchingColor( getIndirectIndex( gl_DrawID ) ); + vColor.xyz *= batchingColor.xyz; +#endif`,Ku=`#define PI 3.141592653589793 +#define PI2 6.283185307179586 +#define PI_HALF 1.5707963267948966 +#define RECIPROCAL_PI 0.3183098861837907 +#define RECIPROCAL_PI2 0.15915494309189535 +#define EPSILON 1e-6 +#ifndef saturate +#define saturate( a ) clamp( a, 0.0, 1.0 ) +#endif +#define whiteComplement( a ) ( 1.0 - saturate( a ) ) +float pow2( const in float x ) { return x*x; } +vec3 pow2( const in vec3 x ) { return x*x; } +float pow3( const in float x ) { return x*x*x; } +float pow4( const in float x ) { float x2 = x*x; return x2*x2; } +float max3( const in vec3 v ) { return max( max( v.x, v.y ), v.z ); } +float average( const in vec3 v ) { return dot( v, vec3( 0.3333333 ) ); } +highp float rand( const in vec2 uv ) { + const highp float a = 12.9898, b = 78.233, c = 43758.5453; + highp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI ); + return fract( sin( sn ) * c ); +} +#ifdef HIGH_PRECISION + float precisionSafeLength( vec3 v ) { return length( v ); } +#else + float precisionSafeLength( vec3 v ) { + float maxComponent = max3( abs( v ) ); + return length( v / maxComponent ) * maxComponent; + } +#endif +struct IncidentLight { + vec3 color; + vec3 direction; + bool visible; +}; +struct ReflectedLight { + vec3 directDiffuse; + vec3 directSpecular; + vec3 indirectDiffuse; + vec3 indirectSpecular; +}; +#ifdef USE_ALPHAHASH + varying vec3 vPosition; +#endif +vec3 transformDirection( in vec3 dir, in mat4 matrix ) { + return normalize( ( matrix * vec4( dir, 0.0 ) ).xyz ); +} +vec3 inverseTransformDirection( in vec3 dir, in mat4 matrix ) { + return normalize( ( vec4( dir, 0.0 ) * matrix ).xyz ); +} +mat3 transposeMat3( const in mat3 m ) { + mat3 tmp; + tmp[ 0 ] = vec3( m[ 0 ].x, m[ 1 ].x, m[ 2 ].x ); + tmp[ 1 ] = vec3( m[ 0 ].y, m[ 1 ].y, m[ 2 ].y ); + tmp[ 2 ] = vec3( m[ 0 ].z, m[ 1 ].z, m[ 2 ].z ); + return tmp; +} +bool isPerspectiveMatrix( mat4 m ) { + return m[ 2 ][ 3 ] == - 1.0; +} +vec2 equirectUv( in vec3 dir ) { + float u = atan( dir.z, dir.x ) * RECIPROCAL_PI2 + 0.5; + float v = asin( clamp( dir.y, - 1.0, 1.0 ) ) * RECIPROCAL_PI + 0.5; + return vec2( u, v ); +} +vec3 BRDF_Lambert( const in vec3 diffuseColor ) { + return RECIPROCAL_PI * diffuseColor; +} +vec3 F_Schlick( const in vec3 f0, const in float f90, const in float dotVH ) { + float fresnel = exp2( ( - 5.55473 * dotVH - 6.98316 ) * dotVH ); + return f0 * ( 1.0 - fresnel ) + ( f90 * fresnel ); +} +float F_Schlick( const in float f0, const in float f90, const in float dotVH ) { + float fresnel = exp2( ( - 5.55473 * dotVH - 6.98316 ) * dotVH ); + return f0 * ( 1.0 - fresnel ) + ( f90 * fresnel ); +} // validated`,$u=`#ifdef ENVMAP_TYPE_CUBE_UV + #define cubeUV_minMipLevel 4.0 + #define cubeUV_minTileSize 16.0 + float getFace( vec3 direction ) { + vec3 absDirection = abs( direction ); + float face = - 1.0; + if ( absDirection.x > absDirection.z ) { + if ( absDirection.x > absDirection.y ) + face = direction.x > 0.0 ? 0.0 : 3.0; + else + face = direction.y > 0.0 ? 1.0 : 4.0; + } else { + if ( absDirection.z > absDirection.y ) + face = direction.z > 0.0 ? 2.0 : 5.0; + else + face = direction.y > 0.0 ? 1.0 : 4.0; + } + return face; + } + vec2 getUV( vec3 direction, float face ) { + vec2 uv; + if ( face == 0.0 ) { + uv = vec2( direction.z, direction.y ) / abs( direction.x ); + } else if ( face == 1.0 ) { + uv = vec2( - direction.x, - direction.z ) / abs( direction.y ); + } else if ( face == 2.0 ) { + uv = vec2( - direction.x, direction.y ) / abs( direction.z ); + } else if ( face == 3.0 ) { + uv = vec2( - direction.z, direction.y ) / abs( direction.x ); + } else if ( face == 4.0 ) { + uv = vec2( - direction.x, direction.z ) / abs( direction.y ); + } else { + uv = vec2( direction.x, direction.y ) / abs( direction.z ); + } + return 0.5 * ( uv + 1.0 ); + } + vec3 bilinearCubeUV( sampler2D envMap, vec3 direction, float mipInt ) { + float face = getFace( direction ); + float filterInt = max( cubeUV_minMipLevel - mipInt, 0.0 ); + mipInt = max( mipInt, cubeUV_minMipLevel ); + float faceSize = exp2( mipInt ); + highp vec2 uv = getUV( direction, face ) * ( faceSize - 2.0 ) + 1.0; + if ( face > 2.0 ) { + uv.y += faceSize; + face -= 3.0; + } + uv.x += face * faceSize; + uv.x += filterInt * 3.0 * cubeUV_minTileSize; + uv.y += 4.0 * ( exp2( CUBEUV_MAX_MIP ) - faceSize ); + uv.x *= CUBEUV_TEXEL_WIDTH; + uv.y *= CUBEUV_TEXEL_HEIGHT; + #ifdef texture2DGradEXT + return texture2DGradEXT( envMap, uv, vec2( 0.0 ), vec2( 0.0 ) ).rgb; + #else + return texture2D( envMap, uv ).rgb; + #endif + } + #define cubeUV_r0 1.0 + #define cubeUV_m0 - 2.0 + #define cubeUV_r1 0.8 + #define cubeUV_m1 - 1.0 + #define cubeUV_r4 0.4 + #define cubeUV_m4 2.0 + #define cubeUV_r5 0.305 + #define cubeUV_m5 3.0 + #define cubeUV_r6 0.21 + #define cubeUV_m6 4.0 + float roughnessToMip( float roughness ) { + float mip = 0.0; + if ( roughness >= cubeUV_r1 ) { + mip = ( cubeUV_r0 - roughness ) * ( cubeUV_m1 - cubeUV_m0 ) / ( cubeUV_r0 - cubeUV_r1 ) + cubeUV_m0; + } else if ( roughness >= cubeUV_r4 ) { + mip = ( cubeUV_r1 - roughness ) * ( cubeUV_m4 - cubeUV_m1 ) / ( cubeUV_r1 - cubeUV_r4 ) + cubeUV_m1; + } else if ( roughness >= cubeUV_r5 ) { + mip = ( cubeUV_r4 - roughness ) * ( cubeUV_m5 - cubeUV_m4 ) / ( cubeUV_r4 - cubeUV_r5 ) + cubeUV_m4; + } else if ( roughness >= cubeUV_r6 ) { + mip = ( cubeUV_r5 - roughness ) * ( cubeUV_m6 - cubeUV_m5 ) / ( cubeUV_r5 - cubeUV_r6 ) + cubeUV_m5; + } else { + mip = - 2.0 * log2( 1.16 * roughness ); } + return mip; + } + vec4 textureCubeUV( sampler2D envMap, vec3 sampleDir, float roughness ) { + float mip = clamp( roughnessToMip( roughness ), cubeUV_m0, CUBEUV_MAX_MIP ); + float mipF = fract( mip ); + float mipInt = floor( mip ); + vec3 color0 = bilinearCubeUV( envMap, sampleDir, mipInt ); + if ( mipF == 0.0 ) { + return vec4( color0, 1.0 ); + } else { + vec3 color1 = bilinearCubeUV( envMap, sampleDir, mipInt + 1.0 ); + return vec4( mix( color0, color1, mipF ), 1.0 ); + } + } +#endif`,Ju=`vec3 transformedNormal = objectNormal; +#ifdef USE_TANGENT + vec3 transformedTangent = objectTangent; +#endif +#ifdef USE_BATCHING + mat3 bm = mat3( batchingMatrix ); + transformedNormal /= vec3( dot( bm[ 0 ], bm[ 0 ] ), dot( bm[ 1 ], bm[ 1 ] ), dot( bm[ 2 ], bm[ 2 ] ) ); + transformedNormal = bm * transformedNormal; + #ifdef USE_TANGENT + transformedTangent = bm * transformedTangent; + #endif +#endif +#ifdef USE_INSTANCING + mat3 im = mat3( instanceMatrix ); + transformedNormal /= vec3( dot( im[ 0 ], im[ 0 ] ), dot( im[ 1 ], im[ 1 ] ), dot( im[ 2 ], im[ 2 ] ) ); + transformedNormal = im * transformedNormal; + #ifdef USE_TANGENT + transformedTangent = im * transformedTangent; + #endif +#endif +transformedNormal = normalMatrix * transformedNormal; +#ifdef FLIP_SIDED + transformedNormal = - transformedNormal; +#endif +#ifdef USE_TANGENT + transformedTangent = ( modelViewMatrix * vec4( transformedTangent, 0.0 ) ).xyz; + #ifdef FLIP_SIDED + transformedTangent = - transformedTangent; + #endif +#endif`,Qu=`#ifdef USE_DISPLACEMENTMAP + uniform sampler2D displacementMap; + uniform float displacementScale; + uniform float displacementBias; +#endif`,td=`#ifdef USE_DISPLACEMENTMAP + transformed += normalize( objectNormal ) * ( texture2D( displacementMap, vDisplacementMapUv ).x * displacementScale + displacementBias ); +#endif`,ed=`#ifdef USE_EMISSIVEMAP + vec4 emissiveColor = texture2D( emissiveMap, vEmissiveMapUv ); + #ifdef DECODE_VIDEO_TEXTURE_EMISSIVE + emissiveColor = sRGBTransferEOTF( emissiveColor ); + #endif + totalEmissiveRadiance *= emissiveColor.rgb; +#endif`,nd=`#ifdef USE_EMISSIVEMAP + uniform sampler2D emissiveMap; +#endif`,id="gl_FragColor = linearToOutputTexel( gl_FragColor );",sd=`vec4 LinearTransferOETF( in vec4 value ) { + return value; +} +vec4 sRGBTransferEOTF( in vec4 value ) { + return vec4( mix( pow( value.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), value.rgb * 0.0773993808, vec3( lessThanEqual( value.rgb, vec3( 0.04045 ) ) ) ), value.a ); +} +vec4 sRGBTransferOETF( in vec4 value ) { + return vec4( mix( pow( value.rgb, vec3( 0.41666 ) ) * 1.055 - vec3( 0.055 ), value.rgb * 12.92, vec3( lessThanEqual( value.rgb, vec3( 0.0031308 ) ) ) ), value.a ); +}`,rd=`#ifdef USE_ENVMAP + #ifdef ENV_WORLDPOS + vec3 cameraToFrag; + if ( isOrthographic ) { + cameraToFrag = normalize( vec3( - viewMatrix[ 0 ][ 2 ], - viewMatrix[ 1 ][ 2 ], - viewMatrix[ 2 ][ 2 ] ) ); + } else { + cameraToFrag = normalize( vWorldPosition - cameraPosition ); + } + vec3 worldNormal = inverseTransformDirection( normal, viewMatrix ); + #ifdef ENVMAP_MODE_REFLECTION + vec3 reflectVec = reflect( cameraToFrag, worldNormal ); + #else + vec3 reflectVec = refract( cameraToFrag, worldNormal, refractionRatio ); + #endif + #else + vec3 reflectVec = vReflect; + #endif + #ifdef ENVMAP_TYPE_CUBE + vec4 envColor = textureCube( envMap, envMapRotation * vec3( flipEnvMap * reflectVec.x, reflectVec.yz ) ); + #else + vec4 envColor = vec4( 0.0 ); + #endif + #ifdef ENVMAP_BLENDING_MULTIPLY + outgoingLight = mix( outgoingLight, outgoingLight * envColor.xyz, specularStrength * reflectivity ); + #elif defined( ENVMAP_BLENDING_MIX ) + outgoingLight = mix( outgoingLight, envColor.xyz, specularStrength * reflectivity ); + #elif defined( ENVMAP_BLENDING_ADD ) + outgoingLight += envColor.xyz * specularStrength * reflectivity; + #endif +#endif`,ad=`#ifdef USE_ENVMAP + uniform float envMapIntensity; + uniform float flipEnvMap; + uniform mat3 envMapRotation; + #ifdef ENVMAP_TYPE_CUBE + uniform samplerCube envMap; + #else + uniform sampler2D envMap; + #endif + +#endif`,od=`#ifdef USE_ENVMAP + uniform float reflectivity; + #if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( PHONG ) || defined( LAMBERT ) + #define ENV_WORLDPOS + #endif + #ifdef ENV_WORLDPOS + varying vec3 vWorldPosition; + uniform float refractionRatio; + #else + varying vec3 vReflect; + #endif +#endif`,ld=`#ifdef USE_ENVMAP + #if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( PHONG ) || defined( LAMBERT ) + #define ENV_WORLDPOS + #endif + #ifdef ENV_WORLDPOS + + varying vec3 vWorldPosition; + #else + varying vec3 vReflect; + uniform float refractionRatio; + #endif +#endif`,cd=`#ifdef USE_ENVMAP + #ifdef ENV_WORLDPOS + vWorldPosition = worldPosition.xyz; + #else + vec3 cameraToVertex; + if ( isOrthographic ) { + cameraToVertex = normalize( vec3( - viewMatrix[ 0 ][ 2 ], - viewMatrix[ 1 ][ 2 ], - viewMatrix[ 2 ][ 2 ] ) ); + } else { + cameraToVertex = normalize( worldPosition.xyz - cameraPosition ); + } + vec3 worldNormal = inverseTransformDirection( transformedNormal, viewMatrix ); + #ifdef ENVMAP_MODE_REFLECTION + vReflect = reflect( cameraToVertex, worldNormal ); + #else + vReflect = refract( cameraToVertex, worldNormal, refractionRatio ); + #endif + #endif +#endif`,hd=`#ifdef USE_FOG + vFogDepth = - mvPosition.z; +#endif`,ud=`#ifdef USE_FOG + varying float vFogDepth; +#endif`,dd=`#ifdef USE_FOG + #ifdef FOG_EXP2 + float fogFactor = 1.0 - exp( - fogDensity * fogDensity * vFogDepth * vFogDepth ); + #else + float fogFactor = smoothstep( fogNear, fogFar, vFogDepth ); + #endif + gl_FragColor.rgb = mix( gl_FragColor.rgb, fogColor, fogFactor ); +#endif`,fd=`#ifdef USE_FOG + uniform vec3 fogColor; + varying float vFogDepth; + #ifdef FOG_EXP2 + uniform float fogDensity; + #else + uniform float fogNear; + uniform float fogFar; + #endif +#endif`,pd=`#ifdef USE_GRADIENTMAP + uniform sampler2D gradientMap; +#endif +vec3 getGradientIrradiance( vec3 normal, vec3 lightDirection ) { + float dotNL = dot( normal, lightDirection ); + vec2 coord = vec2( dotNL * 0.5 + 0.5, 0.0 ); + #ifdef USE_GRADIENTMAP + return vec3( texture2D( gradientMap, coord ).r ); + #else + vec2 fw = fwidth( coord ) * 0.5; + return mix( vec3( 0.7 ), vec3( 1.0 ), smoothstep( 0.7 - fw.x, 0.7 + fw.x, coord.x ) ); + #endif +}`,md=`#ifdef USE_LIGHTMAP + uniform sampler2D lightMap; + uniform float lightMapIntensity; +#endif`,gd=`LambertMaterial material; +material.diffuseColor = diffuseColor.rgb; +material.specularStrength = specularStrength;`,_d=`varying vec3 vViewPosition; +struct LambertMaterial { + vec3 diffuseColor; + float specularStrength; +}; +void RE_Direct_Lambert( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in LambertMaterial material, inout ReflectedLight reflectedLight ) { + float dotNL = saturate( dot( geometryNormal, directLight.direction ) ); + vec3 irradiance = dotNL * directLight.color; + reflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); +} +void RE_IndirectDiffuse_Lambert( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in LambertMaterial material, inout ReflectedLight reflectedLight ) { + reflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); +} +#define RE_Direct RE_Direct_Lambert +#define RE_IndirectDiffuse RE_IndirectDiffuse_Lambert`,vd=`uniform bool receiveShadow; +uniform vec3 ambientLightColor; +#if defined( USE_LIGHT_PROBES ) + uniform vec3 lightProbe[ 9 ]; +#endif +vec3 shGetIrradianceAt( in vec3 normal, in vec3 shCoefficients[ 9 ] ) { + float x = normal.x, y = normal.y, z = normal.z; + vec3 result = shCoefficients[ 0 ] * 0.886227; + result += shCoefficients[ 1 ] * 2.0 * 0.511664 * y; + result += shCoefficients[ 2 ] * 2.0 * 0.511664 * z; + result += shCoefficients[ 3 ] * 2.0 * 0.511664 * x; + result += shCoefficients[ 4 ] * 2.0 * 0.429043 * x * y; + result += shCoefficients[ 5 ] * 2.0 * 0.429043 * y * z; + result += shCoefficients[ 6 ] * ( 0.743125 * z * z - 0.247708 ); + result += shCoefficients[ 7 ] * 2.0 * 0.429043 * x * z; + result += shCoefficients[ 8 ] * 0.429043 * ( x * x - y * y ); + return result; +} +vec3 getLightProbeIrradiance( const in vec3 lightProbe[ 9 ], const in vec3 normal ) { + vec3 worldNormal = inverseTransformDirection( normal, viewMatrix ); + vec3 irradiance = shGetIrradianceAt( worldNormal, lightProbe ); + return irradiance; +} +vec3 getAmbientLightIrradiance( const in vec3 ambientLightColor ) { + vec3 irradiance = ambientLightColor; + return irradiance; +} +float getDistanceAttenuation( const in float lightDistance, const in float cutoffDistance, const in float decayExponent ) { + float distanceFalloff = 1.0 / max( pow( lightDistance, decayExponent ), 0.01 ); + if ( cutoffDistance > 0.0 ) { + distanceFalloff *= pow2( saturate( 1.0 - pow4( lightDistance / cutoffDistance ) ) ); + } + return distanceFalloff; +} +float getSpotAttenuation( const in float coneCosine, const in float penumbraCosine, const in float angleCosine ) { + return smoothstep( coneCosine, penumbraCosine, angleCosine ); +} +#if NUM_DIR_LIGHTS > 0 + struct DirectionalLight { + vec3 direction; + vec3 color; + }; + uniform DirectionalLight directionalLights[ NUM_DIR_LIGHTS ]; + void getDirectionalLightInfo( const in DirectionalLight directionalLight, out IncidentLight light ) { + light.color = directionalLight.color; + light.direction = directionalLight.direction; + light.visible = true; + } +#endif +#if NUM_POINT_LIGHTS > 0 + struct PointLight { + vec3 position; + vec3 color; + float distance; + float decay; + }; + uniform PointLight pointLights[ NUM_POINT_LIGHTS ]; + void getPointLightInfo( const in PointLight pointLight, const in vec3 geometryPosition, out IncidentLight light ) { + vec3 lVector = pointLight.position - geometryPosition; + light.direction = normalize( lVector ); + float lightDistance = length( lVector ); + light.color = pointLight.color; + light.color *= getDistanceAttenuation( lightDistance, pointLight.distance, pointLight.decay ); + light.visible = ( light.color != vec3( 0.0 ) ); + } +#endif +#if NUM_SPOT_LIGHTS > 0 + struct SpotLight { + vec3 position; + vec3 direction; + vec3 color; + float distance; + float decay; + float coneCos; + float penumbraCos; + }; + uniform SpotLight spotLights[ NUM_SPOT_LIGHTS ]; + void getSpotLightInfo( const in SpotLight spotLight, const in vec3 geometryPosition, out IncidentLight light ) { + vec3 lVector = spotLight.position - geometryPosition; + light.direction = normalize( lVector ); + float angleCos = dot( light.direction, spotLight.direction ); + float spotAttenuation = getSpotAttenuation( spotLight.coneCos, spotLight.penumbraCos, angleCos ); + if ( spotAttenuation > 0.0 ) { + float lightDistance = length( lVector ); + light.color = spotLight.color * spotAttenuation; + light.color *= getDistanceAttenuation( lightDistance, spotLight.distance, spotLight.decay ); + light.visible = ( light.color != vec3( 0.0 ) ); + } else { + light.color = vec3( 0.0 ); + light.visible = false; + } + } +#endif +#if NUM_RECT_AREA_LIGHTS > 0 + struct RectAreaLight { + vec3 color; + vec3 position; + vec3 halfWidth; + vec3 halfHeight; + }; + uniform sampler2D ltc_1; uniform sampler2D ltc_2; + uniform RectAreaLight rectAreaLights[ NUM_RECT_AREA_LIGHTS ]; +#endif +#if NUM_HEMI_LIGHTS > 0 + struct HemisphereLight { + vec3 direction; + vec3 skyColor; + vec3 groundColor; + }; + uniform HemisphereLight hemisphereLights[ NUM_HEMI_LIGHTS ]; + vec3 getHemisphereLightIrradiance( const in HemisphereLight hemiLight, const in vec3 normal ) { + float dotNL = dot( normal, hemiLight.direction ); + float hemiDiffuseWeight = 0.5 * dotNL + 0.5; + vec3 irradiance = mix( hemiLight.groundColor, hemiLight.skyColor, hemiDiffuseWeight ); + return irradiance; + } +#endif`,xd=`#ifdef USE_ENVMAP + vec3 getIBLIrradiance( const in vec3 normal ) { + #ifdef ENVMAP_TYPE_CUBE_UV + vec3 worldNormal = inverseTransformDirection( normal, viewMatrix ); + vec4 envMapColor = textureCubeUV( envMap, envMapRotation * worldNormal, 1.0 ); + return PI * envMapColor.rgb * envMapIntensity; + #else + return vec3( 0.0 ); + #endif + } + vec3 getIBLRadiance( const in vec3 viewDir, const in vec3 normal, const in float roughness ) { + #ifdef ENVMAP_TYPE_CUBE_UV + vec3 reflectVec = reflect( - viewDir, normal ); + reflectVec = normalize( mix( reflectVec, normal, roughness * roughness) ); + reflectVec = inverseTransformDirection( reflectVec, viewMatrix ); + vec4 envMapColor = textureCubeUV( envMap, envMapRotation * reflectVec, roughness ); + return envMapColor.rgb * envMapIntensity; + #else + return vec3( 0.0 ); + #endif + } + #ifdef USE_ANISOTROPY + vec3 getIBLAnisotropyRadiance( const in vec3 viewDir, const in vec3 normal, const in float roughness, const in vec3 bitangent, const in float anisotropy ) { + #ifdef ENVMAP_TYPE_CUBE_UV + vec3 bentNormal = cross( bitangent, viewDir ); + bentNormal = normalize( cross( bentNormal, bitangent ) ); + bentNormal = normalize( mix( bentNormal, normal, pow2( pow2( 1.0 - anisotropy * ( 1.0 - roughness ) ) ) ) ); + return getIBLRadiance( viewDir, bentNormal, roughness ); + #else + return vec3( 0.0 ); + #endif + } + #endif +#endif`,Md=`ToonMaterial material; +material.diffuseColor = diffuseColor.rgb;`,Sd=`varying vec3 vViewPosition; +struct ToonMaterial { + vec3 diffuseColor; +}; +void RE_Direct_Toon( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in ToonMaterial material, inout ReflectedLight reflectedLight ) { + vec3 irradiance = getGradientIrradiance( geometryNormal, directLight.direction ) * directLight.color; + reflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); +} +void RE_IndirectDiffuse_Toon( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in ToonMaterial material, inout ReflectedLight reflectedLight ) { + reflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); +} +#define RE_Direct RE_Direct_Toon +#define RE_IndirectDiffuse RE_IndirectDiffuse_Toon`,yd=`BlinnPhongMaterial material; +material.diffuseColor = diffuseColor.rgb; +material.specularColor = specular; +material.specularShininess = shininess; +material.specularStrength = specularStrength;`,Ed=`varying vec3 vViewPosition; +struct BlinnPhongMaterial { + vec3 diffuseColor; + vec3 specularColor; + float specularShininess; + float specularStrength; +}; +void RE_Direct_BlinnPhong( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) { + float dotNL = saturate( dot( geometryNormal, directLight.direction ) ); + vec3 irradiance = dotNL * directLight.color; + reflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); + reflectedLight.directSpecular += irradiance * BRDF_BlinnPhong( directLight.direction, geometryViewDir, geometryNormal, material.specularColor, material.specularShininess ) * material.specularStrength; +} +void RE_IndirectDiffuse_BlinnPhong( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) { + reflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); +} +#define RE_Direct RE_Direct_BlinnPhong +#define RE_IndirectDiffuse RE_IndirectDiffuse_BlinnPhong`,bd=`PhysicalMaterial material; +material.diffuseColor = diffuseColor.rgb * ( 1.0 - metalnessFactor ); +vec3 dxy = max( abs( dFdx( nonPerturbedNormal ) ), abs( dFdy( nonPerturbedNormal ) ) ); +float geometryRoughness = max( max( dxy.x, dxy.y ), dxy.z ); +material.roughness = max( roughnessFactor, 0.0525 );material.roughness += geometryRoughness; +material.roughness = min( material.roughness, 1.0 ); +#ifdef IOR + material.ior = ior; + #ifdef USE_SPECULAR + float specularIntensityFactor = specularIntensity; + vec3 specularColorFactor = specularColor; + #ifdef USE_SPECULAR_COLORMAP + specularColorFactor *= texture2D( specularColorMap, vSpecularColorMapUv ).rgb; + #endif + #ifdef USE_SPECULAR_INTENSITYMAP + specularIntensityFactor *= texture2D( specularIntensityMap, vSpecularIntensityMapUv ).a; + #endif + material.specularF90 = mix( specularIntensityFactor, 1.0, metalnessFactor ); + #else + float specularIntensityFactor = 1.0; + vec3 specularColorFactor = vec3( 1.0 ); + material.specularF90 = 1.0; + #endif + material.specularColor = mix( min( pow2( ( material.ior - 1.0 ) / ( material.ior + 1.0 ) ) * specularColorFactor, vec3( 1.0 ) ) * specularIntensityFactor, diffuseColor.rgb, metalnessFactor ); +#else + material.specularColor = mix( vec3( 0.04 ), diffuseColor.rgb, metalnessFactor ); + material.specularF90 = 1.0; +#endif +#ifdef USE_CLEARCOAT + material.clearcoat = clearcoat; + material.clearcoatRoughness = clearcoatRoughness; + material.clearcoatF0 = vec3( 0.04 ); + material.clearcoatF90 = 1.0; + #ifdef USE_CLEARCOATMAP + material.clearcoat *= texture2D( clearcoatMap, vClearcoatMapUv ).x; + #endif + #ifdef USE_CLEARCOAT_ROUGHNESSMAP + material.clearcoatRoughness *= texture2D( clearcoatRoughnessMap, vClearcoatRoughnessMapUv ).y; + #endif + material.clearcoat = saturate( material.clearcoat ); material.clearcoatRoughness = max( material.clearcoatRoughness, 0.0525 ); + material.clearcoatRoughness += geometryRoughness; + material.clearcoatRoughness = min( material.clearcoatRoughness, 1.0 ); +#endif +#ifdef USE_DISPERSION + material.dispersion = dispersion; +#endif +#ifdef USE_IRIDESCENCE + material.iridescence = iridescence; + material.iridescenceIOR = iridescenceIOR; + #ifdef USE_IRIDESCENCEMAP + material.iridescence *= texture2D( iridescenceMap, vIridescenceMapUv ).r; + #endif + #ifdef USE_IRIDESCENCE_THICKNESSMAP + material.iridescenceThickness = (iridescenceThicknessMaximum - iridescenceThicknessMinimum) * texture2D( iridescenceThicknessMap, vIridescenceThicknessMapUv ).g + iridescenceThicknessMinimum; + #else + material.iridescenceThickness = iridescenceThicknessMaximum; + #endif +#endif +#ifdef USE_SHEEN + material.sheenColor = sheenColor; + #ifdef USE_SHEEN_COLORMAP + material.sheenColor *= texture2D( sheenColorMap, vSheenColorMapUv ).rgb; + #endif + material.sheenRoughness = clamp( sheenRoughness, 0.07, 1.0 ); + #ifdef USE_SHEEN_ROUGHNESSMAP + material.sheenRoughness *= texture2D( sheenRoughnessMap, vSheenRoughnessMapUv ).a; + #endif +#endif +#ifdef USE_ANISOTROPY + #ifdef USE_ANISOTROPYMAP + mat2 anisotropyMat = mat2( anisotropyVector.x, anisotropyVector.y, - anisotropyVector.y, anisotropyVector.x ); + vec3 anisotropyPolar = texture2D( anisotropyMap, vAnisotropyMapUv ).rgb; + vec2 anisotropyV = anisotropyMat * normalize( 2.0 * anisotropyPolar.rg - vec2( 1.0 ) ) * anisotropyPolar.b; + #else + vec2 anisotropyV = anisotropyVector; + #endif + material.anisotropy = length( anisotropyV ); + if( material.anisotropy == 0.0 ) { + anisotropyV = vec2( 1.0, 0.0 ); + } else { + anisotropyV /= material.anisotropy; + material.anisotropy = saturate( material.anisotropy ); + } + material.alphaT = mix( pow2( material.roughness ), 1.0, pow2( material.anisotropy ) ); + material.anisotropyT = tbn[ 0 ] * anisotropyV.x + tbn[ 1 ] * anisotropyV.y; + material.anisotropyB = tbn[ 1 ] * anisotropyV.x - tbn[ 0 ] * anisotropyV.y; +#endif`,Td=`struct PhysicalMaterial { + vec3 diffuseColor; + float roughness; + vec3 specularColor; + float specularF90; + float dispersion; + #ifdef USE_CLEARCOAT + float clearcoat; + float clearcoatRoughness; + vec3 clearcoatF0; + float clearcoatF90; + #endif + #ifdef USE_IRIDESCENCE + float iridescence; + float iridescenceIOR; + float iridescenceThickness; + vec3 iridescenceFresnel; + vec3 iridescenceF0; + #endif + #ifdef USE_SHEEN + vec3 sheenColor; + float sheenRoughness; + #endif + #ifdef IOR + float ior; + #endif + #ifdef USE_TRANSMISSION + float transmission; + float transmissionAlpha; + float thickness; + float attenuationDistance; + vec3 attenuationColor; + #endif + #ifdef USE_ANISOTROPY + float anisotropy; + float alphaT; + vec3 anisotropyT; + vec3 anisotropyB; + #endif +}; +vec3 clearcoatSpecularDirect = vec3( 0.0 ); +vec3 clearcoatSpecularIndirect = vec3( 0.0 ); +vec3 sheenSpecularDirect = vec3( 0.0 ); +vec3 sheenSpecularIndirect = vec3(0.0 ); +vec3 Schlick_to_F0( const in vec3 f, const in float f90, const in float dotVH ) { + float x = clamp( 1.0 - dotVH, 0.0, 1.0 ); + float x2 = x * x; + float x5 = clamp( x * x2 * x2, 0.0, 0.9999 ); + return ( f - vec3( f90 ) * x5 ) / ( 1.0 - x5 ); +} +float V_GGX_SmithCorrelated( const in float alpha, const in float dotNL, const in float dotNV ) { + float a2 = pow2( alpha ); + float gv = dotNL * sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNV ) ); + float gl = dotNV * sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNL ) ); + return 0.5 / max( gv + gl, EPSILON ); +} +float D_GGX( const in float alpha, const in float dotNH ) { + float a2 = pow2( alpha ); + float denom = pow2( dotNH ) * ( a2 - 1.0 ) + 1.0; + return RECIPROCAL_PI * a2 / pow2( denom ); +} +#ifdef USE_ANISOTROPY + float V_GGX_SmithCorrelated_Anisotropic( const in float alphaT, const in float alphaB, const in float dotTV, const in float dotBV, const in float dotTL, const in float dotBL, const in float dotNV, const in float dotNL ) { + float gv = dotNL * length( vec3( alphaT * dotTV, alphaB * dotBV, dotNV ) ); + float gl = dotNV * length( vec3( alphaT * dotTL, alphaB * dotBL, dotNL ) ); + float v = 0.5 / ( gv + gl ); + return saturate(v); + } + float D_GGX_Anisotropic( const in float alphaT, const in float alphaB, const in float dotNH, const in float dotTH, const in float dotBH ) { + float a2 = alphaT * alphaB; + highp vec3 v = vec3( alphaB * dotTH, alphaT * dotBH, a2 * dotNH ); + highp float v2 = dot( v, v ); + float w2 = a2 / v2; + return RECIPROCAL_PI * a2 * pow2 ( w2 ); + } +#endif +#ifdef USE_CLEARCOAT + vec3 BRDF_GGX_Clearcoat( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, const in PhysicalMaterial material) { + vec3 f0 = material.clearcoatF0; + float f90 = material.clearcoatF90; + float roughness = material.clearcoatRoughness; + float alpha = pow2( roughness ); + vec3 halfDir = normalize( lightDir + viewDir ); + float dotNL = saturate( dot( normal, lightDir ) ); + float dotNV = saturate( dot( normal, viewDir ) ); + float dotNH = saturate( dot( normal, halfDir ) ); + float dotVH = saturate( dot( viewDir, halfDir ) ); + vec3 F = F_Schlick( f0, f90, dotVH ); + float V = V_GGX_SmithCorrelated( alpha, dotNL, dotNV ); + float D = D_GGX( alpha, dotNH ); + return F * ( V * D ); + } +#endif +vec3 BRDF_GGX( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, const in PhysicalMaterial material ) { + vec3 f0 = material.specularColor; + float f90 = material.specularF90; + float roughness = material.roughness; + float alpha = pow2( roughness ); + vec3 halfDir = normalize( lightDir + viewDir ); + float dotNL = saturate( dot( normal, lightDir ) ); + float dotNV = saturate( dot( normal, viewDir ) ); + float dotNH = saturate( dot( normal, halfDir ) ); + float dotVH = saturate( dot( viewDir, halfDir ) ); + vec3 F = F_Schlick( f0, f90, dotVH ); + #ifdef USE_IRIDESCENCE + F = mix( F, material.iridescenceFresnel, material.iridescence ); + #endif + #ifdef USE_ANISOTROPY + float dotTL = dot( material.anisotropyT, lightDir ); + float dotTV = dot( material.anisotropyT, viewDir ); + float dotTH = dot( material.anisotropyT, halfDir ); + float dotBL = dot( material.anisotropyB, lightDir ); + float dotBV = dot( material.anisotropyB, viewDir ); + float dotBH = dot( material.anisotropyB, halfDir ); + float V = V_GGX_SmithCorrelated_Anisotropic( material.alphaT, alpha, dotTV, dotBV, dotTL, dotBL, dotNV, dotNL ); + float D = D_GGX_Anisotropic( material.alphaT, alpha, dotNH, dotTH, dotBH ); + #else + float V = V_GGX_SmithCorrelated( alpha, dotNL, dotNV ); + float D = D_GGX( alpha, dotNH ); + #endif + return F * ( V * D ); +} +vec2 LTC_Uv( const in vec3 N, const in vec3 V, const in float roughness ) { + const float LUT_SIZE = 64.0; + const float LUT_SCALE = ( LUT_SIZE - 1.0 ) / LUT_SIZE; + const float LUT_BIAS = 0.5 / LUT_SIZE; + float dotNV = saturate( dot( N, V ) ); + vec2 uv = vec2( roughness, sqrt( 1.0 - dotNV ) ); + uv = uv * LUT_SCALE + LUT_BIAS; + return uv; +} +float LTC_ClippedSphereFormFactor( const in vec3 f ) { + float l = length( f ); + return max( ( l * l + f.z ) / ( l + 1.0 ), 0.0 ); +} +vec3 LTC_EdgeVectorFormFactor( const in vec3 v1, const in vec3 v2 ) { + float x = dot( v1, v2 ); + float y = abs( x ); + float a = 0.8543985 + ( 0.4965155 + 0.0145206 * y ) * y; + float b = 3.4175940 + ( 4.1616724 + y ) * y; + float v = a / b; + float theta_sintheta = ( x > 0.0 ) ? v : 0.5 * inversesqrt( max( 1.0 - x * x, 1e-7 ) ) - v; + return cross( v1, v2 ) * theta_sintheta; +} +vec3 LTC_Evaluate( const in vec3 N, const in vec3 V, const in vec3 P, const in mat3 mInv, const in vec3 rectCoords[ 4 ] ) { + vec3 v1 = rectCoords[ 1 ] - rectCoords[ 0 ]; + vec3 v2 = rectCoords[ 3 ] - rectCoords[ 0 ]; + vec3 lightNormal = cross( v1, v2 ); + if( dot( lightNormal, P - rectCoords[ 0 ] ) < 0.0 ) return vec3( 0.0 ); + vec3 T1, T2; + T1 = normalize( V - N * dot( V, N ) ); + T2 = - cross( N, T1 ); + mat3 mat = mInv * transposeMat3( mat3( T1, T2, N ) ); + vec3 coords[ 4 ]; + coords[ 0 ] = mat * ( rectCoords[ 0 ] - P ); + coords[ 1 ] = mat * ( rectCoords[ 1 ] - P ); + coords[ 2 ] = mat * ( rectCoords[ 2 ] - P ); + coords[ 3 ] = mat * ( rectCoords[ 3 ] - P ); + coords[ 0 ] = normalize( coords[ 0 ] ); + coords[ 1 ] = normalize( coords[ 1 ] ); + coords[ 2 ] = normalize( coords[ 2 ] ); + coords[ 3 ] = normalize( coords[ 3 ] ); + vec3 vectorFormFactor = vec3( 0.0 ); + vectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 0 ], coords[ 1 ] ); + vectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 1 ], coords[ 2 ] ); + vectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 2 ], coords[ 3 ] ); + vectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 3 ], coords[ 0 ] ); + float result = LTC_ClippedSphereFormFactor( vectorFormFactor ); + return vec3( result ); +} +#if defined( USE_SHEEN ) +float D_Charlie( float roughness, float dotNH ) { + float alpha = pow2( roughness ); + float invAlpha = 1.0 / alpha; + float cos2h = dotNH * dotNH; + float sin2h = max( 1.0 - cos2h, 0.0078125 ); + return ( 2.0 + invAlpha ) * pow( sin2h, invAlpha * 0.5 ) / ( 2.0 * PI ); +} +float V_Neubelt( float dotNV, float dotNL ) { + return saturate( 1.0 / ( 4.0 * ( dotNL + dotNV - dotNL * dotNV ) ) ); +} +vec3 BRDF_Sheen( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, vec3 sheenColor, const in float sheenRoughness ) { + vec3 halfDir = normalize( lightDir + viewDir ); + float dotNL = saturate( dot( normal, lightDir ) ); + float dotNV = saturate( dot( normal, viewDir ) ); + float dotNH = saturate( dot( normal, halfDir ) ); + float D = D_Charlie( sheenRoughness, dotNH ); + float V = V_Neubelt( dotNV, dotNL ); + return sheenColor * ( D * V ); +} +#endif +float IBLSheenBRDF( const in vec3 normal, const in vec3 viewDir, const in float roughness ) { + float dotNV = saturate( dot( normal, viewDir ) ); + float r2 = roughness * roughness; + float a = roughness < 0.25 ? -339.2 * r2 + 161.4 * roughness - 25.9 : -8.48 * r2 + 14.3 * roughness - 9.95; + float b = roughness < 0.25 ? 44.0 * r2 - 23.7 * roughness + 3.26 : 1.97 * r2 - 3.27 * roughness + 0.72; + float DG = exp( a * dotNV + b ) + ( roughness < 0.25 ? 0.0 : 0.1 * ( roughness - 0.25 ) ); + return saturate( DG * RECIPROCAL_PI ); +} +vec2 DFGApprox( const in vec3 normal, const in vec3 viewDir, const in float roughness ) { + float dotNV = saturate( dot( normal, viewDir ) ); + const vec4 c0 = vec4( - 1, - 0.0275, - 0.572, 0.022 ); + const vec4 c1 = vec4( 1, 0.0425, 1.04, - 0.04 ); + vec4 r = roughness * c0 + c1; + float a004 = min( r.x * r.x, exp2( - 9.28 * dotNV ) ) * r.x + r.y; + vec2 fab = vec2( - 1.04, 1.04 ) * a004 + r.zw; + return fab; +} +vec3 EnvironmentBRDF( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float roughness ) { + vec2 fab = DFGApprox( normal, viewDir, roughness ); + return specularColor * fab.x + specularF90 * fab.y; +} +#ifdef USE_IRIDESCENCE +void computeMultiscatteringIridescence( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float iridescence, const in vec3 iridescenceF0, const in float roughness, inout vec3 singleScatter, inout vec3 multiScatter ) { +#else +void computeMultiscattering( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float roughness, inout vec3 singleScatter, inout vec3 multiScatter ) { +#endif + vec2 fab = DFGApprox( normal, viewDir, roughness ); + #ifdef USE_IRIDESCENCE + vec3 Fr = mix( specularColor, iridescenceF0, iridescence ); + #else + vec3 Fr = specularColor; + #endif + vec3 FssEss = Fr * fab.x + specularF90 * fab.y; + float Ess = fab.x + fab.y; + float Ems = 1.0 - Ess; + vec3 Favg = Fr + ( 1.0 - Fr ) * 0.047619; vec3 Fms = FssEss * Favg / ( 1.0 - Ems * Favg ); + singleScatter += FssEss; + multiScatter += Fms * Ems; +} +#if NUM_RECT_AREA_LIGHTS > 0 + void RE_Direct_RectArea_Physical( const in RectAreaLight rectAreaLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) { + vec3 normal = geometryNormal; + vec3 viewDir = geometryViewDir; + vec3 position = geometryPosition; + vec3 lightPos = rectAreaLight.position; + vec3 halfWidth = rectAreaLight.halfWidth; + vec3 halfHeight = rectAreaLight.halfHeight; + vec3 lightColor = rectAreaLight.color; + float roughness = material.roughness; + vec3 rectCoords[ 4 ]; + rectCoords[ 0 ] = lightPos + halfWidth - halfHeight; rectCoords[ 1 ] = lightPos - halfWidth - halfHeight; + rectCoords[ 2 ] = lightPos - halfWidth + halfHeight; + rectCoords[ 3 ] = lightPos + halfWidth + halfHeight; + vec2 uv = LTC_Uv( normal, viewDir, roughness ); + vec4 t1 = texture2D( ltc_1, uv ); + vec4 t2 = texture2D( ltc_2, uv ); + mat3 mInv = mat3( + vec3( t1.x, 0, t1.y ), + vec3( 0, 1, 0 ), + vec3( t1.z, 0, t1.w ) + ); + vec3 fresnel = ( material.specularColor * t2.x + ( vec3( 1.0 ) - material.specularColor ) * t2.y ); + reflectedLight.directSpecular += lightColor * fresnel * LTC_Evaluate( normal, viewDir, position, mInv, rectCoords ); + reflectedLight.directDiffuse += lightColor * material.diffuseColor * LTC_Evaluate( normal, viewDir, position, mat3( 1.0 ), rectCoords ); + } +#endif +void RE_Direct_Physical( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) { + float dotNL = saturate( dot( geometryNormal, directLight.direction ) ); + vec3 irradiance = dotNL * directLight.color; + #ifdef USE_CLEARCOAT + float dotNLcc = saturate( dot( geometryClearcoatNormal, directLight.direction ) ); + vec3 ccIrradiance = dotNLcc * directLight.color; + clearcoatSpecularDirect += ccIrradiance * BRDF_GGX_Clearcoat( directLight.direction, geometryViewDir, geometryClearcoatNormal, material ); + #endif + #ifdef USE_SHEEN + sheenSpecularDirect += irradiance * BRDF_Sheen( directLight.direction, geometryViewDir, geometryNormal, material.sheenColor, material.sheenRoughness ); + #endif + reflectedLight.directSpecular += irradiance * BRDF_GGX( directLight.direction, geometryViewDir, geometryNormal, material ); + reflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); +} +void RE_IndirectDiffuse_Physical( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) { + reflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); +} +void RE_IndirectSpecular_Physical( const in vec3 radiance, const in vec3 irradiance, const in vec3 clearcoatRadiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight) { + #ifdef USE_CLEARCOAT + clearcoatSpecularIndirect += clearcoatRadiance * EnvironmentBRDF( geometryClearcoatNormal, geometryViewDir, material.clearcoatF0, material.clearcoatF90, material.clearcoatRoughness ); + #endif + #ifdef USE_SHEEN + sheenSpecularIndirect += irradiance * material.sheenColor * IBLSheenBRDF( geometryNormal, geometryViewDir, material.sheenRoughness ); + #endif + vec3 singleScattering = vec3( 0.0 ); + vec3 multiScattering = vec3( 0.0 ); + vec3 cosineWeightedIrradiance = irradiance * RECIPROCAL_PI; + #ifdef USE_IRIDESCENCE + computeMultiscatteringIridescence( geometryNormal, geometryViewDir, material.specularColor, material.specularF90, material.iridescence, material.iridescenceFresnel, material.roughness, singleScattering, multiScattering ); + #else + computeMultiscattering( geometryNormal, geometryViewDir, material.specularColor, material.specularF90, material.roughness, singleScattering, multiScattering ); + #endif + vec3 totalScattering = singleScattering + multiScattering; + vec3 diffuse = material.diffuseColor * ( 1.0 - max( max( totalScattering.r, totalScattering.g ), totalScattering.b ) ); + reflectedLight.indirectSpecular += radiance * singleScattering; + reflectedLight.indirectSpecular += multiScattering * cosineWeightedIrradiance; + reflectedLight.indirectDiffuse += diffuse * cosineWeightedIrradiance; +} +#define RE_Direct RE_Direct_Physical +#define RE_Direct_RectArea RE_Direct_RectArea_Physical +#define RE_IndirectDiffuse RE_IndirectDiffuse_Physical +#define RE_IndirectSpecular RE_IndirectSpecular_Physical +float computeSpecularOcclusion( const in float dotNV, const in float ambientOcclusion, const in float roughness ) { + return saturate( pow( dotNV + ambientOcclusion, exp2( - 16.0 * roughness - 1.0 ) ) - 1.0 + ambientOcclusion ); +}`,wd=` +vec3 geometryPosition = - vViewPosition; +vec3 geometryNormal = normal; +vec3 geometryViewDir = ( isOrthographic ) ? vec3( 0, 0, 1 ) : normalize( vViewPosition ); +vec3 geometryClearcoatNormal = vec3( 0.0 ); +#ifdef USE_CLEARCOAT + geometryClearcoatNormal = clearcoatNormal; +#endif +#ifdef USE_IRIDESCENCE + float dotNVi = saturate( dot( normal, geometryViewDir ) ); + if ( material.iridescenceThickness == 0.0 ) { + material.iridescence = 0.0; + } else { + material.iridescence = saturate( material.iridescence ); + } + if ( material.iridescence > 0.0 ) { + material.iridescenceFresnel = evalIridescence( 1.0, material.iridescenceIOR, dotNVi, material.iridescenceThickness, material.specularColor ); + material.iridescenceF0 = Schlick_to_F0( material.iridescenceFresnel, 1.0, dotNVi ); + } +#endif +IncidentLight directLight; +#if ( NUM_POINT_LIGHTS > 0 ) && defined( RE_Direct ) + PointLight pointLight; + #if defined( USE_SHADOWMAP ) && NUM_POINT_LIGHT_SHADOWS > 0 + PointLightShadow pointLightShadow; + #endif + #pragma unroll_loop_start + for ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) { + pointLight = pointLights[ i ]; + getPointLightInfo( pointLight, geometryPosition, directLight ); + #if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_POINT_LIGHT_SHADOWS ) + pointLightShadow = pointLightShadows[ i ]; + directLight.color *= ( directLight.visible && receiveShadow ) ? getPointShadow( pointShadowMap[ i ], pointLightShadow.shadowMapSize, pointLightShadow.shadowIntensity, pointLightShadow.shadowBias, pointLightShadow.shadowRadius, vPointShadowCoord[ i ], pointLightShadow.shadowCameraNear, pointLightShadow.shadowCameraFar ) : 1.0; + #endif + RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight ); + } + #pragma unroll_loop_end +#endif +#if ( NUM_SPOT_LIGHTS > 0 ) && defined( RE_Direct ) + SpotLight spotLight; + vec4 spotColor; + vec3 spotLightCoord; + bool inSpotLightMap; + #if defined( USE_SHADOWMAP ) && NUM_SPOT_LIGHT_SHADOWS > 0 + SpotLightShadow spotLightShadow; + #endif + #pragma unroll_loop_start + for ( int i = 0; i < NUM_SPOT_LIGHTS; i ++ ) { + spotLight = spotLights[ i ]; + getSpotLightInfo( spotLight, geometryPosition, directLight ); + #if ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS_WITH_MAPS ) + #define SPOT_LIGHT_MAP_INDEX UNROLLED_LOOP_INDEX + #elif ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS ) + #define SPOT_LIGHT_MAP_INDEX NUM_SPOT_LIGHT_MAPS + #else + #define SPOT_LIGHT_MAP_INDEX ( UNROLLED_LOOP_INDEX - NUM_SPOT_LIGHT_SHADOWS + NUM_SPOT_LIGHT_SHADOWS_WITH_MAPS ) + #endif + #if ( SPOT_LIGHT_MAP_INDEX < NUM_SPOT_LIGHT_MAPS ) + spotLightCoord = vSpotLightCoord[ i ].xyz / vSpotLightCoord[ i ].w; + inSpotLightMap = all( lessThan( abs( spotLightCoord * 2. - 1. ), vec3( 1.0 ) ) ); + spotColor = texture2D( spotLightMap[ SPOT_LIGHT_MAP_INDEX ], spotLightCoord.xy ); + directLight.color = inSpotLightMap ? directLight.color * spotColor.rgb : directLight.color; + #endif + #undef SPOT_LIGHT_MAP_INDEX + #if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS ) + spotLightShadow = spotLightShadows[ i ]; + directLight.color *= ( directLight.visible && receiveShadow ) ? getShadow( spotShadowMap[ i ], spotLightShadow.shadowMapSize, spotLightShadow.shadowIntensity, spotLightShadow.shadowBias, spotLightShadow.shadowRadius, vSpotLightCoord[ i ] ) : 1.0; + #endif + RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight ); + } + #pragma unroll_loop_end +#endif +#if ( NUM_DIR_LIGHTS > 0 ) && defined( RE_Direct ) + DirectionalLight directionalLight; + #if defined( USE_SHADOWMAP ) && NUM_DIR_LIGHT_SHADOWS > 0 + DirectionalLightShadow directionalLightShadow; + #endif + #pragma unroll_loop_start + for ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) { + directionalLight = directionalLights[ i ]; + getDirectionalLightInfo( directionalLight, directLight ); + #if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS ) + directionalLightShadow = directionalLightShadows[ i ]; + directLight.color *= ( directLight.visible && receiveShadow ) ? getShadow( directionalShadowMap[ i ], directionalLightShadow.shadowMapSize, directionalLightShadow.shadowIntensity, directionalLightShadow.shadowBias, directionalLightShadow.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0; + #endif + RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight ); + } + #pragma unroll_loop_end +#endif +#if ( NUM_RECT_AREA_LIGHTS > 0 ) && defined( RE_Direct_RectArea ) + RectAreaLight rectAreaLight; + #pragma unroll_loop_start + for ( int i = 0; i < NUM_RECT_AREA_LIGHTS; i ++ ) { + rectAreaLight = rectAreaLights[ i ]; + RE_Direct_RectArea( rectAreaLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight ); + } + #pragma unroll_loop_end +#endif +#if defined( RE_IndirectDiffuse ) + vec3 iblIrradiance = vec3( 0.0 ); + vec3 irradiance = getAmbientLightIrradiance( ambientLightColor ); + #if defined( USE_LIGHT_PROBES ) + irradiance += getLightProbeIrradiance( lightProbe, geometryNormal ); + #endif + #if ( NUM_HEMI_LIGHTS > 0 ) + #pragma unroll_loop_start + for ( int i = 0; i < NUM_HEMI_LIGHTS; i ++ ) { + irradiance += getHemisphereLightIrradiance( hemisphereLights[ i ], geometryNormal ); + } + #pragma unroll_loop_end + #endif +#endif +#if defined( RE_IndirectSpecular ) + vec3 radiance = vec3( 0.0 ); + vec3 clearcoatRadiance = vec3( 0.0 ); +#endif`,Ad=`#if defined( RE_IndirectDiffuse ) + #ifdef USE_LIGHTMAP + vec4 lightMapTexel = texture2D( lightMap, vLightMapUv ); + vec3 lightMapIrradiance = lightMapTexel.rgb * lightMapIntensity; + irradiance += lightMapIrradiance; + #endif + #if defined( USE_ENVMAP ) && defined( STANDARD ) && defined( ENVMAP_TYPE_CUBE_UV ) + iblIrradiance += getIBLIrradiance( geometryNormal ); + #endif +#endif +#if defined( USE_ENVMAP ) && defined( RE_IndirectSpecular ) + #ifdef USE_ANISOTROPY + radiance += getIBLAnisotropyRadiance( geometryViewDir, geometryNormal, material.roughness, material.anisotropyB, material.anisotropy ); + #else + radiance += getIBLRadiance( geometryViewDir, geometryNormal, material.roughness ); + #endif + #ifdef USE_CLEARCOAT + clearcoatRadiance += getIBLRadiance( geometryViewDir, geometryClearcoatNormal, material.clearcoatRoughness ); + #endif +#endif`,Rd=`#if defined( RE_IndirectDiffuse ) + RE_IndirectDiffuse( irradiance, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight ); +#endif +#if defined( RE_IndirectSpecular ) + RE_IndirectSpecular( radiance, iblIrradiance, clearcoatRadiance, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight ); +#endif`,Cd=`#if defined( USE_LOGDEPTHBUF ) + gl_FragDepth = vIsPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5; +#endif`,Pd=`#if defined( USE_LOGDEPTHBUF ) + uniform float logDepthBufFC; + varying float vFragDepth; + varying float vIsPerspective; +#endif`,Dd=`#ifdef USE_LOGDEPTHBUF + varying float vFragDepth; + varying float vIsPerspective; +#endif`,Ld=`#ifdef USE_LOGDEPTHBUF + vFragDepth = 1.0 + gl_Position.w; + vIsPerspective = float( isPerspectiveMatrix( projectionMatrix ) ); +#endif`,Ud=`#ifdef USE_MAP + vec4 sampledDiffuseColor = texture2D( map, vMapUv ); + #ifdef DECODE_VIDEO_TEXTURE + sampledDiffuseColor = sRGBTransferEOTF( sampledDiffuseColor ); + #endif + diffuseColor *= sampledDiffuseColor; +#endif`,Id=`#ifdef USE_MAP + uniform sampler2D map; +#endif`,Nd=`#if defined( USE_MAP ) || defined( USE_ALPHAMAP ) + #if defined( USE_POINTS_UV ) + vec2 uv = vUv; + #else + vec2 uv = ( uvTransform * vec3( gl_PointCoord.x, 1.0 - gl_PointCoord.y, 1 ) ).xy; + #endif +#endif +#ifdef USE_MAP + diffuseColor *= texture2D( map, uv ); +#endif +#ifdef USE_ALPHAMAP + diffuseColor.a *= texture2D( alphaMap, uv ).g; +#endif`,Fd=`#if defined( USE_POINTS_UV ) + varying vec2 vUv; +#else + #if defined( USE_MAP ) || defined( USE_ALPHAMAP ) + uniform mat3 uvTransform; + #endif +#endif +#ifdef USE_MAP + uniform sampler2D map; +#endif +#ifdef USE_ALPHAMAP + uniform sampler2D alphaMap; +#endif`,Od=`float metalnessFactor = metalness; +#ifdef USE_METALNESSMAP + vec4 texelMetalness = texture2D( metalnessMap, vMetalnessMapUv ); + metalnessFactor *= texelMetalness.b; +#endif`,Bd=`#ifdef USE_METALNESSMAP + uniform sampler2D metalnessMap; +#endif`,zd=`#ifdef USE_INSTANCING_MORPH + float morphTargetInfluences[ MORPHTARGETS_COUNT ]; + float morphTargetBaseInfluence = texelFetch( morphTexture, ivec2( 0, gl_InstanceID ), 0 ).r; + for ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) { + morphTargetInfluences[i] = texelFetch( morphTexture, ivec2( i + 1, gl_InstanceID ), 0 ).r; + } +#endif`,kd=`#if defined( USE_MORPHCOLORS ) + vColor *= morphTargetBaseInfluence; + for ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) { + #if defined( USE_COLOR_ALPHA ) + if ( morphTargetInfluences[ i ] != 0.0 ) vColor += getMorph( gl_VertexID, i, 2 ) * morphTargetInfluences[ i ]; + #elif defined( USE_COLOR ) + if ( morphTargetInfluences[ i ] != 0.0 ) vColor += getMorph( gl_VertexID, i, 2 ).rgb * morphTargetInfluences[ i ]; + #endif + } +#endif`,Hd=`#ifdef USE_MORPHNORMALS + objectNormal *= morphTargetBaseInfluence; + for ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) { + if ( morphTargetInfluences[ i ] != 0.0 ) objectNormal += getMorph( gl_VertexID, i, 1 ).xyz * morphTargetInfluences[ i ]; + } +#endif`,Vd=`#ifdef USE_MORPHTARGETS + #ifndef USE_INSTANCING_MORPH + uniform float morphTargetBaseInfluence; + uniform float morphTargetInfluences[ MORPHTARGETS_COUNT ]; + #endif + uniform sampler2DArray morphTargetsTexture; + uniform ivec2 morphTargetsTextureSize; + vec4 getMorph( const in int vertexIndex, const in int morphTargetIndex, const in int offset ) { + int texelIndex = vertexIndex * MORPHTARGETS_TEXTURE_STRIDE + offset; + int y = texelIndex / morphTargetsTextureSize.x; + int x = texelIndex - y * morphTargetsTextureSize.x; + ivec3 morphUV = ivec3( x, y, morphTargetIndex ); + return texelFetch( morphTargetsTexture, morphUV, 0 ); + } +#endif`,Gd=`#ifdef USE_MORPHTARGETS + transformed *= morphTargetBaseInfluence; + for ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) { + if ( morphTargetInfluences[ i ] != 0.0 ) transformed += getMorph( gl_VertexID, i, 0 ).xyz * morphTargetInfluences[ i ]; + } +#endif`,Wd=`float faceDirection = gl_FrontFacing ? 1.0 : - 1.0; +#ifdef FLAT_SHADED + vec3 fdx = dFdx( vViewPosition ); + vec3 fdy = dFdy( vViewPosition ); + vec3 normal = normalize( cross( fdx, fdy ) ); +#else + vec3 normal = normalize( vNormal ); + #ifdef DOUBLE_SIDED + normal *= faceDirection; + #endif +#endif +#if defined( USE_NORMALMAP_TANGENTSPACE ) || defined( USE_CLEARCOAT_NORMALMAP ) || defined( USE_ANISOTROPY ) + #ifdef USE_TANGENT + mat3 tbn = mat3( normalize( vTangent ), normalize( vBitangent ), normal ); + #else + mat3 tbn = getTangentFrame( - vViewPosition, normal, + #if defined( USE_NORMALMAP ) + vNormalMapUv + #elif defined( USE_CLEARCOAT_NORMALMAP ) + vClearcoatNormalMapUv + #else + vUv + #endif + ); + #endif + #if defined( DOUBLE_SIDED ) && ! defined( FLAT_SHADED ) + tbn[0] *= faceDirection; + tbn[1] *= faceDirection; + #endif +#endif +#ifdef USE_CLEARCOAT_NORMALMAP + #ifdef USE_TANGENT + mat3 tbn2 = mat3( normalize( vTangent ), normalize( vBitangent ), normal ); + #else + mat3 tbn2 = getTangentFrame( - vViewPosition, normal, vClearcoatNormalMapUv ); + #endif + #if defined( DOUBLE_SIDED ) && ! defined( FLAT_SHADED ) + tbn2[0] *= faceDirection; + tbn2[1] *= faceDirection; + #endif +#endif +vec3 nonPerturbedNormal = normal;`,Xd=`#ifdef USE_NORMALMAP_OBJECTSPACE + normal = texture2D( normalMap, vNormalMapUv ).xyz * 2.0 - 1.0; + #ifdef FLIP_SIDED + normal = - normal; + #endif + #ifdef DOUBLE_SIDED + normal = normal * faceDirection; + #endif + normal = normalize( normalMatrix * normal ); +#elif defined( USE_NORMALMAP_TANGENTSPACE ) + vec3 mapN = texture2D( normalMap, vNormalMapUv ).xyz * 2.0 - 1.0; + mapN.xy *= normalScale; + normal = normalize( tbn * mapN ); +#elif defined( USE_BUMPMAP ) + normal = perturbNormalArb( - vViewPosition, normal, dHdxy_fwd(), faceDirection ); +#endif`,Yd=`#ifndef FLAT_SHADED + varying vec3 vNormal; + #ifdef USE_TANGENT + varying vec3 vTangent; + varying vec3 vBitangent; + #endif +#endif`,qd=`#ifndef FLAT_SHADED + varying vec3 vNormal; + #ifdef USE_TANGENT + varying vec3 vTangent; + varying vec3 vBitangent; + #endif +#endif`,jd=`#ifndef FLAT_SHADED + vNormal = normalize( transformedNormal ); + #ifdef USE_TANGENT + vTangent = normalize( transformedTangent ); + vBitangent = normalize( cross( vNormal, vTangent ) * tangent.w ); + #endif +#endif`,Zd=`#ifdef USE_NORMALMAP + uniform sampler2D normalMap; + uniform vec2 normalScale; +#endif +#ifdef USE_NORMALMAP_OBJECTSPACE + uniform mat3 normalMatrix; +#endif +#if ! defined ( USE_TANGENT ) && ( defined ( USE_NORMALMAP_TANGENTSPACE ) || defined ( USE_CLEARCOAT_NORMALMAP ) || defined( USE_ANISOTROPY ) ) + mat3 getTangentFrame( vec3 eye_pos, vec3 surf_norm, vec2 uv ) { + vec3 q0 = dFdx( eye_pos.xyz ); + vec3 q1 = dFdy( eye_pos.xyz ); + vec2 st0 = dFdx( uv.st ); + vec2 st1 = dFdy( uv.st ); + vec3 N = surf_norm; + vec3 q1perp = cross( q1, N ); + vec3 q0perp = cross( N, q0 ); + vec3 T = q1perp * st0.x + q0perp * st1.x; + vec3 B = q1perp * st0.y + q0perp * st1.y; + float det = max( dot( T, T ), dot( B, B ) ); + float scale = ( det == 0.0 ) ? 0.0 : inversesqrt( det ); + return mat3( T * scale, B * scale, N ); + } +#endif`,Kd=`#ifdef USE_CLEARCOAT + vec3 clearcoatNormal = nonPerturbedNormal; +#endif`,$d=`#ifdef USE_CLEARCOAT_NORMALMAP + vec3 clearcoatMapN = texture2D( clearcoatNormalMap, vClearcoatNormalMapUv ).xyz * 2.0 - 1.0; + clearcoatMapN.xy *= clearcoatNormalScale; + clearcoatNormal = normalize( tbn2 * clearcoatMapN ); +#endif`,Jd=`#ifdef USE_CLEARCOATMAP + uniform sampler2D clearcoatMap; +#endif +#ifdef USE_CLEARCOAT_NORMALMAP + uniform sampler2D clearcoatNormalMap; + uniform vec2 clearcoatNormalScale; +#endif +#ifdef USE_CLEARCOAT_ROUGHNESSMAP + uniform sampler2D clearcoatRoughnessMap; +#endif`,Qd=`#ifdef USE_IRIDESCENCEMAP + uniform sampler2D iridescenceMap; +#endif +#ifdef USE_IRIDESCENCE_THICKNESSMAP + uniform sampler2D iridescenceThicknessMap; +#endif`,tf=`#ifdef OPAQUE +diffuseColor.a = 1.0; +#endif +#ifdef USE_TRANSMISSION +diffuseColor.a *= material.transmissionAlpha; +#endif +gl_FragColor = vec4( outgoingLight, diffuseColor.a );`,ef=`vec3 packNormalToRGB( const in vec3 normal ) { + return normalize( normal ) * 0.5 + 0.5; +} +vec3 unpackRGBToNormal( const in vec3 rgb ) { + return 2.0 * rgb.xyz - 1.0; +} +const float PackUpscale = 256. / 255.;const float UnpackDownscale = 255. / 256.;const float ShiftRight8 = 1. / 256.; +const float Inv255 = 1. / 255.; +const vec4 PackFactors = vec4( 1.0, 256.0, 256.0 * 256.0, 256.0 * 256.0 * 256.0 ); +const vec2 UnpackFactors2 = vec2( UnpackDownscale, 1.0 / PackFactors.g ); +const vec3 UnpackFactors3 = vec3( UnpackDownscale / PackFactors.rg, 1.0 / PackFactors.b ); +const vec4 UnpackFactors4 = vec4( UnpackDownscale / PackFactors.rgb, 1.0 / PackFactors.a ); +vec4 packDepthToRGBA( const in float v ) { + if( v <= 0.0 ) + return vec4( 0., 0., 0., 0. ); + if( v >= 1.0 ) + return vec4( 1., 1., 1., 1. ); + float vuf; + float af = modf( v * PackFactors.a, vuf ); + float bf = modf( vuf * ShiftRight8, vuf ); + float gf = modf( vuf * ShiftRight8, vuf ); + return vec4( vuf * Inv255, gf * PackUpscale, bf * PackUpscale, af ); +} +vec3 packDepthToRGB( const in float v ) { + if( v <= 0.0 ) + return vec3( 0., 0., 0. ); + if( v >= 1.0 ) + return vec3( 1., 1., 1. ); + float vuf; + float bf = modf( v * PackFactors.b, vuf ); + float gf = modf( vuf * ShiftRight8, vuf ); + return vec3( vuf * Inv255, gf * PackUpscale, bf ); +} +vec2 packDepthToRG( const in float v ) { + if( v <= 0.0 ) + return vec2( 0., 0. ); + if( v >= 1.0 ) + return vec2( 1., 1. ); + float vuf; + float gf = modf( v * 256., vuf ); + return vec2( vuf * Inv255, gf ); +} +float unpackRGBAToDepth( const in vec4 v ) { + return dot( v, UnpackFactors4 ); +} +float unpackRGBToDepth( const in vec3 v ) { + return dot( v, UnpackFactors3 ); +} +float unpackRGToDepth( const in vec2 v ) { + return v.r * UnpackFactors2.r + v.g * UnpackFactors2.g; +} +vec4 pack2HalfToRGBA( const in vec2 v ) { + vec4 r = vec4( v.x, fract( v.x * 255.0 ), v.y, fract( v.y * 255.0 ) ); + return vec4( r.x - r.y / 255.0, r.y, r.z - r.w / 255.0, r.w ); +} +vec2 unpackRGBATo2Half( const in vec4 v ) { + return vec2( v.x + ( v.y / 255.0 ), v.z + ( v.w / 255.0 ) ); +} +float viewZToOrthographicDepth( const in float viewZ, const in float near, const in float far ) { + return ( viewZ + near ) / ( near - far ); +} +float orthographicDepthToViewZ( const in float depth, const in float near, const in float far ) { + return depth * ( near - far ) - near; +} +float viewZToPerspectiveDepth( const in float viewZ, const in float near, const in float far ) { + return ( ( near + viewZ ) * far ) / ( ( far - near ) * viewZ ); +} +float perspectiveDepthToViewZ( const in float depth, const in float near, const in float far ) { + return ( near * far ) / ( ( far - near ) * depth - far ); +}`,nf=`#ifdef PREMULTIPLIED_ALPHA + gl_FragColor.rgb *= gl_FragColor.a; +#endif`,sf=`vec4 mvPosition = vec4( transformed, 1.0 ); +#ifdef USE_BATCHING + mvPosition = batchingMatrix * mvPosition; +#endif +#ifdef USE_INSTANCING + mvPosition = instanceMatrix * mvPosition; +#endif +mvPosition = modelViewMatrix * mvPosition; +gl_Position = projectionMatrix * mvPosition;`,rf=`#ifdef DITHERING + gl_FragColor.rgb = dithering( gl_FragColor.rgb ); +#endif`,af=`#ifdef DITHERING + vec3 dithering( vec3 color ) { + float grid_position = rand( gl_FragCoord.xy ); + vec3 dither_shift_RGB = vec3( 0.25 / 255.0, -0.25 / 255.0, 0.25 / 255.0 ); + dither_shift_RGB = mix( 2.0 * dither_shift_RGB, -2.0 * dither_shift_RGB, grid_position ); + return color + dither_shift_RGB; + } +#endif`,of=`float roughnessFactor = roughness; +#ifdef USE_ROUGHNESSMAP + vec4 texelRoughness = texture2D( roughnessMap, vRoughnessMapUv ); + roughnessFactor *= texelRoughness.g; +#endif`,lf=`#ifdef USE_ROUGHNESSMAP + uniform sampler2D roughnessMap; +#endif`,cf=`#if NUM_SPOT_LIGHT_COORDS > 0 + varying vec4 vSpotLightCoord[ NUM_SPOT_LIGHT_COORDS ]; +#endif +#if NUM_SPOT_LIGHT_MAPS > 0 + uniform sampler2D spotLightMap[ NUM_SPOT_LIGHT_MAPS ]; +#endif +#ifdef USE_SHADOWMAP + #if NUM_DIR_LIGHT_SHADOWS > 0 + uniform sampler2D directionalShadowMap[ NUM_DIR_LIGHT_SHADOWS ]; + varying vec4 vDirectionalShadowCoord[ NUM_DIR_LIGHT_SHADOWS ]; + struct DirectionalLightShadow { + float shadowIntensity; + float shadowBias; + float shadowNormalBias; + float shadowRadius; + vec2 shadowMapSize; + }; + uniform DirectionalLightShadow directionalLightShadows[ NUM_DIR_LIGHT_SHADOWS ]; + #endif + #if NUM_SPOT_LIGHT_SHADOWS > 0 + uniform sampler2D spotShadowMap[ NUM_SPOT_LIGHT_SHADOWS ]; + struct SpotLightShadow { + float shadowIntensity; + float shadowBias; + float shadowNormalBias; + float shadowRadius; + vec2 shadowMapSize; + }; + uniform SpotLightShadow spotLightShadows[ NUM_SPOT_LIGHT_SHADOWS ]; + #endif + #if NUM_POINT_LIGHT_SHADOWS > 0 + uniform sampler2D pointShadowMap[ NUM_POINT_LIGHT_SHADOWS ]; + varying vec4 vPointShadowCoord[ NUM_POINT_LIGHT_SHADOWS ]; + struct PointLightShadow { + float shadowIntensity; + float shadowBias; + float shadowNormalBias; + float shadowRadius; + vec2 shadowMapSize; + float shadowCameraNear; + float shadowCameraFar; + }; + uniform PointLightShadow pointLightShadows[ NUM_POINT_LIGHT_SHADOWS ]; + #endif + float texture2DCompare( sampler2D depths, vec2 uv, float compare ) { + return step( compare, unpackRGBAToDepth( texture2D( depths, uv ) ) ); + } + vec2 texture2DDistribution( sampler2D shadow, vec2 uv ) { + return unpackRGBATo2Half( texture2D( shadow, uv ) ); + } + float VSMShadow (sampler2D shadow, vec2 uv, float compare ){ + float occlusion = 1.0; + vec2 distribution = texture2DDistribution( shadow, uv ); + float hard_shadow = step( compare , distribution.x ); + if (hard_shadow != 1.0 ) { + float distance = compare - distribution.x ; + float variance = max( 0.00000, distribution.y * distribution.y ); + float softness_probability = variance / (variance + distance * distance ); softness_probability = clamp( ( softness_probability - 0.3 ) / ( 0.95 - 0.3 ), 0.0, 1.0 ); occlusion = clamp( max( hard_shadow, softness_probability ), 0.0, 1.0 ); + } + return occlusion; + } + float getShadow( sampler2D shadowMap, vec2 shadowMapSize, float shadowIntensity, float shadowBias, float shadowRadius, vec4 shadowCoord ) { + float shadow = 1.0; + shadowCoord.xyz /= shadowCoord.w; + shadowCoord.z += shadowBias; + bool inFrustum = shadowCoord.x >= 0.0 && shadowCoord.x <= 1.0 && shadowCoord.y >= 0.0 && shadowCoord.y <= 1.0; + bool frustumTest = inFrustum && shadowCoord.z <= 1.0; + if ( frustumTest ) { + #if defined( SHADOWMAP_TYPE_PCF ) + vec2 texelSize = vec2( 1.0 ) / shadowMapSize; + float dx0 = - texelSize.x * shadowRadius; + float dy0 = - texelSize.y * shadowRadius; + float dx1 = + texelSize.x * shadowRadius; + float dy1 = + texelSize.y * shadowRadius; + float dx2 = dx0 / 2.0; + float dy2 = dy0 / 2.0; + float dx3 = dx1 / 2.0; + float dy3 = dy1 / 2.0; + shadow = ( + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, dy0 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy0 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, dy0 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, dy2 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy2 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, dy2 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, 0.0 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, 0.0 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy, shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, 0.0 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, 0.0 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, dy3 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy3 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, dy3 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, dy1 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy1 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, dy1 ), shadowCoord.z ) + ) * ( 1.0 / 17.0 ); + #elif defined( SHADOWMAP_TYPE_PCF_SOFT ) + vec2 texelSize = vec2( 1.0 ) / shadowMapSize; + float dx = texelSize.x; + float dy = texelSize.y; + vec2 uv = shadowCoord.xy; + vec2 f = fract( uv * shadowMapSize + 0.5 ); + uv -= f * texelSize; + shadow = ( + texture2DCompare( shadowMap, uv, shadowCoord.z ) + + texture2DCompare( shadowMap, uv + vec2( dx, 0.0 ), shadowCoord.z ) + + texture2DCompare( shadowMap, uv + vec2( 0.0, dy ), shadowCoord.z ) + + texture2DCompare( shadowMap, uv + texelSize, shadowCoord.z ) + + mix( texture2DCompare( shadowMap, uv + vec2( -dx, 0.0 ), shadowCoord.z ), + texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, 0.0 ), shadowCoord.z ), + f.x ) + + mix( texture2DCompare( shadowMap, uv + vec2( -dx, dy ), shadowCoord.z ), + texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, dy ), shadowCoord.z ), + f.x ) + + mix( texture2DCompare( shadowMap, uv + vec2( 0.0, -dy ), shadowCoord.z ), + texture2DCompare( shadowMap, uv + vec2( 0.0, 2.0 * dy ), shadowCoord.z ), + f.y ) + + mix( texture2DCompare( shadowMap, uv + vec2( dx, -dy ), shadowCoord.z ), + texture2DCompare( shadowMap, uv + vec2( dx, 2.0 * dy ), shadowCoord.z ), + f.y ) + + mix( mix( texture2DCompare( shadowMap, uv + vec2( -dx, -dy ), shadowCoord.z ), + texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, -dy ), shadowCoord.z ), + f.x ), + mix( texture2DCompare( shadowMap, uv + vec2( -dx, 2.0 * dy ), shadowCoord.z ), + texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, 2.0 * dy ), shadowCoord.z ), + f.x ), + f.y ) + ) * ( 1.0 / 9.0 ); + #elif defined( SHADOWMAP_TYPE_VSM ) + shadow = VSMShadow( shadowMap, shadowCoord.xy, shadowCoord.z ); + #else + shadow = texture2DCompare( shadowMap, shadowCoord.xy, shadowCoord.z ); + #endif + } + return mix( 1.0, shadow, shadowIntensity ); + } + vec2 cubeToUV( vec3 v, float texelSizeY ) { + vec3 absV = abs( v ); + float scaleToCube = 1.0 / max( absV.x, max( absV.y, absV.z ) ); + absV *= scaleToCube; + v *= scaleToCube * ( 1.0 - 2.0 * texelSizeY ); + vec2 planar = v.xy; + float almostATexel = 1.5 * texelSizeY; + float almostOne = 1.0 - almostATexel; + if ( absV.z >= almostOne ) { + if ( v.z > 0.0 ) + planar.x = 4.0 - v.x; + } else if ( absV.x >= almostOne ) { + float signX = sign( v.x ); + planar.x = v.z * signX + 2.0 * signX; + } else if ( absV.y >= almostOne ) { + float signY = sign( v.y ); + planar.x = v.x + 2.0 * signY + 2.0; + planar.y = v.z * signY - 2.0; + } + return vec2( 0.125, 0.25 ) * planar + vec2( 0.375, 0.75 ); + } + float getPointShadow( sampler2D shadowMap, vec2 shadowMapSize, float shadowIntensity, float shadowBias, float shadowRadius, vec4 shadowCoord, float shadowCameraNear, float shadowCameraFar ) { + float shadow = 1.0; + vec3 lightToPosition = shadowCoord.xyz; + + float lightToPositionLength = length( lightToPosition ); + if ( lightToPositionLength - shadowCameraFar <= 0.0 && lightToPositionLength - shadowCameraNear >= 0.0 ) { + float dp = ( lightToPositionLength - shadowCameraNear ) / ( shadowCameraFar - shadowCameraNear ); dp += shadowBias; + vec3 bd3D = normalize( lightToPosition ); + vec2 texelSize = vec2( 1.0 ) / ( shadowMapSize * vec2( 4.0, 2.0 ) ); + #if defined( SHADOWMAP_TYPE_PCF ) || defined( SHADOWMAP_TYPE_PCF_SOFT ) || defined( SHADOWMAP_TYPE_VSM ) + vec2 offset = vec2( - 1, 1 ) * shadowRadius * texelSize.y; + shadow = ( + texture2DCompare( shadowMap, cubeToUV( bd3D + offset.xyy, texelSize.y ), dp ) + + texture2DCompare( shadowMap, cubeToUV( bd3D + offset.yyy, texelSize.y ), dp ) + + texture2DCompare( shadowMap, cubeToUV( bd3D + offset.xyx, texelSize.y ), dp ) + + texture2DCompare( shadowMap, cubeToUV( bd3D + offset.yyx, texelSize.y ), dp ) + + texture2DCompare( shadowMap, cubeToUV( bd3D, texelSize.y ), dp ) + + texture2DCompare( shadowMap, cubeToUV( bd3D + offset.xxy, texelSize.y ), dp ) + + texture2DCompare( shadowMap, cubeToUV( bd3D + offset.yxy, texelSize.y ), dp ) + + texture2DCompare( shadowMap, cubeToUV( bd3D + offset.xxx, texelSize.y ), dp ) + + texture2DCompare( shadowMap, cubeToUV( bd3D + offset.yxx, texelSize.y ), dp ) + ) * ( 1.0 / 9.0 ); + #else + shadow = texture2DCompare( shadowMap, cubeToUV( bd3D, texelSize.y ), dp ); + #endif + } + return mix( 1.0, shadow, shadowIntensity ); + } +#endif`,hf=`#if NUM_SPOT_LIGHT_COORDS > 0 + uniform mat4 spotLightMatrix[ NUM_SPOT_LIGHT_COORDS ]; + varying vec4 vSpotLightCoord[ NUM_SPOT_LIGHT_COORDS ]; +#endif +#ifdef USE_SHADOWMAP + #if NUM_DIR_LIGHT_SHADOWS > 0 + uniform mat4 directionalShadowMatrix[ NUM_DIR_LIGHT_SHADOWS ]; + varying vec4 vDirectionalShadowCoord[ NUM_DIR_LIGHT_SHADOWS ]; + struct DirectionalLightShadow { + float shadowIntensity; + float shadowBias; + float shadowNormalBias; + float shadowRadius; + vec2 shadowMapSize; + }; + uniform DirectionalLightShadow directionalLightShadows[ NUM_DIR_LIGHT_SHADOWS ]; + #endif + #if NUM_SPOT_LIGHT_SHADOWS > 0 + struct SpotLightShadow { + float shadowIntensity; + float shadowBias; + float shadowNormalBias; + float shadowRadius; + vec2 shadowMapSize; + }; + uniform SpotLightShadow spotLightShadows[ NUM_SPOT_LIGHT_SHADOWS ]; + #endif + #if NUM_POINT_LIGHT_SHADOWS > 0 + uniform mat4 pointShadowMatrix[ NUM_POINT_LIGHT_SHADOWS ]; + varying vec4 vPointShadowCoord[ NUM_POINT_LIGHT_SHADOWS ]; + struct PointLightShadow { + float shadowIntensity; + float shadowBias; + float shadowNormalBias; + float shadowRadius; + vec2 shadowMapSize; + float shadowCameraNear; + float shadowCameraFar; + }; + uniform PointLightShadow pointLightShadows[ NUM_POINT_LIGHT_SHADOWS ]; + #endif +#endif`,uf=`#if ( defined( USE_SHADOWMAP ) && ( NUM_DIR_LIGHT_SHADOWS > 0 || NUM_POINT_LIGHT_SHADOWS > 0 ) ) || ( NUM_SPOT_LIGHT_COORDS > 0 ) + vec3 shadowWorldNormal = inverseTransformDirection( transformedNormal, viewMatrix ); + vec4 shadowWorldPosition; +#endif +#if defined( USE_SHADOWMAP ) + #if NUM_DIR_LIGHT_SHADOWS > 0 + #pragma unroll_loop_start + for ( int i = 0; i < NUM_DIR_LIGHT_SHADOWS; i ++ ) { + shadowWorldPosition = worldPosition + vec4( shadowWorldNormal * directionalLightShadows[ i ].shadowNormalBias, 0 ); + vDirectionalShadowCoord[ i ] = directionalShadowMatrix[ i ] * shadowWorldPosition; + } + #pragma unroll_loop_end + #endif + #if NUM_POINT_LIGHT_SHADOWS > 0 + #pragma unroll_loop_start + for ( int i = 0; i < NUM_POINT_LIGHT_SHADOWS; i ++ ) { + shadowWorldPosition = worldPosition + vec4( shadowWorldNormal * pointLightShadows[ i ].shadowNormalBias, 0 ); + vPointShadowCoord[ i ] = pointShadowMatrix[ i ] * shadowWorldPosition; + } + #pragma unroll_loop_end + #endif +#endif +#if NUM_SPOT_LIGHT_COORDS > 0 + #pragma unroll_loop_start + for ( int i = 0; i < NUM_SPOT_LIGHT_COORDS; i ++ ) { + shadowWorldPosition = worldPosition; + #if ( defined( USE_SHADOWMAP ) && UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS ) + shadowWorldPosition.xyz += shadowWorldNormal * spotLightShadows[ i ].shadowNormalBias; + #endif + vSpotLightCoord[ i ] = spotLightMatrix[ i ] * shadowWorldPosition; + } + #pragma unroll_loop_end +#endif`,df=`float getShadowMask() { + float shadow = 1.0; + #ifdef USE_SHADOWMAP + #if NUM_DIR_LIGHT_SHADOWS > 0 + DirectionalLightShadow directionalLight; + #pragma unroll_loop_start + for ( int i = 0; i < NUM_DIR_LIGHT_SHADOWS; i ++ ) { + directionalLight = directionalLightShadows[ i ]; + shadow *= receiveShadow ? getShadow( directionalShadowMap[ i ], directionalLight.shadowMapSize, directionalLight.shadowIntensity, directionalLight.shadowBias, directionalLight.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0; + } + #pragma unroll_loop_end + #endif + #if NUM_SPOT_LIGHT_SHADOWS > 0 + SpotLightShadow spotLight; + #pragma unroll_loop_start + for ( int i = 0; i < NUM_SPOT_LIGHT_SHADOWS; i ++ ) { + spotLight = spotLightShadows[ i ]; + shadow *= receiveShadow ? getShadow( spotShadowMap[ i ], spotLight.shadowMapSize, spotLight.shadowIntensity, spotLight.shadowBias, spotLight.shadowRadius, vSpotLightCoord[ i ] ) : 1.0; + } + #pragma unroll_loop_end + #endif + #if NUM_POINT_LIGHT_SHADOWS > 0 + PointLightShadow pointLight; + #pragma unroll_loop_start + for ( int i = 0; i < NUM_POINT_LIGHT_SHADOWS; i ++ ) { + pointLight = pointLightShadows[ i ]; + shadow *= receiveShadow ? getPointShadow( pointShadowMap[ i ], pointLight.shadowMapSize, pointLight.shadowIntensity, pointLight.shadowBias, pointLight.shadowRadius, vPointShadowCoord[ i ], pointLight.shadowCameraNear, pointLight.shadowCameraFar ) : 1.0; + } + #pragma unroll_loop_end + #endif + #endif + return shadow; +}`,ff=`#ifdef USE_SKINNING + mat4 boneMatX = getBoneMatrix( skinIndex.x ); + mat4 boneMatY = getBoneMatrix( skinIndex.y ); + mat4 boneMatZ = getBoneMatrix( skinIndex.z ); + mat4 boneMatW = getBoneMatrix( skinIndex.w ); +#endif`,pf=`#ifdef USE_SKINNING + uniform mat4 bindMatrix; + uniform mat4 bindMatrixInverse; + uniform highp sampler2D boneTexture; + mat4 getBoneMatrix( const in float i ) { + int size = textureSize( boneTexture, 0 ).x; + int j = int( i ) * 4; + int x = j % size; + int y = j / size; + vec4 v1 = texelFetch( boneTexture, ivec2( x, y ), 0 ); + vec4 v2 = texelFetch( boneTexture, ivec2( x + 1, y ), 0 ); + vec4 v3 = texelFetch( boneTexture, ivec2( x + 2, y ), 0 ); + vec4 v4 = texelFetch( boneTexture, ivec2( x + 3, y ), 0 ); + return mat4( v1, v2, v3, v4 ); + } +#endif`,mf=`#ifdef USE_SKINNING + vec4 skinVertex = bindMatrix * vec4( transformed, 1.0 ); + vec4 skinned = vec4( 0.0 ); + skinned += boneMatX * skinVertex * skinWeight.x; + skinned += boneMatY * skinVertex * skinWeight.y; + skinned += boneMatZ * skinVertex * skinWeight.z; + skinned += boneMatW * skinVertex * skinWeight.w; + transformed = ( bindMatrixInverse * skinned ).xyz; +#endif`,gf=`#ifdef USE_SKINNING + mat4 skinMatrix = mat4( 0.0 ); + skinMatrix += skinWeight.x * boneMatX; + skinMatrix += skinWeight.y * boneMatY; + skinMatrix += skinWeight.z * boneMatZ; + skinMatrix += skinWeight.w * boneMatW; + skinMatrix = bindMatrixInverse * skinMatrix * bindMatrix; + objectNormal = vec4( skinMatrix * vec4( objectNormal, 0.0 ) ).xyz; + #ifdef USE_TANGENT + objectTangent = vec4( skinMatrix * vec4( objectTangent, 0.0 ) ).xyz; + #endif +#endif`,_f=`float specularStrength; +#ifdef USE_SPECULARMAP + vec4 texelSpecular = texture2D( specularMap, vSpecularMapUv ); + specularStrength = texelSpecular.r; +#else + specularStrength = 1.0; +#endif`,vf=`#ifdef USE_SPECULARMAP + uniform sampler2D specularMap; +#endif`,xf=`#if defined( TONE_MAPPING ) + gl_FragColor.rgb = toneMapping( gl_FragColor.rgb ); +#endif`,Mf=`#ifndef saturate +#define saturate( a ) clamp( a, 0.0, 1.0 ) +#endif +uniform float toneMappingExposure; +vec3 LinearToneMapping( vec3 color ) { + return saturate( toneMappingExposure * color ); +} +vec3 ReinhardToneMapping( vec3 color ) { + color *= toneMappingExposure; + return saturate( color / ( vec3( 1.0 ) + color ) ); +} +vec3 CineonToneMapping( vec3 color ) { + color *= toneMappingExposure; + color = max( vec3( 0.0 ), color - 0.004 ); + return pow( ( color * ( 6.2 * color + 0.5 ) ) / ( color * ( 6.2 * color + 1.7 ) + 0.06 ), vec3( 2.2 ) ); +} +vec3 RRTAndODTFit( vec3 v ) { + vec3 a = v * ( v + 0.0245786 ) - 0.000090537; + vec3 b = v * ( 0.983729 * v + 0.4329510 ) + 0.238081; + return a / b; +} +vec3 ACESFilmicToneMapping( vec3 color ) { + const mat3 ACESInputMat = mat3( + vec3( 0.59719, 0.07600, 0.02840 ), vec3( 0.35458, 0.90834, 0.13383 ), + vec3( 0.04823, 0.01566, 0.83777 ) + ); + const mat3 ACESOutputMat = mat3( + vec3( 1.60475, -0.10208, -0.00327 ), vec3( -0.53108, 1.10813, -0.07276 ), + vec3( -0.07367, -0.00605, 1.07602 ) + ); + color *= toneMappingExposure / 0.6; + color = ACESInputMat * color; + color = RRTAndODTFit( color ); + color = ACESOutputMat * color; + return saturate( color ); +} +const mat3 LINEAR_REC2020_TO_LINEAR_SRGB = mat3( + vec3( 1.6605, - 0.1246, - 0.0182 ), + vec3( - 0.5876, 1.1329, - 0.1006 ), + vec3( - 0.0728, - 0.0083, 1.1187 ) +); +const mat3 LINEAR_SRGB_TO_LINEAR_REC2020 = mat3( + vec3( 0.6274, 0.0691, 0.0164 ), + vec3( 0.3293, 0.9195, 0.0880 ), + vec3( 0.0433, 0.0113, 0.8956 ) +); +vec3 agxDefaultContrastApprox( vec3 x ) { + vec3 x2 = x * x; + vec3 x4 = x2 * x2; + return + 15.5 * x4 * x2 + - 40.14 * x4 * x + + 31.96 * x4 + - 6.868 * x2 * x + + 0.4298 * x2 + + 0.1191 * x + - 0.00232; +} +vec3 AgXToneMapping( vec3 color ) { + const mat3 AgXInsetMatrix = mat3( + vec3( 0.856627153315983, 0.137318972929847, 0.11189821299995 ), + vec3( 0.0951212405381588, 0.761241990602591, 0.0767994186031903 ), + vec3( 0.0482516061458583, 0.101439036467562, 0.811302368396859 ) + ); + const mat3 AgXOutsetMatrix = mat3( + vec3( 1.1271005818144368, - 0.1413297634984383, - 0.14132976349843826 ), + vec3( - 0.11060664309660323, 1.157823702216272, - 0.11060664309660294 ), + vec3( - 0.016493938717834573, - 0.016493938717834257, 1.2519364065950405 ) + ); + const float AgxMinEv = - 12.47393; const float AgxMaxEv = 4.026069; + color *= toneMappingExposure; + color = LINEAR_SRGB_TO_LINEAR_REC2020 * color; + color = AgXInsetMatrix * color; + color = max( color, 1e-10 ); color = log2( color ); + color = ( color - AgxMinEv ) / ( AgxMaxEv - AgxMinEv ); + color = clamp( color, 0.0, 1.0 ); + color = agxDefaultContrastApprox( color ); + color = AgXOutsetMatrix * color; + color = pow( max( vec3( 0.0 ), color ), vec3( 2.2 ) ); + color = LINEAR_REC2020_TO_LINEAR_SRGB * color; + color = clamp( color, 0.0, 1.0 ); + return color; +} +vec3 NeutralToneMapping( vec3 color ) { + const float StartCompression = 0.8 - 0.04; + const float Desaturation = 0.15; + color *= toneMappingExposure; + float x = min( color.r, min( color.g, color.b ) ); + float offset = x < 0.08 ? x - 6.25 * x * x : 0.04; + color -= offset; + float peak = max( color.r, max( color.g, color.b ) ); + if ( peak < StartCompression ) return color; + float d = 1. - StartCompression; + float newPeak = 1. - d * d / ( peak + d - StartCompression ); + color *= newPeak / peak; + float g = 1. - 1. / ( Desaturation * ( peak - newPeak ) + 1. ); + return mix( color, vec3( newPeak ), g ); +} +vec3 CustomToneMapping( vec3 color ) { return color; }`,Sf=`#ifdef USE_TRANSMISSION + material.transmission = transmission; + material.transmissionAlpha = 1.0; + material.thickness = thickness; + material.attenuationDistance = attenuationDistance; + material.attenuationColor = attenuationColor; + #ifdef USE_TRANSMISSIONMAP + material.transmission *= texture2D( transmissionMap, vTransmissionMapUv ).r; + #endif + #ifdef USE_THICKNESSMAP + material.thickness *= texture2D( thicknessMap, vThicknessMapUv ).g; + #endif + vec3 pos = vWorldPosition; + vec3 v = normalize( cameraPosition - pos ); + vec3 n = inverseTransformDirection( normal, viewMatrix ); + vec4 transmitted = getIBLVolumeRefraction( + n, v, material.roughness, material.diffuseColor, material.specularColor, material.specularF90, + pos, modelMatrix, viewMatrix, projectionMatrix, material.dispersion, material.ior, material.thickness, + material.attenuationColor, material.attenuationDistance ); + material.transmissionAlpha = mix( material.transmissionAlpha, transmitted.a, material.transmission ); + totalDiffuse = mix( totalDiffuse, transmitted.rgb, material.transmission ); +#endif`,yf=`#ifdef USE_TRANSMISSION + uniform float transmission; + uniform float thickness; + uniform float attenuationDistance; + uniform vec3 attenuationColor; + #ifdef USE_TRANSMISSIONMAP + uniform sampler2D transmissionMap; + #endif + #ifdef USE_THICKNESSMAP + uniform sampler2D thicknessMap; + #endif + uniform vec2 transmissionSamplerSize; + uniform sampler2D transmissionSamplerMap; + uniform mat4 modelMatrix; + uniform mat4 projectionMatrix; + varying vec3 vWorldPosition; + float w0( float a ) { + return ( 1.0 / 6.0 ) * ( a * ( a * ( - a + 3.0 ) - 3.0 ) + 1.0 ); + } + float w1( float a ) { + return ( 1.0 / 6.0 ) * ( a * a * ( 3.0 * a - 6.0 ) + 4.0 ); + } + float w2( float a ){ + return ( 1.0 / 6.0 ) * ( a * ( a * ( - 3.0 * a + 3.0 ) + 3.0 ) + 1.0 ); + } + float w3( float a ) { + return ( 1.0 / 6.0 ) * ( a * a * a ); + } + float g0( float a ) { + return w0( a ) + w1( a ); + } + float g1( float a ) { + return w2( a ) + w3( a ); + } + float h0( float a ) { + return - 1.0 + w1( a ) / ( w0( a ) + w1( a ) ); + } + float h1( float a ) { + return 1.0 + w3( a ) / ( w2( a ) + w3( a ) ); + } + vec4 bicubic( sampler2D tex, vec2 uv, vec4 texelSize, float lod ) { + uv = uv * texelSize.zw + 0.5; + vec2 iuv = floor( uv ); + vec2 fuv = fract( uv ); + float g0x = g0( fuv.x ); + float g1x = g1( fuv.x ); + float h0x = h0( fuv.x ); + float h1x = h1( fuv.x ); + float h0y = h0( fuv.y ); + float h1y = h1( fuv.y ); + vec2 p0 = ( vec2( iuv.x + h0x, iuv.y + h0y ) - 0.5 ) * texelSize.xy; + vec2 p1 = ( vec2( iuv.x + h1x, iuv.y + h0y ) - 0.5 ) * texelSize.xy; + vec2 p2 = ( vec2( iuv.x + h0x, iuv.y + h1y ) - 0.5 ) * texelSize.xy; + vec2 p3 = ( vec2( iuv.x + h1x, iuv.y + h1y ) - 0.5 ) * texelSize.xy; + return g0( fuv.y ) * ( g0x * textureLod( tex, p0, lod ) + g1x * textureLod( tex, p1, lod ) ) + + g1( fuv.y ) * ( g0x * textureLod( tex, p2, lod ) + g1x * textureLod( tex, p3, lod ) ); + } + vec4 textureBicubic( sampler2D sampler, vec2 uv, float lod ) { + vec2 fLodSize = vec2( textureSize( sampler, int( lod ) ) ); + vec2 cLodSize = vec2( textureSize( sampler, int( lod + 1.0 ) ) ); + vec2 fLodSizeInv = 1.0 / fLodSize; + vec2 cLodSizeInv = 1.0 / cLodSize; + vec4 fSample = bicubic( sampler, uv, vec4( fLodSizeInv, fLodSize ), floor( lod ) ); + vec4 cSample = bicubic( sampler, uv, vec4( cLodSizeInv, cLodSize ), ceil( lod ) ); + return mix( fSample, cSample, fract( lod ) ); + } + vec3 getVolumeTransmissionRay( const in vec3 n, const in vec3 v, const in float thickness, const in float ior, const in mat4 modelMatrix ) { + vec3 refractionVector = refract( - v, normalize( n ), 1.0 / ior ); + vec3 modelScale; + modelScale.x = length( vec3( modelMatrix[ 0 ].xyz ) ); + modelScale.y = length( vec3( modelMatrix[ 1 ].xyz ) ); + modelScale.z = length( vec3( modelMatrix[ 2 ].xyz ) ); + return normalize( refractionVector ) * thickness * modelScale; + } + float applyIorToRoughness( const in float roughness, const in float ior ) { + return roughness * clamp( ior * 2.0 - 2.0, 0.0, 1.0 ); + } + vec4 getTransmissionSample( const in vec2 fragCoord, const in float roughness, const in float ior ) { + float lod = log2( transmissionSamplerSize.x ) * applyIorToRoughness( roughness, ior ); + return textureBicubic( transmissionSamplerMap, fragCoord.xy, lod ); + } + vec3 volumeAttenuation( const in float transmissionDistance, const in vec3 attenuationColor, const in float attenuationDistance ) { + if ( isinf( attenuationDistance ) ) { + return vec3( 1.0 ); + } else { + vec3 attenuationCoefficient = -log( attenuationColor ) / attenuationDistance; + vec3 transmittance = exp( - attenuationCoefficient * transmissionDistance ); return transmittance; + } + } + vec4 getIBLVolumeRefraction( const in vec3 n, const in vec3 v, const in float roughness, const in vec3 diffuseColor, + const in vec3 specularColor, const in float specularF90, const in vec3 position, const in mat4 modelMatrix, + const in mat4 viewMatrix, const in mat4 projMatrix, const in float dispersion, const in float ior, const in float thickness, + const in vec3 attenuationColor, const in float attenuationDistance ) { + vec4 transmittedLight; + vec3 transmittance; + #ifdef USE_DISPERSION + float halfSpread = ( ior - 1.0 ) * 0.025 * dispersion; + vec3 iors = vec3( ior - halfSpread, ior, ior + halfSpread ); + for ( int i = 0; i < 3; i ++ ) { + vec3 transmissionRay = getVolumeTransmissionRay( n, v, thickness, iors[ i ], modelMatrix ); + vec3 refractedRayExit = position + transmissionRay; + vec4 ndcPos = projMatrix * viewMatrix * vec4( refractedRayExit, 1.0 ); + vec2 refractionCoords = ndcPos.xy / ndcPos.w; + refractionCoords += 1.0; + refractionCoords /= 2.0; + vec4 transmissionSample = getTransmissionSample( refractionCoords, roughness, iors[ i ] ); + transmittedLight[ i ] = transmissionSample[ i ]; + transmittedLight.a += transmissionSample.a; + transmittance[ i ] = diffuseColor[ i ] * volumeAttenuation( length( transmissionRay ), attenuationColor, attenuationDistance )[ i ]; + } + transmittedLight.a /= 3.0; + #else + vec3 transmissionRay = getVolumeTransmissionRay( n, v, thickness, ior, modelMatrix ); + vec3 refractedRayExit = position + transmissionRay; + vec4 ndcPos = projMatrix * viewMatrix * vec4( refractedRayExit, 1.0 ); + vec2 refractionCoords = ndcPos.xy / ndcPos.w; + refractionCoords += 1.0; + refractionCoords /= 2.0; + transmittedLight = getTransmissionSample( refractionCoords, roughness, ior ); + transmittance = diffuseColor * volumeAttenuation( length( transmissionRay ), attenuationColor, attenuationDistance ); + #endif + vec3 attenuatedColor = transmittance * transmittedLight.rgb; + vec3 F = EnvironmentBRDF( n, v, specularColor, specularF90, roughness ); + float transmittanceFactor = ( transmittance.r + transmittance.g + transmittance.b ) / 3.0; + return vec4( ( 1.0 - F ) * attenuatedColor, 1.0 - ( 1.0 - transmittedLight.a ) * transmittanceFactor ); + } +#endif`,Ef=`#if defined( USE_UV ) || defined( USE_ANISOTROPY ) + varying vec2 vUv; +#endif +#ifdef USE_MAP + varying vec2 vMapUv; +#endif +#ifdef USE_ALPHAMAP + varying vec2 vAlphaMapUv; +#endif +#ifdef USE_LIGHTMAP + varying vec2 vLightMapUv; +#endif +#ifdef USE_AOMAP + varying vec2 vAoMapUv; +#endif +#ifdef USE_BUMPMAP + varying vec2 vBumpMapUv; +#endif +#ifdef USE_NORMALMAP + varying vec2 vNormalMapUv; +#endif +#ifdef USE_EMISSIVEMAP + varying vec2 vEmissiveMapUv; +#endif +#ifdef USE_METALNESSMAP + varying vec2 vMetalnessMapUv; +#endif +#ifdef USE_ROUGHNESSMAP + varying vec2 vRoughnessMapUv; +#endif +#ifdef USE_ANISOTROPYMAP + varying vec2 vAnisotropyMapUv; +#endif +#ifdef USE_CLEARCOATMAP + varying vec2 vClearcoatMapUv; +#endif +#ifdef USE_CLEARCOAT_NORMALMAP + varying vec2 vClearcoatNormalMapUv; +#endif +#ifdef USE_CLEARCOAT_ROUGHNESSMAP + varying vec2 vClearcoatRoughnessMapUv; +#endif +#ifdef USE_IRIDESCENCEMAP + varying vec2 vIridescenceMapUv; +#endif +#ifdef USE_IRIDESCENCE_THICKNESSMAP + varying vec2 vIridescenceThicknessMapUv; +#endif +#ifdef USE_SHEEN_COLORMAP + varying vec2 vSheenColorMapUv; +#endif +#ifdef USE_SHEEN_ROUGHNESSMAP + varying vec2 vSheenRoughnessMapUv; +#endif +#ifdef USE_SPECULARMAP + varying vec2 vSpecularMapUv; +#endif +#ifdef USE_SPECULAR_COLORMAP + varying vec2 vSpecularColorMapUv; +#endif +#ifdef USE_SPECULAR_INTENSITYMAP + varying vec2 vSpecularIntensityMapUv; +#endif +#ifdef USE_TRANSMISSIONMAP + uniform mat3 transmissionMapTransform; + varying vec2 vTransmissionMapUv; +#endif +#ifdef USE_THICKNESSMAP + uniform mat3 thicknessMapTransform; + varying vec2 vThicknessMapUv; +#endif`,bf=`#if defined( USE_UV ) || defined( USE_ANISOTROPY ) + varying vec2 vUv; +#endif +#ifdef USE_MAP + uniform mat3 mapTransform; + varying vec2 vMapUv; +#endif +#ifdef USE_ALPHAMAP + uniform mat3 alphaMapTransform; + varying vec2 vAlphaMapUv; +#endif +#ifdef USE_LIGHTMAP + uniform mat3 lightMapTransform; + varying vec2 vLightMapUv; +#endif +#ifdef USE_AOMAP + uniform mat3 aoMapTransform; + varying vec2 vAoMapUv; +#endif +#ifdef USE_BUMPMAP + uniform mat3 bumpMapTransform; + varying vec2 vBumpMapUv; +#endif +#ifdef USE_NORMALMAP + uniform mat3 normalMapTransform; + varying vec2 vNormalMapUv; +#endif +#ifdef USE_DISPLACEMENTMAP + uniform mat3 displacementMapTransform; + varying vec2 vDisplacementMapUv; +#endif +#ifdef USE_EMISSIVEMAP + uniform mat3 emissiveMapTransform; + varying vec2 vEmissiveMapUv; +#endif +#ifdef USE_METALNESSMAP + uniform mat3 metalnessMapTransform; + varying vec2 vMetalnessMapUv; +#endif +#ifdef USE_ROUGHNESSMAP + uniform mat3 roughnessMapTransform; + varying vec2 vRoughnessMapUv; +#endif +#ifdef USE_ANISOTROPYMAP + uniform mat3 anisotropyMapTransform; + varying vec2 vAnisotropyMapUv; +#endif +#ifdef USE_CLEARCOATMAP + uniform mat3 clearcoatMapTransform; + varying vec2 vClearcoatMapUv; +#endif +#ifdef USE_CLEARCOAT_NORMALMAP + uniform mat3 clearcoatNormalMapTransform; + varying vec2 vClearcoatNormalMapUv; +#endif +#ifdef USE_CLEARCOAT_ROUGHNESSMAP + uniform mat3 clearcoatRoughnessMapTransform; + varying vec2 vClearcoatRoughnessMapUv; +#endif +#ifdef USE_SHEEN_COLORMAP + uniform mat3 sheenColorMapTransform; + varying vec2 vSheenColorMapUv; +#endif +#ifdef USE_SHEEN_ROUGHNESSMAP + uniform mat3 sheenRoughnessMapTransform; + varying vec2 vSheenRoughnessMapUv; +#endif +#ifdef USE_IRIDESCENCEMAP + uniform mat3 iridescenceMapTransform; + varying vec2 vIridescenceMapUv; +#endif +#ifdef USE_IRIDESCENCE_THICKNESSMAP + uniform mat3 iridescenceThicknessMapTransform; + varying vec2 vIridescenceThicknessMapUv; +#endif +#ifdef USE_SPECULARMAP + uniform mat3 specularMapTransform; + varying vec2 vSpecularMapUv; +#endif +#ifdef USE_SPECULAR_COLORMAP + uniform mat3 specularColorMapTransform; + varying vec2 vSpecularColorMapUv; +#endif +#ifdef USE_SPECULAR_INTENSITYMAP + uniform mat3 specularIntensityMapTransform; + varying vec2 vSpecularIntensityMapUv; +#endif +#ifdef USE_TRANSMISSIONMAP + uniform mat3 transmissionMapTransform; + varying vec2 vTransmissionMapUv; +#endif +#ifdef USE_THICKNESSMAP + uniform mat3 thicknessMapTransform; + varying vec2 vThicknessMapUv; +#endif`,Tf=`#if defined( USE_UV ) || defined( USE_ANISOTROPY ) + vUv = vec3( uv, 1 ).xy; +#endif +#ifdef USE_MAP + vMapUv = ( mapTransform * vec3( MAP_UV, 1 ) ).xy; +#endif +#ifdef USE_ALPHAMAP + vAlphaMapUv = ( alphaMapTransform * vec3( ALPHAMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_LIGHTMAP + vLightMapUv = ( lightMapTransform * vec3( LIGHTMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_AOMAP + vAoMapUv = ( aoMapTransform * vec3( AOMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_BUMPMAP + vBumpMapUv = ( bumpMapTransform * vec3( BUMPMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_NORMALMAP + vNormalMapUv = ( normalMapTransform * vec3( NORMALMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_DISPLACEMENTMAP + vDisplacementMapUv = ( displacementMapTransform * vec3( DISPLACEMENTMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_EMISSIVEMAP + vEmissiveMapUv = ( emissiveMapTransform * vec3( EMISSIVEMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_METALNESSMAP + vMetalnessMapUv = ( metalnessMapTransform * vec3( METALNESSMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_ROUGHNESSMAP + vRoughnessMapUv = ( roughnessMapTransform * vec3( ROUGHNESSMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_ANISOTROPYMAP + vAnisotropyMapUv = ( anisotropyMapTransform * vec3( ANISOTROPYMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_CLEARCOATMAP + vClearcoatMapUv = ( clearcoatMapTransform * vec3( CLEARCOATMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_CLEARCOAT_NORMALMAP + vClearcoatNormalMapUv = ( clearcoatNormalMapTransform * vec3( CLEARCOAT_NORMALMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_CLEARCOAT_ROUGHNESSMAP + vClearcoatRoughnessMapUv = ( clearcoatRoughnessMapTransform * vec3( CLEARCOAT_ROUGHNESSMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_IRIDESCENCEMAP + vIridescenceMapUv = ( iridescenceMapTransform * vec3( IRIDESCENCEMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_IRIDESCENCE_THICKNESSMAP + vIridescenceThicknessMapUv = ( iridescenceThicknessMapTransform * vec3( IRIDESCENCE_THICKNESSMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_SHEEN_COLORMAP + vSheenColorMapUv = ( sheenColorMapTransform * vec3( SHEEN_COLORMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_SHEEN_ROUGHNESSMAP + vSheenRoughnessMapUv = ( sheenRoughnessMapTransform * vec3( SHEEN_ROUGHNESSMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_SPECULARMAP + vSpecularMapUv = ( specularMapTransform * vec3( SPECULARMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_SPECULAR_COLORMAP + vSpecularColorMapUv = ( specularColorMapTransform * vec3( SPECULAR_COLORMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_SPECULAR_INTENSITYMAP + vSpecularIntensityMapUv = ( specularIntensityMapTransform * vec3( SPECULAR_INTENSITYMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_TRANSMISSIONMAP + vTransmissionMapUv = ( transmissionMapTransform * vec3( TRANSMISSIONMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_THICKNESSMAP + vThicknessMapUv = ( thicknessMapTransform * vec3( THICKNESSMAP_UV, 1 ) ).xy; +#endif`,wf=`#if defined( USE_ENVMAP ) || defined( DISTANCE ) || defined ( USE_SHADOWMAP ) || defined ( USE_TRANSMISSION ) || NUM_SPOT_LIGHT_COORDS > 0 + vec4 worldPosition = vec4( transformed, 1.0 ); + #ifdef USE_BATCHING + worldPosition = batchingMatrix * worldPosition; + #endif + #ifdef USE_INSTANCING + worldPosition = instanceMatrix * worldPosition; + #endif + worldPosition = modelMatrix * worldPosition; +#endif`;const Af=`varying vec2 vUv; +uniform mat3 uvTransform; +void main() { + vUv = ( uvTransform * vec3( uv, 1 ) ).xy; + gl_Position = vec4( position.xy, 1.0, 1.0 ); +}`,Rf=`uniform sampler2D t2D; +uniform float backgroundIntensity; +varying vec2 vUv; +void main() { + vec4 texColor = texture2D( t2D, vUv ); + #ifdef DECODE_VIDEO_TEXTURE + texColor = vec4( mix( pow( texColor.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), texColor.rgb * 0.0773993808, vec3( lessThanEqual( texColor.rgb, vec3( 0.04045 ) ) ) ), texColor.w ); + #endif + texColor.rgb *= backgroundIntensity; + gl_FragColor = texColor; + #include + #include +}`,Cf=`varying vec3 vWorldDirection; +#include +void main() { + vWorldDirection = transformDirection( position, modelMatrix ); + #include + #include + gl_Position.z = gl_Position.w; +}`,Pf=`#ifdef ENVMAP_TYPE_CUBE + uniform samplerCube envMap; +#elif defined( ENVMAP_TYPE_CUBE_UV ) + uniform sampler2D envMap; +#endif +uniform float flipEnvMap; +uniform float backgroundBlurriness; +uniform float backgroundIntensity; +uniform mat3 backgroundRotation; +varying vec3 vWorldDirection; +#include +void main() { + #ifdef ENVMAP_TYPE_CUBE + vec4 texColor = textureCube( envMap, backgroundRotation * vec3( flipEnvMap * vWorldDirection.x, vWorldDirection.yz ) ); + #elif defined( ENVMAP_TYPE_CUBE_UV ) + vec4 texColor = textureCubeUV( envMap, backgroundRotation * vWorldDirection, backgroundBlurriness ); + #else + vec4 texColor = vec4( 0.0, 0.0, 0.0, 1.0 ); + #endif + texColor.rgb *= backgroundIntensity; + gl_FragColor = texColor; + #include + #include +}`,Df=`varying vec3 vWorldDirection; +#include +void main() { + vWorldDirection = transformDirection( position, modelMatrix ); + #include + #include + gl_Position.z = gl_Position.w; +}`,Lf=`uniform samplerCube tCube; +uniform float tFlip; +uniform float opacity; +varying vec3 vWorldDirection; +void main() { + vec4 texColor = textureCube( tCube, vec3( tFlip * vWorldDirection.x, vWorldDirection.yz ) ); + gl_FragColor = texColor; + gl_FragColor.a *= opacity; + #include + #include +}`,Uf=`#include +#include +#include +#include +#include +#include +#include +#include +varying vec2 vHighPrecisionZW; +void main() { + #include + #include + #include + #include + #ifdef USE_DISPLACEMENTMAP + #include + #include + #include + #endif + #include + #include + #include + #include + #include + #include + #include + vHighPrecisionZW = gl_Position.zw; +}`,If=`#if DEPTH_PACKING == 3200 + uniform float opacity; +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +varying vec2 vHighPrecisionZW; +void main() { + vec4 diffuseColor = vec4( 1.0 ); + #include + #if DEPTH_PACKING == 3200 + diffuseColor.a = opacity; + #endif + #include + #include + #include + #include + #include + float fragCoordZ = 0.5 * vHighPrecisionZW[0] / vHighPrecisionZW[1] + 0.5; + #if DEPTH_PACKING == 3200 + gl_FragColor = vec4( vec3( 1.0 - fragCoordZ ), opacity ); + #elif DEPTH_PACKING == 3201 + gl_FragColor = packDepthToRGBA( fragCoordZ ); + #elif DEPTH_PACKING == 3202 + gl_FragColor = vec4( packDepthToRGB( fragCoordZ ), 1.0 ); + #elif DEPTH_PACKING == 3203 + gl_FragColor = vec4( packDepthToRG( fragCoordZ ), 0.0, 1.0 ); + #endif +}`,Nf=`#define DISTANCE +varying vec3 vWorldPosition; +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #include + #ifdef USE_DISPLACEMENTMAP + #include + #include + #include + #endif + #include + #include + #include + #include + #include + #include + #include + vWorldPosition = worldPosition.xyz; +}`,Ff=`#define DISTANCE +uniform vec3 referencePosition; +uniform float nearDistance; +uniform float farDistance; +varying vec3 vWorldPosition; +#include +#include +#include +#include +#include +#include +#include +#include +void main () { + vec4 diffuseColor = vec4( 1.0 ); + #include + #include + #include + #include + #include + float dist = length( vWorldPosition - referencePosition ); + dist = ( dist - nearDistance ) / ( farDistance - nearDistance ); + dist = saturate( dist ); + gl_FragColor = packDepthToRGBA( dist ); +}`,Of=`varying vec3 vWorldDirection; +#include +void main() { + vWorldDirection = transformDirection( position, modelMatrix ); + #include + #include +}`,Bf=`uniform sampler2D tEquirect; +varying vec3 vWorldDirection; +#include +void main() { + vec3 direction = normalize( vWorldDirection ); + vec2 sampleUV = equirectUv( direction ); + gl_FragColor = texture2D( tEquirect, sampleUV ); + #include + #include +}`,zf=`uniform float scale; +attribute float lineDistance; +varying float vLineDistance; +#include +#include +#include +#include +#include +#include +#include +void main() { + vLineDistance = scale * lineDistance; + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include +}`,kf=`uniform vec3 diffuse; +uniform float opacity; +uniform float dashSize; +uniform float totalSize; +varying float vLineDistance; +#include +#include +#include +#include +#include +#include +#include +void main() { + vec4 diffuseColor = vec4( diffuse, opacity ); + #include + if ( mod( vLineDistance, totalSize ) > dashSize ) { + discard; + } + vec3 outgoingLight = vec3( 0.0 ); + #include + #include + #include + outgoingLight = diffuseColor.rgb; + #include + #include + #include + #include + #include +}`,Hf=`#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #include + #include + #if defined ( USE_ENVMAP ) || defined ( USE_SKINNING ) + #include + #include + #include + #include + #include + #endif + #include + #include + #include + #include + #include + #include + #include + #include + #include +}`,Vf=`uniform vec3 diffuse; +uniform float opacity; +#ifndef FLAT_SHADED + varying vec3 vNormal; +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + vec4 diffuseColor = vec4( diffuse, opacity ); + #include + #include + #include + #include + #include + #include + #include + #include + ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); + #ifdef USE_LIGHTMAP + vec4 lightMapTexel = texture2D( lightMap, vLightMapUv ); + reflectedLight.indirectDiffuse += lightMapTexel.rgb * lightMapIntensity * RECIPROCAL_PI; + #else + reflectedLight.indirectDiffuse += vec3( 1.0 ); + #endif + #include + reflectedLight.indirectDiffuse *= diffuseColor.rgb; + vec3 outgoingLight = reflectedLight.indirectDiffuse; + #include + #include + #include + #include + #include + #include + #include +}`,Gf=`#define LAMBERT +varying vec3 vViewPosition; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vViewPosition = - mvPosition.xyz; + #include + #include + #include + #include +}`,Wf=`#define LAMBERT +uniform vec3 diffuse; +uniform vec3 emissive; +uniform float opacity; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + vec4 diffuseColor = vec4( diffuse, opacity ); + #include + ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); + vec3 totalEmissiveRadiance = emissive; + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance; + #include + #include + #include + #include + #include + #include + #include +}`,Xf=`#define MATCAP +varying vec3 vViewPosition; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vViewPosition = - mvPosition.xyz; +}`,Yf=`#define MATCAP +uniform vec3 diffuse; +uniform float opacity; +uniform sampler2D matcap; +varying vec3 vViewPosition; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + vec4 diffuseColor = vec4( diffuse, opacity ); + #include + #include + #include + #include + #include + #include + #include + #include + #include + vec3 viewDir = normalize( vViewPosition ); + vec3 x = normalize( vec3( viewDir.z, 0.0, - viewDir.x ) ); + vec3 y = cross( viewDir, x ); + vec2 uv = vec2( dot( x, normal ), dot( y, normal ) ) * 0.495 + 0.5; + #ifdef USE_MATCAP + vec4 matcapColor = texture2D( matcap, uv ); + #else + vec4 matcapColor = vec4( vec3( mix( 0.2, 0.8, uv.y ) ), 1.0 ); + #endif + vec3 outgoingLight = diffuseColor.rgb * matcapColor.rgb; + #include + #include + #include + #include + #include + #include +}`,qf=`#define NORMAL +#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP_TANGENTSPACE ) + varying vec3 vViewPosition; +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include +#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP_TANGENTSPACE ) + vViewPosition = - mvPosition.xyz; +#endif +}`,jf=`#define NORMAL +uniform float opacity; +#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP_TANGENTSPACE ) + varying vec3 vViewPosition; +#endif +#include +#include +#include +#include +#include +#include +#include +void main() { + vec4 diffuseColor = vec4( 0.0, 0.0, 0.0, opacity ); + #include + #include + #include + #include + gl_FragColor = vec4( packNormalToRGB( normal ), diffuseColor.a ); + #ifdef OPAQUE + gl_FragColor.a = 1.0; + #endif +}`,Zf=`#define PHONG +varying vec3 vViewPosition; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vViewPosition = - mvPosition.xyz; + #include + #include + #include + #include +}`,Kf=`#define PHONG +uniform vec3 diffuse; +uniform vec3 emissive; +uniform vec3 specular; +uniform float shininess; +uniform float opacity; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + vec4 diffuseColor = vec4( diffuse, opacity ); + #include + ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); + vec3 totalEmissiveRadiance = emissive; + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance; + #include + #include + #include + #include + #include + #include + #include +}`,$f=`#define STANDARD +varying vec3 vViewPosition; +#ifdef USE_TRANSMISSION + varying vec3 vWorldPosition; +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vViewPosition = - mvPosition.xyz; + #include + #include + #include +#ifdef USE_TRANSMISSION + vWorldPosition = worldPosition.xyz; +#endif +}`,Jf=`#define STANDARD +#ifdef PHYSICAL + #define IOR + #define USE_SPECULAR +#endif +uniform vec3 diffuse; +uniform vec3 emissive; +uniform float roughness; +uniform float metalness; +uniform float opacity; +#ifdef IOR + uniform float ior; +#endif +#ifdef USE_SPECULAR + uniform float specularIntensity; + uniform vec3 specularColor; + #ifdef USE_SPECULAR_COLORMAP + uniform sampler2D specularColorMap; + #endif + #ifdef USE_SPECULAR_INTENSITYMAP + uniform sampler2D specularIntensityMap; + #endif +#endif +#ifdef USE_CLEARCOAT + uniform float clearcoat; + uniform float clearcoatRoughness; +#endif +#ifdef USE_DISPERSION + uniform float dispersion; +#endif +#ifdef USE_IRIDESCENCE + uniform float iridescence; + uniform float iridescenceIOR; + uniform float iridescenceThicknessMinimum; + uniform float iridescenceThicknessMaximum; +#endif +#ifdef USE_SHEEN + uniform vec3 sheenColor; + uniform float sheenRoughness; + #ifdef USE_SHEEN_COLORMAP + uniform sampler2D sheenColorMap; + #endif + #ifdef USE_SHEEN_ROUGHNESSMAP + uniform sampler2D sheenRoughnessMap; + #endif +#endif +#ifdef USE_ANISOTROPY + uniform vec2 anisotropyVector; + #ifdef USE_ANISOTROPYMAP + uniform sampler2D anisotropyMap; + #endif +#endif +varying vec3 vViewPosition; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + vec4 diffuseColor = vec4( diffuse, opacity ); + #include + ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); + vec3 totalEmissiveRadiance = emissive; + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vec3 totalDiffuse = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse; + vec3 totalSpecular = reflectedLight.directSpecular + reflectedLight.indirectSpecular; + #include + vec3 outgoingLight = totalDiffuse + totalSpecular + totalEmissiveRadiance; + #ifdef USE_SHEEN + float sheenEnergyComp = 1.0 - 0.157 * max3( material.sheenColor ); + outgoingLight = outgoingLight * sheenEnergyComp + sheenSpecularDirect + sheenSpecularIndirect; + #endif + #ifdef USE_CLEARCOAT + float dotNVcc = saturate( dot( geometryClearcoatNormal, geometryViewDir ) ); + vec3 Fcc = F_Schlick( material.clearcoatF0, material.clearcoatF90, dotNVcc ); + outgoingLight = outgoingLight * ( 1.0 - material.clearcoat * Fcc ) + ( clearcoatSpecularDirect + clearcoatSpecularIndirect ) * material.clearcoat; + #endif + #include + #include + #include + #include + #include + #include +}`,Qf=`#define TOON +varying vec3 vViewPosition; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vViewPosition = - mvPosition.xyz; + #include + #include + #include +}`,tp=`#define TOON +uniform vec3 diffuse; +uniform vec3 emissive; +uniform float opacity; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + vec4 diffuseColor = vec4( diffuse, opacity ); + #include + ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); + vec3 totalEmissiveRadiance = emissive; + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance; + #include + #include + #include + #include + #include + #include +}`,ep=`uniform float size; +uniform float scale; +#include +#include +#include +#include +#include +#include +#ifdef USE_POINTS_UV + varying vec2 vUv; + uniform mat3 uvTransform; +#endif +void main() { + #ifdef USE_POINTS_UV + vUv = ( uvTransform * vec3( uv, 1 ) ).xy; + #endif + #include + #include + #include + #include + #include + #include + gl_PointSize = size; + #ifdef USE_SIZEATTENUATION + bool isPerspective = isPerspectiveMatrix( projectionMatrix ); + if ( isPerspective ) gl_PointSize *= ( scale / - mvPosition.z ); + #endif + #include + #include + #include + #include +}`,np=`uniform vec3 diffuse; +uniform float opacity; +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + vec4 diffuseColor = vec4( diffuse, opacity ); + #include + vec3 outgoingLight = vec3( 0.0 ); + #include + #include + #include + #include + #include + outgoingLight = diffuseColor.rgb; + #include + #include + #include + #include + #include +}`,ip=`#include +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include +}`,sp=`uniform vec3 color; +uniform float opacity; +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + gl_FragColor = vec4( color, opacity * ( 1.0 - getShadowMask() ) ); + #include + #include + #include +}`,rp=`uniform float rotation; +uniform vec2 center; +#include +#include +#include +#include +#include +void main() { + #include + vec4 mvPosition = modelViewMatrix[ 3 ]; + vec2 scale = vec2( length( modelMatrix[ 0 ].xyz ), length( modelMatrix[ 1 ].xyz ) ); + #ifndef USE_SIZEATTENUATION + bool isPerspective = isPerspectiveMatrix( projectionMatrix ); + if ( isPerspective ) scale *= - mvPosition.z; + #endif + vec2 alignedPosition = ( position.xy - ( center - vec2( 0.5 ) ) ) * scale; + vec2 rotatedPosition; + rotatedPosition.x = cos( rotation ) * alignedPosition.x - sin( rotation ) * alignedPosition.y; + rotatedPosition.y = sin( rotation ) * alignedPosition.x + cos( rotation ) * alignedPosition.y; + mvPosition.xy += rotatedPosition; + gl_Position = projectionMatrix * mvPosition; + #include + #include + #include +}`,ap=`uniform vec3 diffuse; +uniform float opacity; +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + vec4 diffuseColor = vec4( diffuse, opacity ); + #include + vec3 outgoingLight = vec3( 0.0 ); + #include + #include + #include + #include + #include + outgoingLight = diffuseColor.rgb; + #include + #include + #include + #include +}`,Gt={alphahash_fragment:Au,alphahash_pars_fragment:Ru,alphamap_fragment:Cu,alphamap_pars_fragment:Pu,alphatest_fragment:Du,alphatest_pars_fragment:Lu,aomap_fragment:Uu,aomap_pars_fragment:Iu,batching_pars_vertex:Nu,batching_vertex:Fu,begin_vertex:Ou,beginnormal_vertex:Bu,bsdfs:zu,iridescence_fragment:ku,bumpmap_pars_fragment:Hu,clipping_planes_fragment:Vu,clipping_planes_pars_fragment:Gu,clipping_planes_pars_vertex:Wu,clipping_planes_vertex:Xu,color_fragment:Yu,color_pars_fragment:qu,color_pars_vertex:ju,color_vertex:Zu,common:Ku,cube_uv_reflection_fragment:$u,defaultnormal_vertex:Ju,displacementmap_pars_vertex:Qu,displacementmap_vertex:td,emissivemap_fragment:ed,emissivemap_pars_fragment:nd,colorspace_fragment:id,colorspace_pars_fragment:sd,envmap_fragment:rd,envmap_common_pars_fragment:ad,envmap_pars_fragment:od,envmap_pars_vertex:ld,envmap_physical_pars_fragment:xd,envmap_vertex:cd,fog_vertex:hd,fog_pars_vertex:ud,fog_fragment:dd,fog_pars_fragment:fd,gradientmap_pars_fragment:pd,lightmap_pars_fragment:md,lights_lambert_fragment:gd,lights_lambert_pars_fragment:_d,lights_pars_begin:vd,lights_toon_fragment:Md,lights_toon_pars_fragment:Sd,lights_phong_fragment:yd,lights_phong_pars_fragment:Ed,lights_physical_fragment:bd,lights_physical_pars_fragment:Td,lights_fragment_begin:wd,lights_fragment_maps:Ad,lights_fragment_end:Rd,logdepthbuf_fragment:Cd,logdepthbuf_pars_fragment:Pd,logdepthbuf_pars_vertex:Dd,logdepthbuf_vertex:Ld,map_fragment:Ud,map_pars_fragment:Id,map_particle_fragment:Nd,map_particle_pars_fragment:Fd,metalnessmap_fragment:Od,metalnessmap_pars_fragment:Bd,morphinstance_vertex:zd,morphcolor_vertex:kd,morphnormal_vertex:Hd,morphtarget_pars_vertex:Vd,morphtarget_vertex:Gd,normal_fragment_begin:Wd,normal_fragment_maps:Xd,normal_pars_fragment:Yd,normal_pars_vertex:qd,normal_vertex:jd,normalmap_pars_fragment:Zd,clearcoat_normal_fragment_begin:Kd,clearcoat_normal_fragment_maps:$d,clearcoat_pars_fragment:Jd,iridescence_pars_fragment:Qd,opaque_fragment:tf,packing:ef,premultiplied_alpha_fragment:nf,project_vertex:sf,dithering_fragment:rf,dithering_pars_fragment:af,roughnessmap_fragment:of,roughnessmap_pars_fragment:lf,shadowmap_pars_fragment:cf,shadowmap_pars_vertex:hf,shadowmap_vertex:uf,shadowmask_pars_fragment:df,skinbase_vertex:ff,skinning_pars_vertex:pf,skinning_vertex:mf,skinnormal_vertex:gf,specularmap_fragment:_f,specularmap_pars_fragment:vf,tonemapping_fragment:xf,tonemapping_pars_fragment:Mf,transmission_fragment:Sf,transmission_pars_fragment:yf,uv_pars_fragment:Ef,uv_pars_vertex:bf,uv_vertex:Tf,worldpos_vertex:wf,background_vert:Af,background_frag:Rf,backgroundCube_vert:Cf,backgroundCube_frag:Pf,cube_vert:Df,cube_frag:Lf,depth_vert:Uf,depth_frag:If,distanceRGBA_vert:Nf,distanceRGBA_frag:Ff,equirect_vert:Of,equirect_frag:Bf,linedashed_vert:zf,linedashed_frag:kf,meshbasic_vert:Hf,meshbasic_frag:Vf,meshlambert_vert:Gf,meshlambert_frag:Wf,meshmatcap_vert:Xf,meshmatcap_frag:Yf,meshnormal_vert:qf,meshnormal_frag:jf,meshphong_vert:Zf,meshphong_frag:Kf,meshphysical_vert:$f,meshphysical_frag:Jf,meshtoon_vert:Qf,meshtoon_frag:tp,points_vert:ep,points_frag:np,shadow_vert:ip,shadow_frag:sp,sprite_vert:rp,sprite_frag:ap},ct={common:{diffuse:{value:new ot(16777215)},opacity:{value:1},map:{value:null},mapTransform:{value:new Ht},alphaMap:{value:null},alphaMapTransform:{value:new Ht},alphaTest:{value:0}},specularmap:{specularMap:{value:null},specularMapTransform:{value:new Ht}},envmap:{envMap:{value:null},envMapRotation:{value:new Ht},flipEnvMap:{value:-1},reflectivity:{value:1},ior:{value:1.5},refractionRatio:{value:.98}},aomap:{aoMap:{value:null},aoMapIntensity:{value:1},aoMapTransform:{value:new Ht}},lightmap:{lightMap:{value:null},lightMapIntensity:{value:1},lightMapTransform:{value:new Ht}},bumpmap:{bumpMap:{value:null},bumpMapTransform:{value:new Ht},bumpScale:{value:1}},normalmap:{normalMap:{value:null},normalMapTransform:{value:new Ht},normalScale:{value:new St(1,1)}},displacementmap:{displacementMap:{value:null},displacementMapTransform:{value:new Ht},displacementScale:{value:1},displacementBias:{value:0}},emissivemap:{emissiveMap:{value:null},emissiveMapTransform:{value:new Ht}},metalnessmap:{metalnessMap:{value:null},metalnessMapTransform:{value:new Ht}},roughnessmap:{roughnessMap:{value:null},roughnessMapTransform:{value:new Ht}},gradientmap:{gradientMap:{value:null}},fog:{fogDensity:{value:25e-5},fogNear:{value:1},fogFar:{value:2e3},fogColor:{value:new ot(16777215)}},lights:{ambientLightColor:{value:[]},lightProbe:{value:[]},directionalLights:{value:[],properties:{direction:{},color:{}}},directionalLightShadows:{value:[],properties:{shadowIntensity:1,shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{}}},directionalShadowMap:{value:[]},directionalShadowMatrix:{value:[]},spotLights:{value:[],properties:{color:{},position:{},direction:{},distance:{},coneCos:{},penumbraCos:{},decay:{}}},spotLightShadows:{value:[],properties:{shadowIntensity:1,shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{}}},spotLightMap:{value:[]},spotShadowMap:{value:[]},spotLightMatrix:{value:[]},pointLights:{value:[],properties:{color:{},position:{},decay:{},distance:{}}},pointLightShadows:{value:[],properties:{shadowIntensity:1,shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{},shadowCameraNear:{},shadowCameraFar:{}}},pointShadowMap:{value:[]},pointShadowMatrix:{value:[]},hemisphereLights:{value:[],properties:{direction:{},skyColor:{},groundColor:{}}},rectAreaLights:{value:[],properties:{color:{},position:{},width:{},height:{}}},ltc_1:{value:null},ltc_2:{value:null}},points:{diffuse:{value:new ot(16777215)},opacity:{value:1},size:{value:1},scale:{value:1},map:{value:null},alphaMap:{value:null},alphaMapTransform:{value:new Ht},alphaTest:{value:0},uvTransform:{value:new Ht}},sprite:{diffuse:{value:new ot(16777215)},opacity:{value:1},center:{value:new St(.5,.5)},rotation:{value:0},map:{value:null},mapTransform:{value:new Ht},alphaMap:{value:null},alphaMapTransform:{value:new Ht},alphaTest:{value:0}}},mn={basic:{uniforms:ke([ct.common,ct.specularmap,ct.envmap,ct.aomap,ct.lightmap,ct.fog]),vertexShader:Gt.meshbasic_vert,fragmentShader:Gt.meshbasic_frag},lambert:{uniforms:ke([ct.common,ct.specularmap,ct.envmap,ct.aomap,ct.lightmap,ct.emissivemap,ct.bumpmap,ct.normalmap,ct.displacementmap,ct.fog,ct.lights,{emissive:{value:new ot(0)}}]),vertexShader:Gt.meshlambert_vert,fragmentShader:Gt.meshlambert_frag},phong:{uniforms:ke([ct.common,ct.specularmap,ct.envmap,ct.aomap,ct.lightmap,ct.emissivemap,ct.bumpmap,ct.normalmap,ct.displacementmap,ct.fog,ct.lights,{emissive:{value:new ot(0)},specular:{value:new ot(1118481)},shininess:{value:30}}]),vertexShader:Gt.meshphong_vert,fragmentShader:Gt.meshphong_frag},standard:{uniforms:ke([ct.common,ct.envmap,ct.aomap,ct.lightmap,ct.emissivemap,ct.bumpmap,ct.normalmap,ct.displacementmap,ct.roughnessmap,ct.metalnessmap,ct.fog,ct.lights,{emissive:{value:new ot(0)},roughness:{value:1},metalness:{value:0},envMapIntensity:{value:1}}]),vertexShader:Gt.meshphysical_vert,fragmentShader:Gt.meshphysical_frag},toon:{uniforms:ke([ct.common,ct.aomap,ct.lightmap,ct.emissivemap,ct.bumpmap,ct.normalmap,ct.displacementmap,ct.gradientmap,ct.fog,ct.lights,{emissive:{value:new ot(0)}}]),vertexShader:Gt.meshtoon_vert,fragmentShader:Gt.meshtoon_frag},matcap:{uniforms:ke([ct.common,ct.bumpmap,ct.normalmap,ct.displacementmap,ct.fog,{matcap:{value:null}}]),vertexShader:Gt.meshmatcap_vert,fragmentShader:Gt.meshmatcap_frag},points:{uniforms:ke([ct.points,ct.fog]),vertexShader:Gt.points_vert,fragmentShader:Gt.points_frag},dashed:{uniforms:ke([ct.common,ct.fog,{scale:{value:1},dashSize:{value:1},totalSize:{value:2}}]),vertexShader:Gt.linedashed_vert,fragmentShader:Gt.linedashed_frag},depth:{uniforms:ke([ct.common,ct.displacementmap]),vertexShader:Gt.depth_vert,fragmentShader:Gt.depth_frag},normal:{uniforms:ke([ct.common,ct.bumpmap,ct.normalmap,ct.displacementmap,{opacity:{value:1}}]),vertexShader:Gt.meshnormal_vert,fragmentShader:Gt.meshnormal_frag},sprite:{uniforms:ke([ct.sprite,ct.fog]),vertexShader:Gt.sprite_vert,fragmentShader:Gt.sprite_frag},background:{uniforms:{uvTransform:{value:new Ht},t2D:{value:null},backgroundIntensity:{value:1}},vertexShader:Gt.background_vert,fragmentShader:Gt.background_frag},backgroundCube:{uniforms:{envMap:{value:null},flipEnvMap:{value:-1},backgroundBlurriness:{value:0},backgroundIntensity:{value:1},backgroundRotation:{value:new Ht}},vertexShader:Gt.backgroundCube_vert,fragmentShader:Gt.backgroundCube_frag},cube:{uniforms:{tCube:{value:null},tFlip:{value:-1},opacity:{value:1}},vertexShader:Gt.cube_vert,fragmentShader:Gt.cube_frag},equirect:{uniforms:{tEquirect:{value:null}},vertexShader:Gt.equirect_vert,fragmentShader:Gt.equirect_frag},distanceRGBA:{uniforms:ke([ct.common,ct.displacementmap,{referencePosition:{value:new P},nearDistance:{value:1},farDistance:{value:1e3}}]),vertexShader:Gt.distanceRGBA_vert,fragmentShader:Gt.distanceRGBA_frag},shadow:{uniforms:ke([ct.lights,ct.fog,{color:{value:new ot(0)},opacity:{value:1}}]),vertexShader:Gt.shadow_vert,fragmentShader:Gt.shadow_frag}};mn.physical={uniforms:ke([mn.standard.uniforms,{clearcoat:{value:0},clearcoatMap:{value:null},clearcoatMapTransform:{value:new Ht},clearcoatNormalMap:{value:null},clearcoatNormalMapTransform:{value:new Ht},clearcoatNormalScale:{value:new St(1,1)},clearcoatRoughness:{value:0},clearcoatRoughnessMap:{value:null},clearcoatRoughnessMapTransform:{value:new Ht},dispersion:{value:0},iridescence:{value:0},iridescenceMap:{value:null},iridescenceMapTransform:{value:new Ht},iridescenceIOR:{value:1.3},iridescenceThicknessMinimum:{value:100},iridescenceThicknessMaximum:{value:400},iridescenceThicknessMap:{value:null},iridescenceThicknessMapTransform:{value:new Ht},sheen:{value:0},sheenColor:{value:new ot(0)},sheenColorMap:{value:null},sheenColorMapTransform:{value:new Ht},sheenRoughness:{value:1},sheenRoughnessMap:{value:null},sheenRoughnessMapTransform:{value:new Ht},transmission:{value:0},transmissionMap:{value:null},transmissionMapTransform:{value:new Ht},transmissionSamplerSize:{value:new St},transmissionSamplerMap:{value:null},thickness:{value:0},thicknessMap:{value:null},thicknessMapTransform:{value:new Ht},attenuationDistance:{value:0},attenuationColor:{value:new ot(0)},specularColor:{value:new ot(1,1,1)},specularColorMap:{value:null},specularColorMapTransform:{value:new Ht},specularIntensity:{value:1},specularIntensityMap:{value:null},specularIntensityMapTransform:{value:new Ht},anisotropyVector:{value:new St},anisotropyMap:{value:null},anisotropyMapTransform:{value:new Ht}}]),vertexShader:Gt.meshphysical_vert,fragmentShader:Gt.meshphysical_frag};const ar={r:0,b:0,g:0},Qn=new Mn,op=new ne;function lp(i,t,e,n,s,r,a){const o=new ot(0);let l=r===!0?0:1,c,h,d=null,p=0,u=null;function g(E){let y=E.isScene===!0?E.background:null;return y&&y.isTexture&&(y=(E.backgroundBlurriness>0?e:t).get(y)),y}function _(E){let y=!1;const D=g(E);D===null?f(o,l):D&&D.isColor&&(f(D,1),y=!0);const w=i.xr.getEnvironmentBlendMode();w==="additive"?n.buffers.color.setClear(0,0,0,1,a):w==="alpha-blend"&&n.buffers.color.setClear(0,0,0,0,a),(i.autoClear||y)&&(n.buffers.depth.setTest(!0),n.buffers.depth.setMask(!0),n.buffers.color.setMask(!0),i.clear(i.autoClearColor,i.autoClearDepth,i.autoClearStencil))}function m(E,y){const D=g(y);D&&(D.isCubeTexture||D.mapping===Pr)?(h===void 0&&(h=new be(new Ts(1,1,1),new He({name:"BackgroundCubeMaterial",uniforms:Qi(mn.backgroundCube.uniforms),vertexShader:mn.backgroundCube.vertexShader,fragmentShader:mn.backgroundCube.fragmentShader,side:Xe,depthTest:!1,depthWrite:!1,fog:!1})),h.geometry.deleteAttribute("normal"),h.geometry.deleteAttribute("uv"),h.onBeforeRender=function(w,C,I){this.matrixWorld.copyPosition(I.matrixWorld)},Object.defineProperty(h.material,"envMap",{get:function(){return this.uniforms.envMap.value}}),s.update(h)),Qn.copy(y.backgroundRotation),Qn.x*=-1,Qn.y*=-1,Qn.z*=-1,D.isCubeTexture&&D.isRenderTargetTexture===!1&&(Qn.y*=-1,Qn.z*=-1),h.material.uniforms.envMap.value=D,h.material.uniforms.flipEnvMap.value=D.isCubeTexture&&D.isRenderTargetTexture===!1?-1:1,h.material.uniforms.backgroundBlurriness.value=y.backgroundBlurriness,h.material.uniforms.backgroundIntensity.value=y.backgroundIntensity,h.material.uniforms.backgroundRotation.value.setFromMatrix4(op.makeRotationFromEuler(Qn)),h.material.toneMapped=Qt.getTransfer(D.colorSpace)!==se,(d!==D||p!==D.version||u!==i.toneMapping)&&(h.material.needsUpdate=!0,d=D,p=D.version,u=i.toneMapping),h.layers.enableAll(),E.unshift(h,h.geometry,h.material,0,0,null)):D&&D.isTexture&&(c===void 0&&(c=new be(new ws(2,2),new He({name:"BackgroundMaterial",uniforms:Qi(mn.background.uniforms),vertexShader:mn.background.vertexShader,fragmentShader:mn.background.fragmentShader,side:Yn,depthTest:!1,depthWrite:!1,fog:!1})),c.geometry.deleteAttribute("normal"),Object.defineProperty(c.material,"map",{get:function(){return this.uniforms.t2D.value}}),s.update(c)),c.material.uniforms.t2D.value=D,c.material.uniforms.backgroundIntensity.value=y.backgroundIntensity,c.material.toneMapped=Qt.getTransfer(D.colorSpace)!==se,D.matrixAutoUpdate===!0&&D.updateMatrix(),c.material.uniforms.uvTransform.value.copy(D.matrix),(d!==D||p!==D.version||u!==i.toneMapping)&&(c.material.needsUpdate=!0,d=D,p=D.version,u=i.toneMapping),c.layers.enableAll(),E.unshift(c,c.geometry,c.material,0,0,null))}function f(E,y){E.getRGB(ar,Mc(i)),n.buffers.color.setClear(ar.r,ar.g,ar.b,y,a)}function T(){h!==void 0&&(h.geometry.dispose(),h.material.dispose()),c!==void 0&&(c.geometry.dispose(),c.material.dispose())}return{getClearColor:function(){return o},setClearColor:function(E,y=1){o.set(E),l=y,f(o,l)},getClearAlpha:function(){return l},setClearAlpha:function(E){l=E,f(o,l)},render:_,addToRenderList:m,dispose:T}}function cp(i,t){const e=i.getParameter(i.MAX_VERTEX_ATTRIBS),n={},s=p(null);let r=s,a=!1;function o(M,A,W,k,q){let Q=!1;const X=d(k,W,A);r!==X&&(r=X,c(r.object)),Q=u(M,k,W,q),Q&&g(M,k,W,q),q!==null&&t.update(q,i.ELEMENT_ARRAY_BUFFER),(Q||a)&&(a=!1,y(M,A,W,k),q!==null&&i.bindBuffer(i.ELEMENT_ARRAY_BUFFER,t.get(q).buffer))}function l(){return i.createVertexArray()}function c(M){return i.bindVertexArray(M)}function h(M){return i.deleteVertexArray(M)}function d(M,A,W){const k=W.wireframe===!0;let q=n[M.id];q===void 0&&(q={},n[M.id]=q);let Q=q[A.id];Q===void 0&&(Q={},q[A.id]=Q);let X=Q[k];return X===void 0&&(X=p(l()),Q[k]=X),X}function p(M){const A=[],W=[],k=[];for(let q=0;q=0){const gt=q[H];let Et=Q[H];if(Et===void 0&&(H==="instanceMatrix"&&M.instanceMatrix&&(Et=M.instanceMatrix),H==="instanceColor"&&M.instanceColor&&(Et=M.instanceColor)),gt===void 0||gt.attribute!==Et||Et&>.data!==Et.data)return!0;X++}return r.attributesNum!==X||r.index!==k}function g(M,A,W,k){const q={},Q=A.attributes;let X=0;const tt=W.getAttributes();for(const H in tt)if(tt[H].location>=0){let gt=Q[H];gt===void 0&&(H==="instanceMatrix"&&M.instanceMatrix&&(gt=M.instanceMatrix),H==="instanceColor"&&M.instanceColor&&(gt=M.instanceColor));const Et={};Et.attribute=gt,gt&>.data&&(Et.data=gt.data),q[H]=Et,X++}r.attributes=q,r.attributesNum=X,r.index=k}function _(){const M=r.newAttributes;for(let A=0,W=M.length;A=0){let st=q[tt];if(st===void 0&&(tt==="instanceMatrix"&&M.instanceMatrix&&(st=M.instanceMatrix),tt==="instanceColor"&&M.instanceColor&&(st=M.instanceColor)),st!==void 0){const gt=st.normalized,Et=st.itemSize,Ft=t.get(st);if(Ft===void 0)continue;const Xt=Ft.buffer,Y=Ft.type,nt=Ft.bytesPerElement,_t=Y===i.INT||Y===i.UNSIGNED_INT||st.gpuType===xo;if(st.isInterleavedBufferAttribute){const lt=st.data,Ct=lt.stride,Lt=st.offset;if(lt.isInstancedInterleavedBuffer){for(let Vt=0;Vt0&&i.getShaderPrecisionFormat(i.FRAGMENT_SHADER,i.HIGH_FLOAT).precision>0)return"highp";C="mediump"}return C==="mediump"&&i.getShaderPrecisionFormat(i.VERTEX_SHADER,i.MEDIUM_FLOAT).precision>0&&i.getShaderPrecisionFormat(i.FRAGMENT_SHADER,i.MEDIUM_FLOAT).precision>0?"mediump":"lowp"}let c=e.precision!==void 0?e.precision:"highp";const h=l(c);h!==c&&(console.warn("THREE.WebGLRenderer:",c,"not supported, using",h,"instead."),c=h);const d=e.logarithmicDepthBuffer===!0,p=e.reverseDepthBuffer===!0&&t.has("EXT_clip_control"),u=i.getParameter(i.MAX_TEXTURE_IMAGE_UNITS),g=i.getParameter(i.MAX_VERTEX_TEXTURE_IMAGE_UNITS),_=i.getParameter(i.MAX_TEXTURE_SIZE),m=i.getParameter(i.MAX_CUBE_MAP_TEXTURE_SIZE),f=i.getParameter(i.MAX_VERTEX_ATTRIBS),T=i.getParameter(i.MAX_VERTEX_UNIFORM_VECTORS),E=i.getParameter(i.MAX_VARYING_VECTORS),y=i.getParameter(i.MAX_FRAGMENT_UNIFORM_VECTORS),D=g>0,w=i.getParameter(i.MAX_SAMPLES);return{isWebGL2:!0,getMaxAnisotropy:r,getMaxPrecision:l,textureFormatReadable:a,textureTypeReadable:o,precision:c,logarithmicDepthBuffer:d,reverseDepthBuffer:p,maxTextures:u,maxVertexTextures:g,maxTextureSize:_,maxCubemapSize:m,maxAttributes:f,maxVertexUniforms:T,maxVaryings:E,maxFragmentUniforms:y,vertexTextures:D,maxSamples:w}}function dp(i){const t=this;let e=null,n=0,s=!1,r=!1;const a=new Hn,o=new Ht,l={value:null,needsUpdate:!1};this.uniform=l,this.numPlanes=0,this.numIntersection=0,this.init=function(d,p){const u=d.length!==0||p||n!==0||s;return s=p,n=d.length,u},this.beginShadows=function(){r=!0,h(null)},this.endShadows=function(){r=!1},this.setGlobalState=function(d,p){e=h(d,p,0)},this.setState=function(d,p,u){const g=d.clippingPlanes,_=d.clipIntersection,m=d.clipShadows,f=i.get(d);if(!s||g===null||g.length===0||r&&!m)r?h(null):c();else{const T=r?0:n,E=T*4;let y=f.clippingState||null;l.value=y,y=h(g,p,E,u);for(let D=0;D!==E;++D)y[D]=e[D];f.clippingState=y,this.numIntersection=_?this.numPlanes:0,this.numPlanes+=T}};function c(){l.value!==e&&(l.value=e,l.needsUpdate=n>0),t.numPlanes=n,t.numIntersection=0}function h(d,p,u,g){const _=d!==null?d.length:0;let m=null;if(_!==0){if(m=l.value,g!==!0||m===null){const f=u+_*4,T=p.matrixWorldInverse;o.getNormalMatrix(T),(m===null||m.length0){const c=new ou(l.height);return c.fromEquirectangularTexture(i,a),t.set(a,c),a.addEventListener("dispose",s),e(c.texture,a.mapping)}else return null}}return a}function s(a){const o=a.target;o.removeEventListener("dispose",s);const l=t.get(o);l!==void 0&&(t.delete(o),l.dispose())}function r(){t=new WeakMap}return{get:n,dispose:r}}const ki=4,vl=[.125,.215,.35,.446,.526,.582],si=20,ha=new Ac,xl=new ot;let ua=null,da=0,fa=0,pa=!1;const ei=(1+Math.sqrt(5))/2,Ni=1/ei,Ml=[new P(-ei,Ni,0),new P(ei,Ni,0),new P(-Ni,0,ei),new P(Ni,0,ei),new P(0,ei,-Ni),new P(0,ei,Ni),new P(-1,1,-1),new P(1,1,-1),new P(-1,1,1),new P(1,1,1)];class Sl{constructor(t){this._renderer=t,this._pingPongRenderTarget=null,this._lodMax=0,this._cubeSize=0,this._lodPlanes=[],this._sizeLods=[],this._sigmas=[],this._blurMaterial=null,this._cubemapMaterial=null,this._equirectMaterial=null,this._compileMaterial(this._blurMaterial)}fromScene(t,e=0,n=.1,s=100){ua=this._renderer.getRenderTarget(),da=this._renderer.getActiveCubeFace(),fa=this._renderer.getActiveMipmapLevel(),pa=this._renderer.xr.enabled,this._renderer.xr.enabled=!1,this._setSize(256);const r=this._allocateTargets();return r.depthBuffer=!0,this._sceneToCubeUV(t,n,s,r),e>0&&this._blur(r,0,0,e),this._applyPMREM(r),this._cleanup(r),r}fromEquirectangular(t,e=null){return this._fromTexture(t,e)}fromCubemap(t,e=null){return this._fromTexture(t,e)}compileCubemapShader(){this._cubemapMaterial===null&&(this._cubemapMaterial=bl(),this._compileMaterial(this._cubemapMaterial))}compileEquirectangularShader(){this._equirectMaterial===null&&(this._equirectMaterial=El(),this._compileMaterial(this._equirectMaterial))}dispose(){this._dispose(),this._cubemapMaterial!==null&&this._cubemapMaterial.dispose(),this._equirectMaterial!==null&&this._equirectMaterial.dispose()}_setSize(t){this._lodMax=Math.floor(Math.log2(t)),this._cubeSize=Math.pow(2,this._lodMax)}_dispose(){this._blurMaterial!==null&&this._blurMaterial.dispose(),this._pingPongRenderTarget!==null&&this._pingPongRenderTarget.dispose();for(let t=0;t2?E:0,E,E),h.setRenderTarget(s),_&&h.render(g,o),h.render(t,o)}g.geometry.dispose(),g.material.dispose(),h.toneMapping=p,h.autoClear=d,t.background=m}_textureToCubeUV(t,e){const n=this._renderer,s=t.mapping===ji||t.mapping===Zi;s?(this._cubemapMaterial===null&&(this._cubemapMaterial=bl()),this._cubemapMaterial.uniforms.flipEnvMap.value=t.isRenderTargetTexture===!1?-1:1):this._equirectMaterial===null&&(this._equirectMaterial=El());const r=s?this._cubemapMaterial:this._equirectMaterial,a=new be(this._lodPlanes[0],r),o=r.uniforms;o.envMap.value=t;const l=this._cubeSize;or(e,0,0,3*l,2*l),n.setRenderTarget(e),n.render(a,ha)}_applyPMREM(t){const e=this._renderer,n=e.autoClear;e.autoClear=!1;const s=this._lodPlanes.length;for(let r=1;rsi&&console.warn(`sigmaRadians, ${r}, is too large and will clip, as it requested ${m} samples when the maximum is set to ${si}`);const f=[];let T=0;for(let C=0;CE-ki?s-E+ki:0),w=4*(this._cubeSize-y);or(e,D,w,3*y,2*y),l.setRenderTarget(e),l.render(d,ha)}}function pp(i){const t=[],e=[],n=[];let s=i;const r=i-ki+1+vl.length;for(let a=0;ai-ki?l=vl[a-i+ki-1]:a===0&&(l=0),n.push(l);const c=1/(o-2),h=-c,d=1+c,p=[h,h,d,h,d,d,h,h,d,d,h,d],u=6,g=6,_=3,m=2,f=1,T=new Float32Array(_*g*u),E=new Float32Array(m*g*u),y=new Float32Array(f*g*u);for(let w=0;w2?0:-1,S=[C,I,0,C+2/3,I,0,C+2/3,I+1,0,C,I,0,C+2/3,I+1,0,C,I+1,0];T.set(S,_*g*w),E.set(p,m*g*w);const M=[w,w,w,w,w,w];y.set(M,f*g*w)}const D=new ge;D.setAttribute("position",new he(T,_)),D.setAttribute("uv",new he(E,m)),D.setAttribute("faceIndex",new he(y,f)),t.push(D),s>ki&&s--}return{lodPlanes:t,sizeLods:e,sigmas:n}}function yl(i,t,e){const n=new fn(i,t,e);return n.texture.mapping=Pr,n.texture.name="PMREM.cubeUv",n.scissorTest=!0,n}function or(i,t,e,n,s){i.viewport.set(t,e,n,s),i.scissor.set(t,e,n,s)}function mp(i,t,e){const n=new Float32Array(si),s=new P(0,1,0);return new He({name:"SphericalGaussianBlur",defines:{n:si,CUBEUV_TEXEL_WIDTH:1/t,CUBEUV_TEXEL_HEIGHT:1/e,CUBEUV_MAX_MIP:`${i}.0`},uniforms:{envMap:{value:null},samples:{value:1},weights:{value:n},latitudinal:{value:!1},dTheta:{value:0},mipInt:{value:0},poleAxis:{value:s}},vertexShader:Co(),fragmentShader:` + + precision mediump float; + precision mediump int; + + varying vec3 vOutputDirection; + + uniform sampler2D envMap; + uniform int samples; + uniform float weights[ n ]; + uniform bool latitudinal; + uniform float dTheta; + uniform float mipInt; + uniform vec3 poleAxis; + + #define ENVMAP_TYPE_CUBE_UV + #include + + vec3 getSample( float theta, vec3 axis ) { + + float cosTheta = cos( theta ); + // Rodrigues' axis-angle rotation + vec3 sampleDirection = vOutputDirection * cosTheta + + cross( axis, vOutputDirection ) * sin( theta ) + + axis * dot( axis, vOutputDirection ) * ( 1.0 - cosTheta ); + + return bilinearCubeUV( envMap, sampleDirection, mipInt ); + + } + + void main() { + + vec3 axis = latitudinal ? poleAxis : cross( poleAxis, vOutputDirection ); + + if ( all( equal( axis, vec3( 0.0 ) ) ) ) { + + axis = vec3( vOutputDirection.z, 0.0, - vOutputDirection.x ); + + } + + axis = normalize( axis ); + + gl_FragColor = vec4( 0.0, 0.0, 0.0, 1.0 ); + gl_FragColor.rgb += weights[ 0 ] * getSample( 0.0, axis ); + + for ( int i = 1; i < n; i++ ) { + + if ( i >= samples ) { + + break; + + } + + float theta = dTheta * float( i ); + gl_FragColor.rgb += weights[ i ] * getSample( -1.0 * theta, axis ); + gl_FragColor.rgb += weights[ i ] * getSample( theta, axis ); + + } + + } + `,blending:Cn,depthTest:!1,depthWrite:!1})}function El(){return new He({name:"EquirectangularToCubeUV",uniforms:{envMap:{value:null}},vertexShader:Co(),fragmentShader:` + + precision mediump float; + precision mediump int; + + varying vec3 vOutputDirection; + + uniform sampler2D envMap; + + #include + + void main() { + + vec3 outputDirection = normalize( vOutputDirection ); + vec2 uv = equirectUv( outputDirection ); + + gl_FragColor = vec4( texture2D ( envMap, uv ).rgb, 1.0 ); + + } + `,blending:Cn,depthTest:!1,depthWrite:!1})}function bl(){return new He({name:"CubemapToCubeUV",uniforms:{envMap:{value:null},flipEnvMap:{value:-1}},vertexShader:Co(),fragmentShader:` + + precision mediump float; + precision mediump int; + + uniform float flipEnvMap; + + varying vec3 vOutputDirection; + + uniform samplerCube envMap; + + void main() { + + gl_FragColor = textureCube( envMap, vec3( flipEnvMap * vOutputDirection.x, vOutputDirection.yz ) ); + + } + `,blending:Cn,depthTest:!1,depthWrite:!1})}function Co(){return` + + precision mediump float; + precision mediump int; + + attribute float faceIndex; + + varying vec3 vOutputDirection; + + // RH coordinate system; PMREM face-indexing convention + vec3 getDirection( vec2 uv, float face ) { + + uv = 2.0 * uv - 1.0; + + vec3 direction = vec3( uv, 1.0 ); + + if ( face == 0.0 ) { + + direction = direction.zyx; // ( 1, v, u ) pos x + + } else if ( face == 1.0 ) { + + direction = direction.xzy; + direction.xz *= -1.0; // ( -u, 1, -v ) pos y + + } else if ( face == 2.0 ) { + + direction.x *= -1.0; // ( -u, v, 1 ) pos z + + } else if ( face == 3.0 ) { + + direction = direction.zyx; + direction.xz *= -1.0; // ( -1, v, -u ) neg x + + } else if ( face == 4.0 ) { + + direction = direction.xzy; + direction.xy *= -1.0; // ( -u, -1, v ) neg y + + } else if ( face == 5.0 ) { + + direction.z *= -1.0; // ( u, v, -1 ) neg z + + } + + return direction; + + } + + void main() { + + vOutputDirection = getDirection( uv, faceIndex ); + gl_Position = vec4( position, 1.0 ); + + } + `}function gp(i){let t=new WeakMap,e=null;function n(o){if(o&&o.isTexture){const l=o.mapping,c=l===Da||l===La,h=l===ji||l===Zi;if(c||h){let d=t.get(o);const p=d!==void 0?d.texture.pmremVersion:0;if(o.isRenderTargetTexture&&o.pmremVersion!==p)return e===null&&(e=new Sl(i)),d=c?e.fromEquirectangular(o,d):e.fromCubemap(o,d),d.texture.pmremVersion=o.pmremVersion,t.set(o,d),d.texture;if(d!==void 0)return d.texture;{const u=o.image;return c&&u&&u.height>0||h&&u&&s(u)?(e===null&&(e=new Sl(i)),d=c?e.fromEquirectangular(o):e.fromCubemap(o),d.texture.pmremVersion=o.pmremVersion,t.set(o,d),o.addEventListener("dispose",r),d.texture):null}}}return o}function s(o){let l=0;const c=6;for(let h=0;ht.maxTextureSize&&(D=Math.ceil(y/t.maxTextureSize),y=t.maxTextureSize);const w=new Float32Array(y*D*4*d),C=new gc(w,y,D,d);C.type=xn,C.needsUpdate=!0;const I=E*4;for(let M=0;M0)return i;const s=t*e;let r=wl[s];if(r===void 0&&(r=new Float32Array(s),wl[s]=r),t!==0){n.toArray(r,0);for(let a=1,o=0;a!==t;++a)o+=e,i[a].toArray(r,o)}return r}function Te(i,t){if(i.length!==t.length)return!1;for(let e=0,n=i.length;e":" "} ${o}: ${e[a]}`)}return n.join(` +`)}const Ul=new Ht;function gm(i){Qt._getMatrix(Ul,Qt.workingColorSpace,i);const t=`mat3( ${Ul.elements.map(e=>e.toFixed(4))} )`;switch(Qt.getTransfer(i)){case yr:return[t,"LinearTransferOETF"];case se:return[t,"sRGBTransferOETF"];default:return console.warn("THREE.WebGLProgram: Unsupported color space: ",i),[t,"LinearTransferOETF"]}}function Il(i,t,e){const n=i.getShaderParameter(t,i.COMPILE_STATUS),s=i.getShaderInfoLog(t).trim();if(n&&s==="")return"";const r=/ERROR: 0:(\d+)/.exec(s);if(r){const a=parseInt(r[1]);return e.toUpperCase()+` + +`+s+` + +`+mm(i.getShaderSource(t),a)}else return s}function _m(i,t){const e=gm(t);return[`vec4 ${i}( vec4 value ) {`,` return ${e[1]}( vec4( value.rgb * ${e[0]}, value.a ) );`,"}"].join(` +`)}function vm(i,t){let e;switch(t){case vh:e="Linear";break;case xh:e="Reinhard";break;case Mh:e="Cineon";break;case ec:e="ACESFilmic";break;case yh:e="AgX";break;case Eh:e="Neutral";break;case Sh:e="Custom";break;default:console.warn("THREE.WebGLProgram: Unsupported toneMapping:",t),e="Linear"}return"vec3 "+i+"( vec3 color ) { return "+e+"ToneMapping( color ); }"}const lr=new P;function xm(){Qt.getLuminanceCoefficients(lr);const i=lr.x.toFixed(4),t=lr.y.toFixed(4),e=lr.z.toFixed(4);return["float luminance( const in vec3 rgb ) {",` const vec3 weights = vec3( ${i}, ${t}, ${e} );`," return dot( weights, rgb );","}"].join(` +`)}function Mm(i){return[i.extensionClipCullDistance?"#extension GL_ANGLE_clip_cull_distance : require":"",i.extensionMultiDraw?"#extension GL_ANGLE_multi_draw : require":""].filter(gs).join(` +`)}function Sm(i){const t=[];for(const e in i){const n=i[e];n!==!1&&t.push("#define "+e+" "+n)}return t.join(` +`)}function ym(i,t){const e={},n=i.getProgramParameter(t,i.ACTIVE_ATTRIBUTES);for(let s=0;s/gm;function fo(i){return i.replace(Em,Tm)}const bm=new Map;function Tm(i,t){let e=Gt[t];if(e===void 0){const n=bm.get(t);if(n!==void 0)e=Gt[n],console.warn('THREE.WebGLRenderer: Shader chunk "%s" has been deprecated. Use "%s" instead.',t,n);else throw new Error("Can not resolve #include <"+t+">")}return fo(e)}const wm=/#pragma unroll_loop_start\s+for\s*\(\s*int\s+i\s*=\s*(\d+)\s*;\s*i\s*<\s*(\d+)\s*;\s*i\s*\+\+\s*\)\s*{([\s\S]+?)}\s+#pragma unroll_loop_end/g;function Ol(i){return i.replace(wm,Am)}function Am(i,t,e,n){let s="";for(let r=parseInt(t);r0&&(m+=` +`),f=["#define SHADER_TYPE "+e.shaderType,"#define SHADER_NAME "+e.shaderName,g].filter(gs).join(` +`),f.length>0&&(f+=` +`)):(m=[Bl(e),"#define SHADER_TYPE "+e.shaderType,"#define SHADER_NAME "+e.shaderName,g,e.extensionClipCullDistance?"#define USE_CLIP_DISTANCE":"",e.batching?"#define USE_BATCHING":"",e.batchingColor?"#define USE_BATCHING_COLOR":"",e.instancing?"#define USE_INSTANCING":"",e.instancingColor?"#define USE_INSTANCING_COLOR":"",e.instancingMorph?"#define USE_INSTANCING_MORPH":"",e.useFog&&e.fog?"#define USE_FOG":"",e.useFog&&e.fogExp2?"#define FOG_EXP2":"",e.map?"#define USE_MAP":"",e.envMap?"#define USE_ENVMAP":"",e.envMap?"#define "+h:"",e.lightMap?"#define USE_LIGHTMAP":"",e.aoMap?"#define USE_AOMAP":"",e.bumpMap?"#define USE_BUMPMAP":"",e.normalMap?"#define USE_NORMALMAP":"",e.normalMapObjectSpace?"#define USE_NORMALMAP_OBJECTSPACE":"",e.normalMapTangentSpace?"#define USE_NORMALMAP_TANGENTSPACE":"",e.displacementMap?"#define USE_DISPLACEMENTMAP":"",e.emissiveMap?"#define USE_EMISSIVEMAP":"",e.anisotropy?"#define USE_ANISOTROPY":"",e.anisotropyMap?"#define USE_ANISOTROPYMAP":"",e.clearcoatMap?"#define USE_CLEARCOATMAP":"",e.clearcoatRoughnessMap?"#define USE_CLEARCOAT_ROUGHNESSMAP":"",e.clearcoatNormalMap?"#define USE_CLEARCOAT_NORMALMAP":"",e.iridescenceMap?"#define USE_IRIDESCENCEMAP":"",e.iridescenceThicknessMap?"#define USE_IRIDESCENCE_THICKNESSMAP":"",e.specularMap?"#define USE_SPECULARMAP":"",e.specularColorMap?"#define USE_SPECULAR_COLORMAP":"",e.specularIntensityMap?"#define USE_SPECULAR_INTENSITYMAP":"",e.roughnessMap?"#define USE_ROUGHNESSMAP":"",e.metalnessMap?"#define USE_METALNESSMAP":"",e.alphaMap?"#define USE_ALPHAMAP":"",e.alphaHash?"#define USE_ALPHAHASH":"",e.transmission?"#define USE_TRANSMISSION":"",e.transmissionMap?"#define USE_TRANSMISSIONMAP":"",e.thicknessMap?"#define USE_THICKNESSMAP":"",e.sheenColorMap?"#define USE_SHEEN_COLORMAP":"",e.sheenRoughnessMap?"#define USE_SHEEN_ROUGHNESSMAP":"",e.mapUv?"#define MAP_UV "+e.mapUv:"",e.alphaMapUv?"#define ALPHAMAP_UV "+e.alphaMapUv:"",e.lightMapUv?"#define LIGHTMAP_UV "+e.lightMapUv:"",e.aoMapUv?"#define AOMAP_UV "+e.aoMapUv:"",e.emissiveMapUv?"#define EMISSIVEMAP_UV "+e.emissiveMapUv:"",e.bumpMapUv?"#define BUMPMAP_UV "+e.bumpMapUv:"",e.normalMapUv?"#define NORMALMAP_UV "+e.normalMapUv:"",e.displacementMapUv?"#define DISPLACEMENTMAP_UV "+e.displacementMapUv:"",e.metalnessMapUv?"#define METALNESSMAP_UV "+e.metalnessMapUv:"",e.roughnessMapUv?"#define ROUGHNESSMAP_UV "+e.roughnessMapUv:"",e.anisotropyMapUv?"#define ANISOTROPYMAP_UV "+e.anisotropyMapUv:"",e.clearcoatMapUv?"#define CLEARCOATMAP_UV "+e.clearcoatMapUv:"",e.clearcoatNormalMapUv?"#define CLEARCOAT_NORMALMAP_UV "+e.clearcoatNormalMapUv:"",e.clearcoatRoughnessMapUv?"#define CLEARCOAT_ROUGHNESSMAP_UV "+e.clearcoatRoughnessMapUv:"",e.iridescenceMapUv?"#define IRIDESCENCEMAP_UV "+e.iridescenceMapUv:"",e.iridescenceThicknessMapUv?"#define IRIDESCENCE_THICKNESSMAP_UV "+e.iridescenceThicknessMapUv:"",e.sheenColorMapUv?"#define SHEEN_COLORMAP_UV "+e.sheenColorMapUv:"",e.sheenRoughnessMapUv?"#define SHEEN_ROUGHNESSMAP_UV "+e.sheenRoughnessMapUv:"",e.specularMapUv?"#define SPECULARMAP_UV "+e.specularMapUv:"",e.specularColorMapUv?"#define SPECULAR_COLORMAP_UV "+e.specularColorMapUv:"",e.specularIntensityMapUv?"#define SPECULAR_INTENSITYMAP_UV "+e.specularIntensityMapUv:"",e.transmissionMapUv?"#define TRANSMISSIONMAP_UV "+e.transmissionMapUv:"",e.thicknessMapUv?"#define THICKNESSMAP_UV "+e.thicknessMapUv:"",e.vertexTangents&&e.flatShading===!1?"#define USE_TANGENT":"",e.vertexColors?"#define USE_COLOR":"",e.vertexAlphas?"#define USE_COLOR_ALPHA":"",e.vertexUv1s?"#define USE_UV1":"",e.vertexUv2s?"#define USE_UV2":"",e.vertexUv3s?"#define USE_UV3":"",e.pointsUvs?"#define USE_POINTS_UV":"",e.flatShading?"#define FLAT_SHADED":"",e.skinning?"#define USE_SKINNING":"",e.morphTargets?"#define USE_MORPHTARGETS":"",e.morphNormals&&e.flatShading===!1?"#define USE_MORPHNORMALS":"",e.morphColors?"#define USE_MORPHCOLORS":"",e.morphTargetsCount>0?"#define MORPHTARGETS_TEXTURE_STRIDE "+e.morphTextureStride:"",e.morphTargetsCount>0?"#define MORPHTARGETS_COUNT "+e.morphTargetsCount:"",e.doubleSided?"#define DOUBLE_SIDED":"",e.flipSided?"#define FLIP_SIDED":"",e.shadowMapEnabled?"#define USE_SHADOWMAP":"",e.shadowMapEnabled?"#define "+l:"",e.sizeAttenuation?"#define USE_SIZEATTENUATION":"",e.numLightProbes>0?"#define USE_LIGHT_PROBES":"",e.logarithmicDepthBuffer?"#define USE_LOGDEPTHBUF":"",e.reverseDepthBuffer?"#define USE_REVERSEDEPTHBUF":"","uniform mat4 modelMatrix;","uniform mat4 modelViewMatrix;","uniform mat4 projectionMatrix;","uniform mat4 viewMatrix;","uniform mat3 normalMatrix;","uniform vec3 cameraPosition;","uniform bool isOrthographic;","#ifdef USE_INSTANCING"," attribute mat4 instanceMatrix;","#endif","#ifdef USE_INSTANCING_COLOR"," attribute vec3 instanceColor;","#endif","#ifdef USE_INSTANCING_MORPH"," uniform sampler2D morphTexture;","#endif","attribute vec3 position;","attribute vec3 normal;","attribute vec2 uv;","#ifdef USE_UV1"," attribute vec2 uv1;","#endif","#ifdef USE_UV2"," attribute vec2 uv2;","#endif","#ifdef USE_UV3"," attribute vec2 uv3;","#endif","#ifdef USE_TANGENT"," attribute vec4 tangent;","#endif","#if defined( USE_COLOR_ALPHA )"," attribute vec4 color;","#elif defined( USE_COLOR )"," attribute vec3 color;","#endif","#ifdef USE_SKINNING"," attribute vec4 skinIndex;"," attribute vec4 skinWeight;","#endif",` +`].filter(gs).join(` +`),f=[Bl(e),"#define SHADER_TYPE "+e.shaderType,"#define SHADER_NAME "+e.shaderName,g,e.useFog&&e.fog?"#define USE_FOG":"",e.useFog&&e.fogExp2?"#define FOG_EXP2":"",e.alphaToCoverage?"#define ALPHA_TO_COVERAGE":"",e.map?"#define USE_MAP":"",e.matcap?"#define USE_MATCAP":"",e.envMap?"#define USE_ENVMAP":"",e.envMap?"#define "+c:"",e.envMap?"#define "+h:"",e.envMap?"#define "+d:"",p?"#define CUBEUV_TEXEL_WIDTH "+p.texelWidth:"",p?"#define CUBEUV_TEXEL_HEIGHT "+p.texelHeight:"",p?"#define CUBEUV_MAX_MIP "+p.maxMip+".0":"",e.lightMap?"#define USE_LIGHTMAP":"",e.aoMap?"#define USE_AOMAP":"",e.bumpMap?"#define USE_BUMPMAP":"",e.normalMap?"#define USE_NORMALMAP":"",e.normalMapObjectSpace?"#define USE_NORMALMAP_OBJECTSPACE":"",e.normalMapTangentSpace?"#define USE_NORMALMAP_TANGENTSPACE":"",e.emissiveMap?"#define USE_EMISSIVEMAP":"",e.anisotropy?"#define USE_ANISOTROPY":"",e.anisotropyMap?"#define USE_ANISOTROPYMAP":"",e.clearcoat?"#define USE_CLEARCOAT":"",e.clearcoatMap?"#define USE_CLEARCOATMAP":"",e.clearcoatRoughnessMap?"#define USE_CLEARCOAT_ROUGHNESSMAP":"",e.clearcoatNormalMap?"#define USE_CLEARCOAT_NORMALMAP":"",e.dispersion?"#define USE_DISPERSION":"",e.iridescence?"#define USE_IRIDESCENCE":"",e.iridescenceMap?"#define USE_IRIDESCENCEMAP":"",e.iridescenceThicknessMap?"#define USE_IRIDESCENCE_THICKNESSMAP":"",e.specularMap?"#define USE_SPECULARMAP":"",e.specularColorMap?"#define USE_SPECULAR_COLORMAP":"",e.specularIntensityMap?"#define USE_SPECULAR_INTENSITYMAP":"",e.roughnessMap?"#define USE_ROUGHNESSMAP":"",e.metalnessMap?"#define USE_METALNESSMAP":"",e.alphaMap?"#define USE_ALPHAMAP":"",e.alphaTest?"#define USE_ALPHATEST":"",e.alphaHash?"#define USE_ALPHAHASH":"",e.sheen?"#define USE_SHEEN":"",e.sheenColorMap?"#define USE_SHEEN_COLORMAP":"",e.sheenRoughnessMap?"#define USE_SHEEN_ROUGHNESSMAP":"",e.transmission?"#define USE_TRANSMISSION":"",e.transmissionMap?"#define USE_TRANSMISSIONMAP":"",e.thicknessMap?"#define USE_THICKNESSMAP":"",e.vertexTangents&&e.flatShading===!1?"#define USE_TANGENT":"",e.vertexColors||e.instancingColor||e.batchingColor?"#define USE_COLOR":"",e.vertexAlphas?"#define USE_COLOR_ALPHA":"",e.vertexUv1s?"#define USE_UV1":"",e.vertexUv2s?"#define USE_UV2":"",e.vertexUv3s?"#define USE_UV3":"",e.pointsUvs?"#define USE_POINTS_UV":"",e.gradientMap?"#define USE_GRADIENTMAP":"",e.flatShading?"#define FLAT_SHADED":"",e.doubleSided?"#define DOUBLE_SIDED":"",e.flipSided?"#define FLIP_SIDED":"",e.shadowMapEnabled?"#define USE_SHADOWMAP":"",e.shadowMapEnabled?"#define "+l:"",e.premultipliedAlpha?"#define PREMULTIPLIED_ALPHA":"",e.numLightProbes>0?"#define USE_LIGHT_PROBES":"",e.decodeVideoTexture?"#define DECODE_VIDEO_TEXTURE":"",e.decodeVideoTextureEmissive?"#define DECODE_VIDEO_TEXTURE_EMISSIVE":"",e.logarithmicDepthBuffer?"#define USE_LOGDEPTHBUF":"",e.reverseDepthBuffer?"#define USE_REVERSEDEPTHBUF":"","uniform mat4 viewMatrix;","uniform vec3 cameraPosition;","uniform bool isOrthographic;",e.toneMapping!==Wn?"#define TONE_MAPPING":"",e.toneMapping!==Wn?Gt.tonemapping_pars_fragment:"",e.toneMapping!==Wn?vm("toneMapping",e.toneMapping):"",e.dithering?"#define DITHERING":"",e.opaque?"#define OPAQUE":"",Gt.colorspace_pars_fragment,_m("linearToOutputTexel",e.outputColorSpace),xm(),e.useDepthPacking?"#define DEPTH_PACKING "+e.depthPacking:"",` +`].filter(gs).join(` +`)),a=fo(a),a=Nl(a,e),a=Fl(a,e),o=fo(o),o=Nl(o,e),o=Fl(o,e),a=Ol(a),o=Ol(o),e.isRawShaderMaterial!==!0&&(T=`#version 300 es +`,m=[u,"#define attribute in","#define varying out","#define texture2D texture"].join(` +`)+` +`+m,f=["#define varying in",e.glslVersion===Fo?"":"layout(location = 0) out highp vec4 pc_fragColor;",e.glslVersion===Fo?"":"#define gl_FragColor pc_fragColor","#define gl_FragDepthEXT gl_FragDepth","#define texture2D texture","#define textureCube texture","#define texture2DProj textureProj","#define texture2DLodEXT textureLod","#define texture2DProjLodEXT textureProjLod","#define textureCubeLodEXT textureLod","#define texture2DGradEXT textureGrad","#define texture2DProjGradEXT textureProjGrad","#define textureCubeGradEXT textureGrad"].join(` +`)+` +`+f);const E=T+m+a,y=T+f+o,D=Ll(s,s.VERTEX_SHADER,E),w=Ll(s,s.FRAGMENT_SHADER,y);s.attachShader(_,D),s.attachShader(_,w),e.index0AttributeName!==void 0?s.bindAttribLocation(_,0,e.index0AttributeName):e.morphTargets===!0&&s.bindAttribLocation(_,0,"position"),s.linkProgram(_);function C(A){if(i.debug.checkShaderErrors){const W=s.getProgramInfoLog(_).trim(),k=s.getShaderInfoLog(D).trim(),q=s.getShaderInfoLog(w).trim();let Q=!0,X=!0;if(s.getProgramParameter(_,s.LINK_STATUS)===!1)if(Q=!1,typeof i.debug.onShaderError=="function")i.debug.onShaderError(s,_,D,w);else{const tt=Il(s,D,"vertex"),H=Il(s,w,"fragment");console.error("THREE.WebGLProgram: Shader Error "+s.getError()+" - VALIDATE_STATUS "+s.getProgramParameter(_,s.VALIDATE_STATUS)+` + +Material Name: `+A.name+` +Material Type: `+A.type+` + +Program Info Log: `+W+` +`+tt+` +`+H)}else W!==""?console.warn("THREE.WebGLProgram: Program Info Log:",W):(k===""||q==="")&&(X=!1);X&&(A.diagnostics={runnable:Q,programLog:W,vertexShader:{log:k,prefix:m},fragmentShader:{log:q,prefix:f}})}s.deleteShader(D),s.deleteShader(w),I=new _r(s,_),S=ym(s,_)}let I;this.getUniforms=function(){return I===void 0&&C(this),I};let S;this.getAttributes=function(){return S===void 0&&C(this),S};let M=e.rendererExtensionParallelShaderCompile===!1;return this.isReady=function(){return M===!1&&(M=s.getProgramParameter(_,fm)),M},this.destroy=function(){n.releaseStatesOfProgram(this),s.deleteProgram(_),this.program=void 0},this.type=e.shaderType,this.name=e.shaderName,this.id=pm++,this.cacheKey=t,this.usedTimes=1,this.program=_,this.vertexShader=D,this.fragmentShader=w,this}let Im=0;class Nm{constructor(){this.shaderCache=new Map,this.materialCache=new Map}update(t){const e=t.vertexShader,n=t.fragmentShader,s=this._getShaderStage(e),r=this._getShaderStage(n),a=this._getShaderCacheForMaterial(t);return a.has(s)===!1&&(a.add(s),s.usedTimes++),a.has(r)===!1&&(a.add(r),r.usedTimes++),this}remove(t){const e=this.materialCache.get(t);for(const n of e)n.usedTimes--,n.usedTimes===0&&this.shaderCache.delete(n.code);return this.materialCache.delete(t),this}getVertexShaderID(t){return this._getShaderStage(t.vertexShader).id}getFragmentShaderID(t){return this._getShaderStage(t.fragmentShader).id}dispose(){this.shaderCache.clear(),this.materialCache.clear()}_getShaderCacheForMaterial(t){const e=this.materialCache;let n=e.get(t);return n===void 0&&(n=new Set,e.set(t,n)),n}_getShaderStage(t){const e=this.shaderCache;let n=e.get(t);return n===void 0&&(n=new Fm(t),e.set(t,n)),n}}class Fm{constructor(t){this.id=Im++,this.code=t,this.usedTimes=0}}function Om(i,t,e,n,s,r,a){const o=new wo,l=new Nm,c=new Set,h=[],d=s.logarithmicDepthBuffer,p=s.vertexTextures;let u=s.precision;const g={MeshDepthMaterial:"depth",MeshDistanceMaterial:"distanceRGBA",MeshNormalMaterial:"normal",MeshBasicMaterial:"basic",MeshLambertMaterial:"lambert",MeshPhongMaterial:"phong",MeshToonMaterial:"toon",MeshStandardMaterial:"physical",MeshPhysicalMaterial:"physical",MeshMatcapMaterial:"matcap",LineBasicMaterial:"basic",LineDashedMaterial:"dashed",PointsMaterial:"points",ShadowMaterial:"shadow",SpriteMaterial:"sprite"};function _(S){return c.add(S),S===0?"uv":`uv${S}`}function m(S,M,A,W,k){const q=W.fog,Q=k.geometry,X=S.isMeshStandardMaterial?W.environment:null,tt=(S.isMeshStandardMaterial?e:t).get(S.envMap||X),H=tt&&tt.mapping===Pr?tt.image.height:null,st=g[S.type];S.precision!==null&&(u=s.getMaxPrecision(S.precision),u!==S.precision&&console.warn("THREE.WebGLProgram.getParameters:",S.precision,"not supported, using",u,"instead."));const gt=Q.morphAttributes.position||Q.morphAttributes.normal||Q.morphAttributes.color,Et=gt!==void 0?gt.length:0;let Ft=0;Q.morphAttributes.position!==void 0&&(Ft=1),Q.morphAttributes.normal!==void 0&&(Ft=2),Q.morphAttributes.color!==void 0&&(Ft=3);let Xt,Y,nt,_t;if(st){const $t=mn[st];Xt=$t.vertexShader,Y=$t.fragmentShader}else Xt=S.vertexShader,Y=S.fragmentShader,l.update(S),nt=l.getVertexShaderID(S),_t=l.getFragmentShaderID(S);const lt=i.getRenderTarget(),Ct=i.state.buffers.depth.getReversed(),Lt=k.isInstancedMesh===!0,Vt=k.isBatchedMesh===!0,ie=!!S.map,Wt=!!S.matcap,ue=!!tt,R=!!S.aoMap,ye=!!S.lightMap,qt=!!S.bumpMap,jt=!!S.normalMap,At=!!S.displacementMap,le=!!S.emissiveMap,Rt=!!S.metalnessMap,b=!!S.roughnessMap,v=S.anisotropy>0,F=S.clearcoat>0,K=S.dispersion>0,J=S.iridescence>0,j=S.sheen>0,Tt=S.transmission>0,dt=v&&!!S.anisotropyMap,V=F&&!!S.clearcoatMap,at=F&&!!S.clearcoatNormalMap,Z=F&&!!S.clearcoatRoughnessMap,it=J&&!!S.iridescenceMap,pt=J&&!!S.iridescenceThicknessMap,wt=j&&!!S.sheenColorMap,ht=j&&!!S.sheenRoughnessMap,Bt=!!S.specularMap,Ut=!!S.specularColorMap,Zt=!!S.specularIntensityMap,L=Tt&&!!S.transmissionMap,rt=Tt&&!!S.thicknessMap,G=!!S.gradientMap,$=!!S.alphaMap,ft=S.alphaTest>0,ut=!!S.alphaHash,Ot=!!S.extensions;let ce=Wn;S.toneMapped&&(lt===null||lt.isXRRenderTarget===!0)&&(ce=i.toneMapping);const Ae={shaderID:st,shaderType:S.type,shaderName:S.name,vertexShader:Xt,fragmentShader:Y,defines:S.defines,customVertexShaderID:nt,customFragmentShaderID:_t,isRawShaderMaterial:S.isRawShaderMaterial===!0,glslVersion:S.glslVersion,precision:u,batching:Vt,batchingColor:Vt&&k._colorsTexture!==null,instancing:Lt,instancingColor:Lt&&k.instanceColor!==null,instancingMorph:Lt&&k.morphTexture!==null,supportsVertexTextures:p,outputColorSpace:lt===null?i.outputColorSpace:lt.isXRRenderTarget===!0?lt.texture.colorSpace:Ji,alphaToCoverage:!!S.alphaToCoverage,map:ie,matcap:Wt,envMap:ue,envMapMode:ue&&tt.mapping,envMapCubeUVHeight:H,aoMap:R,lightMap:ye,bumpMap:qt,normalMap:jt,displacementMap:p&&At,emissiveMap:le,normalMapObjectSpace:jt&&S.normalMapType===Ah,normalMapTangentSpace:jt&&S.normalMapType===dc,metalnessMap:Rt,roughnessMap:b,anisotropy:v,anisotropyMap:dt,clearcoat:F,clearcoatMap:V,clearcoatNormalMap:at,clearcoatRoughnessMap:Z,dispersion:K,iridescence:J,iridescenceMap:it,iridescenceThicknessMap:pt,sheen:j,sheenColorMap:wt,sheenRoughnessMap:ht,specularMap:Bt,specularColorMap:Ut,specularIntensityMap:Zt,transmission:Tt,transmissionMap:L,thicknessMap:rt,gradientMap:G,opaque:S.transparent===!1&&S.blending===Vi&&S.alphaToCoverage===!1,alphaMap:$,alphaTest:ft,alphaHash:ut,combine:S.combine,mapUv:ie&&_(S.map.channel),aoMapUv:R&&_(S.aoMap.channel),lightMapUv:ye&&_(S.lightMap.channel),bumpMapUv:qt&&_(S.bumpMap.channel),normalMapUv:jt&&_(S.normalMap.channel),displacementMapUv:At&&_(S.displacementMap.channel),emissiveMapUv:le&&_(S.emissiveMap.channel),metalnessMapUv:Rt&&_(S.metalnessMap.channel),roughnessMapUv:b&&_(S.roughnessMap.channel),anisotropyMapUv:dt&&_(S.anisotropyMap.channel),clearcoatMapUv:V&&_(S.clearcoatMap.channel),clearcoatNormalMapUv:at&&_(S.clearcoatNormalMap.channel),clearcoatRoughnessMapUv:Z&&_(S.clearcoatRoughnessMap.channel),iridescenceMapUv:it&&_(S.iridescenceMap.channel),iridescenceThicknessMapUv:pt&&_(S.iridescenceThicknessMap.channel),sheenColorMapUv:wt&&_(S.sheenColorMap.channel),sheenRoughnessMapUv:ht&&_(S.sheenRoughnessMap.channel),specularMapUv:Bt&&_(S.specularMap.channel),specularColorMapUv:Ut&&_(S.specularColorMap.channel),specularIntensityMapUv:Zt&&_(S.specularIntensityMap.channel),transmissionMapUv:L&&_(S.transmissionMap.channel),thicknessMapUv:rt&&_(S.thicknessMap.channel),alphaMapUv:$&&_(S.alphaMap.channel),vertexTangents:!!Q.attributes.tangent&&(jt||v),vertexColors:S.vertexColors,vertexAlphas:S.vertexColors===!0&&!!Q.attributes.color&&Q.attributes.color.itemSize===4,pointsUvs:k.isPoints===!0&&!!Q.attributes.uv&&(ie||$),fog:!!q,useFog:S.fog===!0,fogExp2:!!q&&q.isFogExp2,flatShading:S.flatShading===!0,sizeAttenuation:S.sizeAttenuation===!0,logarithmicDepthBuffer:d,reverseDepthBuffer:Ct,skinning:k.isSkinnedMesh===!0,morphTargets:Q.morphAttributes.position!==void 0,morphNormals:Q.morphAttributes.normal!==void 0,morphColors:Q.morphAttributes.color!==void 0,morphTargetsCount:Et,morphTextureStride:Ft,numDirLights:M.directional.length,numPointLights:M.point.length,numSpotLights:M.spot.length,numSpotLightMaps:M.spotLightMap.length,numRectAreaLights:M.rectArea.length,numHemiLights:M.hemi.length,numDirLightShadows:M.directionalShadowMap.length,numPointLightShadows:M.pointShadowMap.length,numSpotLightShadows:M.spotShadowMap.length,numSpotLightShadowsWithMaps:M.numSpotLightShadowsWithMaps,numLightProbes:M.numLightProbes,numClippingPlanes:a.numPlanes,numClipIntersection:a.numIntersection,dithering:S.dithering,shadowMapEnabled:i.shadowMap.enabled&&A.length>0,shadowMapType:i.shadowMap.type,toneMapping:ce,decodeVideoTexture:ie&&S.map.isVideoTexture===!0&&Qt.getTransfer(S.map.colorSpace)===se,decodeVideoTextureEmissive:le&&S.emissiveMap.isVideoTexture===!0&&Qt.getTransfer(S.emissiveMap.colorSpace)===se,premultipliedAlpha:S.premultipliedAlpha,doubleSided:S.side===gn,flipSided:S.side===Xe,useDepthPacking:S.depthPacking>=0,depthPacking:S.depthPacking||0,index0AttributeName:S.index0AttributeName,extensionClipCullDistance:Ot&&S.extensions.clipCullDistance===!0&&n.has("WEBGL_clip_cull_distance"),extensionMultiDraw:(Ot&&S.extensions.multiDraw===!0||Vt)&&n.has("WEBGL_multi_draw"),rendererExtensionParallelShaderCompile:n.has("KHR_parallel_shader_compile"),customProgramCacheKey:S.customProgramCacheKey()};return Ae.vertexUv1s=c.has(1),Ae.vertexUv2s=c.has(2),Ae.vertexUv3s=c.has(3),c.clear(),Ae}function f(S){const M=[];if(S.shaderID?M.push(S.shaderID):(M.push(S.customVertexShaderID),M.push(S.customFragmentShaderID)),S.defines!==void 0)for(const A in S.defines)M.push(A),M.push(S.defines[A]);return S.isRawShaderMaterial===!1&&(T(M,S),E(M,S),M.push(i.outputColorSpace)),M.push(S.customProgramCacheKey),M.join()}function T(S,M){S.push(M.precision),S.push(M.outputColorSpace),S.push(M.envMapMode),S.push(M.envMapCubeUVHeight),S.push(M.mapUv),S.push(M.alphaMapUv),S.push(M.lightMapUv),S.push(M.aoMapUv),S.push(M.bumpMapUv),S.push(M.normalMapUv),S.push(M.displacementMapUv),S.push(M.emissiveMapUv),S.push(M.metalnessMapUv),S.push(M.roughnessMapUv),S.push(M.anisotropyMapUv),S.push(M.clearcoatMapUv),S.push(M.clearcoatNormalMapUv),S.push(M.clearcoatRoughnessMapUv),S.push(M.iridescenceMapUv),S.push(M.iridescenceThicknessMapUv),S.push(M.sheenColorMapUv),S.push(M.sheenRoughnessMapUv),S.push(M.specularMapUv),S.push(M.specularColorMapUv),S.push(M.specularIntensityMapUv),S.push(M.transmissionMapUv),S.push(M.thicknessMapUv),S.push(M.combine),S.push(M.fogExp2),S.push(M.sizeAttenuation),S.push(M.morphTargetsCount),S.push(M.morphAttributeCount),S.push(M.numDirLights),S.push(M.numPointLights),S.push(M.numSpotLights),S.push(M.numSpotLightMaps),S.push(M.numHemiLights),S.push(M.numRectAreaLights),S.push(M.numDirLightShadows),S.push(M.numPointLightShadows),S.push(M.numSpotLightShadows),S.push(M.numSpotLightShadowsWithMaps),S.push(M.numLightProbes),S.push(M.shadowMapType),S.push(M.toneMapping),S.push(M.numClippingPlanes),S.push(M.numClipIntersection),S.push(M.depthPacking)}function E(S,M){o.disableAll(),M.supportsVertexTextures&&o.enable(0),M.instancing&&o.enable(1),M.instancingColor&&o.enable(2),M.instancingMorph&&o.enable(3),M.matcap&&o.enable(4),M.envMap&&o.enable(5),M.normalMapObjectSpace&&o.enable(6),M.normalMapTangentSpace&&o.enable(7),M.clearcoat&&o.enable(8),M.iridescence&&o.enable(9),M.alphaTest&&o.enable(10),M.vertexColors&&o.enable(11),M.vertexAlphas&&o.enable(12),M.vertexUv1s&&o.enable(13),M.vertexUv2s&&o.enable(14),M.vertexUv3s&&o.enable(15),M.vertexTangents&&o.enable(16),M.anisotropy&&o.enable(17),M.alphaHash&&o.enable(18),M.batching&&o.enable(19),M.dispersion&&o.enable(20),M.batchingColor&&o.enable(21),S.push(o.mask),o.disableAll(),M.fog&&o.enable(0),M.useFog&&o.enable(1),M.flatShading&&o.enable(2),M.logarithmicDepthBuffer&&o.enable(3),M.reverseDepthBuffer&&o.enable(4),M.skinning&&o.enable(5),M.morphTargets&&o.enable(6),M.morphNormals&&o.enable(7),M.morphColors&&o.enable(8),M.premultipliedAlpha&&o.enable(9),M.shadowMapEnabled&&o.enable(10),M.doubleSided&&o.enable(11),M.flipSided&&o.enable(12),M.useDepthPacking&&o.enable(13),M.dithering&&o.enable(14),M.transmission&&o.enable(15),M.sheen&&o.enable(16),M.opaque&&o.enable(17),M.pointsUvs&&o.enable(18),M.decodeVideoTexture&&o.enable(19),M.decodeVideoTextureEmissive&&o.enable(20),M.alphaToCoverage&&o.enable(21),S.push(o.mask)}function y(S){const M=g[S.type];let A;if(M){const W=mn[M];A=Tr.clone(W.uniforms)}else A=S.uniforms;return A}function D(S,M){let A;for(let W=0,k=h.length;W0?n.push(f):u.transparent===!0?s.push(f):e.push(f)}function l(d,p,u,g,_,m){const f=a(d,p,u,g,_,m);u.transmission>0?n.unshift(f):u.transparent===!0?s.unshift(f):e.unshift(f)}function c(d,p){e.length>1&&e.sort(d||zm),n.length>1&&n.sort(p||zl),s.length>1&&s.sort(p||zl)}function h(){for(let d=t,p=i.length;d=r.length?(a=new kl,r.push(a)):a=r[s],a}function e(){i=new WeakMap}return{get:t,dispose:e}}function Hm(){const i={};return{get:function(t){if(i[t.id]!==void 0)return i[t.id];let e;switch(t.type){case"DirectionalLight":e={direction:new P,color:new ot};break;case"SpotLight":e={position:new P,direction:new P,color:new ot,distance:0,coneCos:0,penumbraCos:0,decay:0};break;case"PointLight":e={position:new P,color:new ot,distance:0,decay:0};break;case"HemisphereLight":e={direction:new P,skyColor:new ot,groundColor:new ot};break;case"RectAreaLight":e={color:new ot,position:new P,halfWidth:new P,halfHeight:new P};break}return i[t.id]=e,e}}}function Vm(){const i={};return{get:function(t){if(i[t.id]!==void 0)return i[t.id];let e;switch(t.type){case"DirectionalLight":e={shadowIntensity:1,shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new St};break;case"SpotLight":e={shadowIntensity:1,shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new St};break;case"PointLight":e={shadowIntensity:1,shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new St,shadowCameraNear:1,shadowCameraFar:1e3};break}return i[t.id]=e,e}}}let Gm=0;function Wm(i,t){return(t.castShadow?2:0)-(i.castShadow?2:0)+(t.map?1:0)-(i.map?1:0)}function Xm(i){const t=new Hm,e=Vm(),n={version:0,hash:{directionalLength:-1,pointLength:-1,spotLength:-1,rectAreaLength:-1,hemiLength:-1,numDirectionalShadows:-1,numPointShadows:-1,numSpotShadows:-1,numSpotMaps:-1,numLightProbes:-1},ambient:[0,0,0],probe:[],directional:[],directionalShadow:[],directionalShadowMap:[],directionalShadowMatrix:[],spot:[],spotLightMap:[],spotShadow:[],spotShadowMap:[],spotLightMatrix:[],rectArea:[],rectAreaLTC1:null,rectAreaLTC2:null,point:[],pointShadow:[],pointShadowMap:[],pointShadowMatrix:[],hemi:[],numSpotLightShadowsWithMaps:0,numLightProbes:0};for(let c=0;c<9;c++)n.probe.push(new P);const s=new P,r=new ne,a=new ne;function o(c){let h=0,d=0,p=0;for(let S=0;S<9;S++)n.probe[S].set(0,0,0);let u=0,g=0,_=0,m=0,f=0,T=0,E=0,y=0,D=0,w=0,C=0;c.sort(Wm);for(let S=0,M=c.length;S0&&(i.has("OES_texture_float_linear")===!0?(n.rectAreaLTC1=ct.LTC_FLOAT_1,n.rectAreaLTC2=ct.LTC_FLOAT_2):(n.rectAreaLTC1=ct.LTC_HALF_1,n.rectAreaLTC2=ct.LTC_HALF_2)),n.ambient[0]=h,n.ambient[1]=d,n.ambient[2]=p;const I=n.hash;(I.directionalLength!==u||I.pointLength!==g||I.spotLength!==_||I.rectAreaLength!==m||I.hemiLength!==f||I.numDirectionalShadows!==T||I.numPointShadows!==E||I.numSpotShadows!==y||I.numSpotMaps!==D||I.numLightProbes!==C)&&(n.directional.length=u,n.spot.length=_,n.rectArea.length=m,n.point.length=g,n.hemi.length=f,n.directionalShadow.length=T,n.directionalShadowMap.length=T,n.pointShadow.length=E,n.pointShadowMap.length=E,n.spotShadow.length=y,n.spotShadowMap.length=y,n.directionalShadowMatrix.length=T,n.pointShadowMatrix.length=E,n.spotLightMatrix.length=y+D-w,n.spotLightMap.length=D,n.numSpotLightShadowsWithMaps=w,n.numLightProbes=C,I.directionalLength=u,I.pointLength=g,I.spotLength=_,I.rectAreaLength=m,I.hemiLength=f,I.numDirectionalShadows=T,I.numPointShadows=E,I.numSpotShadows=y,I.numSpotMaps=D,I.numLightProbes=C,n.version=Gm++)}function l(c,h){let d=0,p=0,u=0,g=0,_=0;const m=h.matrixWorldInverse;for(let f=0,T=c.length;f=a.length?(o=new Hl(i),a.push(o)):o=a[r],o}function n(){t=new WeakMap}return{get:e,dispose:n}}const qm=`void main() { + gl_Position = vec4( position, 1.0 ); +}`,jm=`uniform sampler2D shadow_pass; +uniform vec2 resolution; +uniform float radius; +#include +void main() { + const float samples = float( VSM_SAMPLES ); + float mean = 0.0; + float squared_mean = 0.0; + float uvStride = samples <= 1.0 ? 0.0 : 2.0 / ( samples - 1.0 ); + float uvStart = samples <= 1.0 ? 0.0 : - 1.0; + for ( float i = 0.0; i < samples; i ++ ) { + float uvOffset = uvStart + i * uvStride; + #ifdef HORIZONTAL_PASS + vec2 distribution = unpackRGBATo2Half( texture2D( shadow_pass, ( gl_FragCoord.xy + vec2( uvOffset, 0.0 ) * radius ) / resolution ) ); + mean += distribution.x; + squared_mean += distribution.y * distribution.y + distribution.x * distribution.x; + #else + float depth = unpackRGBAToDepth( texture2D( shadow_pass, ( gl_FragCoord.xy + vec2( 0.0, uvOffset ) * radius ) / resolution ) ); + mean += depth; + squared_mean += depth * depth; + #endif + } + mean = mean / samples; + squared_mean = squared_mean / samples; + float std_dev = sqrt( squared_mean - mean * mean ); + gl_FragColor = pack2HalfToRGBA( vec2( mean, std_dev ) ); +}`;function Zm(i,t,e){let n=new Ao;const s=new St,r=new St,a=new oe,o=new gu({depthPacking:wh}),l=new _u,c={},h=e.maxTextureSize,d={[Yn]:Xe,[Xe]:Yn,[gn]:gn},p=new He({defines:{VSM_SAMPLES:8},uniforms:{shadow_pass:{value:null},resolution:{value:new St},radius:{value:4}},vertexShader:qm,fragmentShader:jm}),u=p.clone();u.defines.HORIZONTAL_PASS=1;const g=new ge;g.setAttribute("position",new he(new Float32Array([-1,-1,.5,3,-1,.5,-1,3,.5]),3));const _=new be(g,p),m=this;this.enabled=!1,this.autoUpdate=!0,this.needsUpdate=!1,this.type=Ql;let f=this.type;this.render=function(w,C,I){if(m.enabled===!1||m.autoUpdate===!1&&m.needsUpdate===!1||w.length===0)return;const S=i.getRenderTarget(),M=i.getActiveCubeFace(),A=i.getActiveMipmapLevel(),W=i.state;W.setBlending(Cn),W.buffers.color.setClear(1,1,1,1),W.buffers.depth.setTest(!0),W.setScissorTest(!1);const k=f!==An&&this.type===An,q=f===An&&this.type!==An;for(let Q=0,X=w.length;Qh||s.y>h)&&(s.x>h&&(r.x=Math.floor(h/st.x),s.x=r.x*st.x,H.mapSize.x=r.x),s.y>h&&(r.y=Math.floor(h/st.y),s.y=r.y*st.y,H.mapSize.y=r.y)),H.map===null||k===!0||q===!0){const Et=this.type!==An?{minFilter:Je,magFilter:Je}:{};H.map!==null&&H.map.dispose(),H.map=new fn(s.x,s.y,Et),H.map.texture.name=tt.name+".shadowMap",H.camera.updateProjectionMatrix()}i.setRenderTarget(H.map),i.clear();const gt=H.getViewportCount();for(let Et=0;Et0||C.map&&C.alphaTest>0){const W=M.uuid,k=C.uuid;let q=c[W];q===void 0&&(q={},c[W]=q);let Q=q[k];Q===void 0&&(Q=M.clone(),q[k]=Q,C.addEventListener("dispose",D)),M=Q}if(M.visible=C.visible,M.wireframe=C.wireframe,S===An?M.side=C.shadowSide!==null?C.shadowSide:C.side:M.side=C.shadowSide!==null?C.shadowSide:d[C.side],M.alphaMap=C.alphaMap,M.alphaTest=C.alphaTest,M.map=C.map,M.clipShadows=C.clipShadows,M.clippingPlanes=C.clippingPlanes,M.clipIntersection=C.clipIntersection,M.displacementMap=C.displacementMap,M.displacementScale=C.displacementScale,M.displacementBias=C.displacementBias,M.wireframeLinewidth=C.wireframeLinewidth,M.linewidth=C.linewidth,I.isPointLight===!0&&M.isMeshDistanceMaterial===!0){const W=i.properties.get(M);W.light=I}return M}function y(w,C,I,S,M){if(w.visible===!1)return;if(w.layers.test(C.layers)&&(w.isMesh||w.isLine||w.isPoints)&&(w.castShadow||w.receiveShadow&&M===An)&&(!w.frustumCulled||n.intersectsObject(w))){w.modelViewMatrix.multiplyMatrices(I.matrixWorldInverse,w.matrixWorld);const k=t.update(w),q=w.material;if(Array.isArray(q)){const Q=k.groups;for(let X=0,tt=Q.length;X=1):H.indexOf("OpenGL ES")!==-1&&(tt=parseFloat(/^OpenGL ES (\d)/.exec(H)[1]),X=tt>=2);let st=null,gt={};const Et=i.getParameter(i.SCISSOR_BOX),Ft=i.getParameter(i.VIEWPORT),Xt=new oe().fromArray(Et),Y=new oe().fromArray(Ft);function nt(L,rt,G,$){const ft=new Uint8Array(4),ut=i.createTexture();i.bindTexture(L,ut),i.texParameteri(L,i.TEXTURE_MIN_FILTER,i.NEAREST),i.texParameteri(L,i.TEXTURE_MAG_FILTER,i.NEAREST);for(let Ot=0;Ot"u"?!1:/OculusBrowser/g.test(navigator.userAgent),c=new St,h=new WeakMap;let d;const p=new WeakMap;let u=!1;try{u=typeof OffscreenCanvas<"u"&&new OffscreenCanvas(1,1).getContext("2d")!==null}catch{}function g(b,v){return u?new OffscreenCanvas(b,v):br("canvas")}function _(b,v,F){let K=1;const J=Rt(b);if((J.width>F||J.height>F)&&(K=F/Math.max(J.width,J.height)),K<1)if(typeof HTMLImageElement<"u"&&b instanceof HTMLImageElement||typeof HTMLCanvasElement<"u"&&b instanceof HTMLCanvasElement||typeof ImageBitmap<"u"&&b instanceof ImageBitmap||typeof VideoFrame<"u"&&b instanceof VideoFrame){const j=Math.floor(K*J.width),Tt=Math.floor(K*J.height);d===void 0&&(d=g(j,Tt));const dt=v?g(j,Tt):d;return dt.width=j,dt.height=Tt,dt.getContext("2d").drawImage(b,0,0,j,Tt),console.warn("THREE.WebGLRenderer: Texture has been resized from ("+J.width+"x"+J.height+") to ("+j+"x"+Tt+")."),dt}else return"data"in b&&console.warn("THREE.WebGLRenderer: Image in DataTexture is too big ("+J.width+"x"+J.height+")."),b;return b}function m(b){return b.generateMipmaps}function f(b){i.generateMipmap(b)}function T(b){return b.isWebGLCubeRenderTarget?i.TEXTURE_CUBE_MAP:b.isWebGL3DRenderTarget?i.TEXTURE_3D:b.isWebGLArrayRenderTarget||b.isCompressedArrayTexture?i.TEXTURE_2D_ARRAY:i.TEXTURE_2D}function E(b,v,F,K,J=!1){if(b!==null){if(i[b]!==void 0)return i[b];console.warn("THREE.WebGLRenderer: Attempt to use non-existing WebGL internal format '"+b+"'")}let j=v;if(v===i.RED&&(F===i.FLOAT&&(j=i.R32F),F===i.HALF_FLOAT&&(j=i.R16F),F===i.UNSIGNED_BYTE&&(j=i.R8)),v===i.RED_INTEGER&&(F===i.UNSIGNED_BYTE&&(j=i.R8UI),F===i.UNSIGNED_SHORT&&(j=i.R16UI),F===i.UNSIGNED_INT&&(j=i.R32UI),F===i.BYTE&&(j=i.R8I),F===i.SHORT&&(j=i.R16I),F===i.INT&&(j=i.R32I)),v===i.RG&&(F===i.FLOAT&&(j=i.RG32F),F===i.HALF_FLOAT&&(j=i.RG16F),F===i.UNSIGNED_BYTE&&(j=i.RG8)),v===i.RG_INTEGER&&(F===i.UNSIGNED_BYTE&&(j=i.RG8UI),F===i.UNSIGNED_SHORT&&(j=i.RG16UI),F===i.UNSIGNED_INT&&(j=i.RG32UI),F===i.BYTE&&(j=i.RG8I),F===i.SHORT&&(j=i.RG16I),F===i.INT&&(j=i.RG32I)),v===i.RGB_INTEGER&&(F===i.UNSIGNED_BYTE&&(j=i.RGB8UI),F===i.UNSIGNED_SHORT&&(j=i.RGB16UI),F===i.UNSIGNED_INT&&(j=i.RGB32UI),F===i.BYTE&&(j=i.RGB8I),F===i.SHORT&&(j=i.RGB16I),F===i.INT&&(j=i.RGB32I)),v===i.RGBA_INTEGER&&(F===i.UNSIGNED_BYTE&&(j=i.RGBA8UI),F===i.UNSIGNED_SHORT&&(j=i.RGBA16UI),F===i.UNSIGNED_INT&&(j=i.RGBA32UI),F===i.BYTE&&(j=i.RGBA8I),F===i.SHORT&&(j=i.RGBA16I),F===i.INT&&(j=i.RGBA32I)),v===i.RGB&&F===i.UNSIGNED_INT_5_9_9_9_REV&&(j=i.RGB9_E5),v===i.RGBA){const Tt=J?yr:Qt.getTransfer(K);F===i.FLOAT&&(j=i.RGBA32F),F===i.HALF_FLOAT&&(j=i.RGBA16F),F===i.UNSIGNED_BYTE&&(j=Tt===se?i.SRGB8_ALPHA8:i.RGBA8),F===i.UNSIGNED_SHORT_4_4_4_4&&(j=i.RGBA4),F===i.UNSIGNED_SHORT_5_5_5_1&&(j=i.RGB5_A1)}return(j===i.R16F||j===i.R32F||j===i.RG16F||j===i.RG32F||j===i.RGBA16F||j===i.RGBA32F)&&t.get("EXT_color_buffer_float"),j}function y(b,v){let F;return b?v===null||v===li||v===Ki?F=i.DEPTH24_STENCIL8:v===xn?F=i.DEPTH32F_STENCIL8:v===Ms&&(F=i.DEPTH24_STENCIL8,console.warn("DepthTexture: 16 bit depth attachment is not supported with stencil. Using 24-bit attachment.")):v===null||v===li||v===Ki?F=i.DEPTH_COMPONENT24:v===xn?F=i.DEPTH_COMPONENT32F:v===Ms&&(F=i.DEPTH_COMPONENT16),F}function D(b,v){return m(b)===!0||b.isFramebufferTexture&&b.minFilter!==Je&&b.minFilter!==vn?Math.log2(Math.max(v.width,v.height))+1:b.mipmaps!==void 0&&b.mipmaps.length>0?b.mipmaps.length:b.isCompressedTexture&&Array.isArray(b.image)?v.mipmaps.length:1}function w(b){const v=b.target;v.removeEventListener("dispose",w),I(v),v.isVideoTexture&&h.delete(v)}function C(b){const v=b.target;v.removeEventListener("dispose",C),M(v)}function I(b){const v=n.get(b);if(v.__webglInit===void 0)return;const F=b.source,K=p.get(F);if(K){const J=K[v.__cacheKey];J.usedTimes--,J.usedTimes===0&&S(b),Object.keys(K).length===0&&p.delete(F)}n.remove(b)}function S(b){const v=n.get(b);i.deleteTexture(v.__webglTexture);const F=b.source,K=p.get(F);delete K[v.__cacheKey],a.memory.textures--}function M(b){const v=n.get(b);if(b.depthTexture&&(b.depthTexture.dispose(),n.remove(b.depthTexture)),b.isWebGLCubeRenderTarget)for(let K=0;K<6;K++){if(Array.isArray(v.__webglFramebuffer[K]))for(let J=0;J=s.maxTextures&&console.warn("THREE.WebGLTextures: Trying to use "+b+" texture units while this GPU supports only "+s.maxTextures),A+=1,b}function q(b){const v=[];return v.push(b.wrapS),v.push(b.wrapT),v.push(b.wrapR||0),v.push(b.magFilter),v.push(b.minFilter),v.push(b.anisotropy),v.push(b.internalFormat),v.push(b.format),v.push(b.type),v.push(b.generateMipmaps),v.push(b.premultiplyAlpha),v.push(b.flipY),v.push(b.unpackAlignment),v.push(b.colorSpace),v.join()}function Q(b,v){const F=n.get(b);if(b.isVideoTexture&&At(b),b.isRenderTargetTexture===!1&&b.version>0&&F.__version!==b.version){const K=b.image;if(K===null)console.warn("THREE.WebGLRenderer: Texture marked for update but no image data found.");else if(K.complete===!1)console.warn("THREE.WebGLRenderer: Texture marked for update but image is incomplete");else{Y(F,b,v);return}}e.bindTexture(i.TEXTURE_2D,F.__webglTexture,i.TEXTURE0+v)}function X(b,v){const F=n.get(b);if(b.version>0&&F.__version!==b.version){Y(F,b,v);return}e.bindTexture(i.TEXTURE_2D_ARRAY,F.__webglTexture,i.TEXTURE0+v)}function tt(b,v){const F=n.get(b);if(b.version>0&&F.__version!==b.version){Y(F,b,v);return}e.bindTexture(i.TEXTURE_3D,F.__webglTexture,i.TEXTURE0+v)}function H(b,v){const F=n.get(b);if(b.version>0&&F.__version!==b.version){nt(F,b,v);return}e.bindTexture(i.TEXTURE_CUBE_MAP,F.__webglTexture,i.TEXTURE0+v)}const st={[Ua]:i.REPEAT,[ri]:i.CLAMP_TO_EDGE,[Ia]:i.MIRRORED_REPEAT},gt={[Je]:i.NEAREST,[bh]:i.NEAREST_MIPMAP_NEAREST,[Is]:i.NEAREST_MIPMAP_LINEAR,[vn]:i.LINEAR,[Br]:i.LINEAR_MIPMAP_NEAREST,[ai]:i.LINEAR_MIPMAP_LINEAR},Et={[Rh]:i.NEVER,[Ih]:i.ALWAYS,[Ch]:i.LESS,[fc]:i.LEQUAL,[Ph]:i.EQUAL,[Uh]:i.GEQUAL,[Dh]:i.GREATER,[Lh]:i.NOTEQUAL};function Ft(b,v){if(v.type===xn&&t.has("OES_texture_float_linear")===!1&&(v.magFilter===vn||v.magFilter===Br||v.magFilter===Is||v.magFilter===ai||v.minFilter===vn||v.minFilter===Br||v.minFilter===Is||v.minFilter===ai)&&console.warn("THREE.WebGLRenderer: Unable to use linear filtering with floating point textures. OES_texture_float_linear not supported on this device."),i.texParameteri(b,i.TEXTURE_WRAP_S,st[v.wrapS]),i.texParameteri(b,i.TEXTURE_WRAP_T,st[v.wrapT]),(b===i.TEXTURE_3D||b===i.TEXTURE_2D_ARRAY)&&i.texParameteri(b,i.TEXTURE_WRAP_R,st[v.wrapR]),i.texParameteri(b,i.TEXTURE_MAG_FILTER,gt[v.magFilter]),i.texParameteri(b,i.TEXTURE_MIN_FILTER,gt[v.minFilter]),v.compareFunction&&(i.texParameteri(b,i.TEXTURE_COMPARE_MODE,i.COMPARE_REF_TO_TEXTURE),i.texParameteri(b,i.TEXTURE_COMPARE_FUNC,Et[v.compareFunction])),t.has("EXT_texture_filter_anisotropic")===!0){if(v.magFilter===Je||v.minFilter!==Is&&v.minFilter!==ai||v.type===xn&&t.has("OES_texture_float_linear")===!1)return;if(v.anisotropy>1||n.get(v).__currentAnisotropy){const F=t.get("EXT_texture_filter_anisotropic");i.texParameterf(b,F.TEXTURE_MAX_ANISOTROPY_EXT,Math.min(v.anisotropy,s.getMaxAnisotropy())),n.get(v).__currentAnisotropy=v.anisotropy}}}function Xt(b,v){let F=!1;b.__webglInit===void 0&&(b.__webglInit=!0,v.addEventListener("dispose",w));const K=v.source;let J=p.get(K);J===void 0&&(J={},p.set(K,J));const j=q(v);if(j!==b.__cacheKey){J[j]===void 0&&(J[j]={texture:i.createTexture(),usedTimes:0},a.memory.textures++,F=!0),J[j].usedTimes++;const Tt=J[b.__cacheKey];Tt!==void 0&&(J[b.__cacheKey].usedTimes--,Tt.usedTimes===0&&S(v)),b.__cacheKey=j,b.__webglTexture=J[j].texture}return F}function Y(b,v,F){let K=i.TEXTURE_2D;(v.isDataArrayTexture||v.isCompressedArrayTexture)&&(K=i.TEXTURE_2D_ARRAY),v.isData3DTexture&&(K=i.TEXTURE_3D);const J=Xt(b,v),j=v.source;e.bindTexture(K,b.__webglTexture,i.TEXTURE0+F);const Tt=n.get(j);if(j.version!==Tt.__version||J===!0){e.activeTexture(i.TEXTURE0+F);const dt=Qt.getPrimaries(Qt.workingColorSpace),V=v.colorSpace===Vn?null:Qt.getPrimaries(v.colorSpace),at=v.colorSpace===Vn||dt===V?i.NONE:i.BROWSER_DEFAULT_WEBGL;i.pixelStorei(i.UNPACK_FLIP_Y_WEBGL,v.flipY),i.pixelStorei(i.UNPACK_PREMULTIPLY_ALPHA_WEBGL,v.premultiplyAlpha),i.pixelStorei(i.UNPACK_ALIGNMENT,v.unpackAlignment),i.pixelStorei(i.UNPACK_COLORSPACE_CONVERSION_WEBGL,at);let Z=_(v.image,!1,s.maxTextureSize);Z=le(v,Z);const it=r.convert(v.format,v.colorSpace),pt=r.convert(v.type);let wt=E(v.internalFormat,it,pt,v.colorSpace,v.isVideoTexture);Ft(K,v);let ht;const Bt=v.mipmaps,Ut=v.isVideoTexture!==!0,Zt=Tt.__version===void 0||J===!0,L=j.dataReady,rt=D(v,Z);if(v.isDepthTexture)wt=y(v.format===$i,v.type),Zt&&(Ut?e.texStorage2D(i.TEXTURE_2D,1,wt,Z.width,Z.height):e.texImage2D(i.TEXTURE_2D,0,wt,Z.width,Z.height,0,it,pt,null));else if(v.isDataTexture)if(Bt.length>0){Ut&&Zt&&e.texStorage2D(i.TEXTURE_2D,rt,wt,Bt[0].width,Bt[0].height);for(let G=0,$=Bt.length;G<$;G++)ht=Bt[G],Ut?L&&e.texSubImage2D(i.TEXTURE_2D,G,0,0,ht.width,ht.height,it,pt,ht.data):e.texImage2D(i.TEXTURE_2D,G,wt,ht.width,ht.height,0,it,pt,ht.data);v.generateMipmaps=!1}else Ut?(Zt&&e.texStorage2D(i.TEXTURE_2D,rt,wt,Z.width,Z.height),L&&e.texSubImage2D(i.TEXTURE_2D,0,0,0,Z.width,Z.height,it,pt,Z.data)):e.texImage2D(i.TEXTURE_2D,0,wt,Z.width,Z.height,0,it,pt,Z.data);else if(v.isCompressedTexture)if(v.isCompressedArrayTexture){Ut&&Zt&&e.texStorage3D(i.TEXTURE_2D_ARRAY,rt,wt,Bt[0].width,Bt[0].height,Z.depth);for(let G=0,$=Bt.length;G<$;G++)if(ht=Bt[G],v.format!==dn)if(it!==null)if(Ut){if(L)if(v.layerUpdates.size>0){const ft=_l(ht.width,ht.height,v.format,v.type);for(const ut of v.layerUpdates){const Ot=ht.data.subarray(ut*ft/ht.data.BYTES_PER_ELEMENT,(ut+1)*ft/ht.data.BYTES_PER_ELEMENT);e.compressedTexSubImage3D(i.TEXTURE_2D_ARRAY,G,0,0,ut,ht.width,ht.height,1,it,Ot)}v.clearLayerUpdates()}else e.compressedTexSubImage3D(i.TEXTURE_2D_ARRAY,G,0,0,0,ht.width,ht.height,Z.depth,it,ht.data)}else e.compressedTexImage3D(i.TEXTURE_2D_ARRAY,G,wt,ht.width,ht.height,Z.depth,0,ht.data,0,0);else console.warn("THREE.WebGLRenderer: Attempt to load unsupported compressed texture format in .uploadTexture()");else Ut?L&&e.texSubImage3D(i.TEXTURE_2D_ARRAY,G,0,0,0,ht.width,ht.height,Z.depth,it,pt,ht.data):e.texImage3D(i.TEXTURE_2D_ARRAY,G,wt,ht.width,ht.height,Z.depth,0,it,pt,ht.data)}else{Ut&&Zt&&e.texStorage2D(i.TEXTURE_2D,rt,wt,Bt[0].width,Bt[0].height);for(let G=0,$=Bt.length;G<$;G++)ht=Bt[G],v.format!==dn?it!==null?Ut?L&&e.compressedTexSubImage2D(i.TEXTURE_2D,G,0,0,ht.width,ht.height,it,ht.data):e.compressedTexImage2D(i.TEXTURE_2D,G,wt,ht.width,ht.height,0,ht.data):console.warn("THREE.WebGLRenderer: Attempt to load unsupported compressed texture format in .uploadTexture()"):Ut?L&&e.texSubImage2D(i.TEXTURE_2D,G,0,0,ht.width,ht.height,it,pt,ht.data):e.texImage2D(i.TEXTURE_2D,G,wt,ht.width,ht.height,0,it,pt,ht.data)}else if(v.isDataArrayTexture)if(Ut){if(Zt&&e.texStorage3D(i.TEXTURE_2D_ARRAY,rt,wt,Z.width,Z.height,Z.depth),L)if(v.layerUpdates.size>0){const G=_l(Z.width,Z.height,v.format,v.type);for(const $ of v.layerUpdates){const ft=Z.data.subarray($*G/Z.data.BYTES_PER_ELEMENT,($+1)*G/Z.data.BYTES_PER_ELEMENT);e.texSubImage3D(i.TEXTURE_2D_ARRAY,0,0,0,$,Z.width,Z.height,1,it,pt,ft)}v.clearLayerUpdates()}else e.texSubImage3D(i.TEXTURE_2D_ARRAY,0,0,0,0,Z.width,Z.height,Z.depth,it,pt,Z.data)}else e.texImage3D(i.TEXTURE_2D_ARRAY,0,wt,Z.width,Z.height,Z.depth,0,it,pt,Z.data);else if(v.isData3DTexture)Ut?(Zt&&e.texStorage3D(i.TEXTURE_3D,rt,wt,Z.width,Z.height,Z.depth),L&&e.texSubImage3D(i.TEXTURE_3D,0,0,0,0,Z.width,Z.height,Z.depth,it,pt,Z.data)):e.texImage3D(i.TEXTURE_3D,0,wt,Z.width,Z.height,Z.depth,0,it,pt,Z.data);else if(v.isFramebufferTexture){if(Zt)if(Ut)e.texStorage2D(i.TEXTURE_2D,rt,wt,Z.width,Z.height);else{let G=Z.width,$=Z.height;for(let ft=0;ft>=1,$>>=1}}else if(Bt.length>0){if(Ut&&Zt){const G=Rt(Bt[0]);e.texStorage2D(i.TEXTURE_2D,rt,wt,G.width,G.height)}for(let G=0,$=Bt.length;G<$;G++)ht=Bt[G],Ut?L&&e.texSubImage2D(i.TEXTURE_2D,G,0,0,it,pt,ht):e.texImage2D(i.TEXTURE_2D,G,wt,it,pt,ht);v.generateMipmaps=!1}else if(Ut){if(Zt){const G=Rt(Z);e.texStorage2D(i.TEXTURE_2D,rt,wt,G.width,G.height)}L&&e.texSubImage2D(i.TEXTURE_2D,0,0,0,it,pt,Z)}else e.texImage2D(i.TEXTURE_2D,0,wt,it,pt,Z);m(v)&&f(K),Tt.__version=j.version,v.onUpdate&&v.onUpdate(v)}b.__version=v.version}function nt(b,v,F){if(v.image.length!==6)return;const K=Xt(b,v),J=v.source;e.bindTexture(i.TEXTURE_CUBE_MAP,b.__webglTexture,i.TEXTURE0+F);const j=n.get(J);if(J.version!==j.__version||K===!0){e.activeTexture(i.TEXTURE0+F);const Tt=Qt.getPrimaries(Qt.workingColorSpace),dt=v.colorSpace===Vn?null:Qt.getPrimaries(v.colorSpace),V=v.colorSpace===Vn||Tt===dt?i.NONE:i.BROWSER_DEFAULT_WEBGL;i.pixelStorei(i.UNPACK_FLIP_Y_WEBGL,v.flipY),i.pixelStorei(i.UNPACK_PREMULTIPLY_ALPHA_WEBGL,v.premultiplyAlpha),i.pixelStorei(i.UNPACK_ALIGNMENT,v.unpackAlignment),i.pixelStorei(i.UNPACK_COLORSPACE_CONVERSION_WEBGL,V);const at=v.isCompressedTexture||v.image[0].isCompressedTexture,Z=v.image[0]&&v.image[0].isDataTexture,it=[];for(let $=0;$<6;$++)!at&&!Z?it[$]=_(v.image[$],!0,s.maxCubemapSize):it[$]=Z?v.image[$].image:v.image[$],it[$]=le(v,it[$]);const pt=it[0],wt=r.convert(v.format,v.colorSpace),ht=r.convert(v.type),Bt=E(v.internalFormat,wt,ht,v.colorSpace),Ut=v.isVideoTexture!==!0,Zt=j.__version===void 0||K===!0,L=J.dataReady;let rt=D(v,pt);Ft(i.TEXTURE_CUBE_MAP,v);let G;if(at){Ut&&Zt&&e.texStorage2D(i.TEXTURE_CUBE_MAP,rt,Bt,pt.width,pt.height);for(let $=0;$<6;$++){G=it[$].mipmaps;for(let ft=0;ft0&&rt++;const $=Rt(it[0]);e.texStorage2D(i.TEXTURE_CUBE_MAP,rt,Bt,$.width,$.height)}for(let $=0;$<6;$++)if(Z){Ut?L&&e.texSubImage2D(i.TEXTURE_CUBE_MAP_POSITIVE_X+$,0,0,0,it[$].width,it[$].height,wt,ht,it[$].data):e.texImage2D(i.TEXTURE_CUBE_MAP_POSITIVE_X+$,0,Bt,it[$].width,it[$].height,0,wt,ht,it[$].data);for(let ft=0;ft>j),pt=Math.max(1,v.height>>j);J===i.TEXTURE_3D||J===i.TEXTURE_2D_ARRAY?e.texImage3D(J,j,V,it,pt,v.depth,0,Tt,dt,null):e.texImage2D(J,j,V,it,pt,0,Tt,dt,null)}e.bindFramebuffer(i.FRAMEBUFFER,b),jt(v)?o.framebufferTexture2DMultisampleEXT(i.FRAMEBUFFER,K,J,Z.__webglTexture,0,qt(v)):(J===i.TEXTURE_2D||J>=i.TEXTURE_CUBE_MAP_POSITIVE_X&&J<=i.TEXTURE_CUBE_MAP_NEGATIVE_Z)&&i.framebufferTexture2D(i.FRAMEBUFFER,K,J,Z.__webglTexture,j),e.bindFramebuffer(i.FRAMEBUFFER,null)}function lt(b,v,F){if(i.bindRenderbuffer(i.RENDERBUFFER,b),v.depthBuffer){const K=v.depthTexture,J=K&&K.isDepthTexture?K.type:null,j=y(v.stencilBuffer,J),Tt=v.stencilBuffer?i.DEPTH_STENCIL_ATTACHMENT:i.DEPTH_ATTACHMENT,dt=qt(v);jt(v)?o.renderbufferStorageMultisampleEXT(i.RENDERBUFFER,dt,j,v.width,v.height):F?i.renderbufferStorageMultisample(i.RENDERBUFFER,dt,j,v.width,v.height):i.renderbufferStorage(i.RENDERBUFFER,j,v.width,v.height),i.framebufferRenderbuffer(i.FRAMEBUFFER,Tt,i.RENDERBUFFER,b)}else{const K=v.textures;for(let J=0;J{delete v.__boundDepthTexture,delete v.__depthDisposeCallback,K.removeEventListener("dispose",J)};K.addEventListener("dispose",J),v.__depthDisposeCallback=J}v.__boundDepthTexture=K}if(b.depthTexture&&!v.__autoAllocateDepthBuffer){if(F)throw new Error("target.depthTexture not supported in Cube render targets");Ct(v.__webglFramebuffer,b)}else if(F){v.__webglDepthbuffer=[];for(let K=0;K<6;K++)if(e.bindFramebuffer(i.FRAMEBUFFER,v.__webglFramebuffer[K]),v.__webglDepthbuffer[K]===void 0)v.__webglDepthbuffer[K]=i.createRenderbuffer(),lt(v.__webglDepthbuffer[K],b,!1);else{const J=b.stencilBuffer?i.DEPTH_STENCIL_ATTACHMENT:i.DEPTH_ATTACHMENT,j=v.__webglDepthbuffer[K];i.bindRenderbuffer(i.RENDERBUFFER,j),i.framebufferRenderbuffer(i.FRAMEBUFFER,J,i.RENDERBUFFER,j)}}else if(e.bindFramebuffer(i.FRAMEBUFFER,v.__webglFramebuffer),v.__webglDepthbuffer===void 0)v.__webglDepthbuffer=i.createRenderbuffer(),lt(v.__webglDepthbuffer,b,!1);else{const K=b.stencilBuffer?i.DEPTH_STENCIL_ATTACHMENT:i.DEPTH_ATTACHMENT,J=v.__webglDepthbuffer;i.bindRenderbuffer(i.RENDERBUFFER,J),i.framebufferRenderbuffer(i.FRAMEBUFFER,K,i.RENDERBUFFER,J)}e.bindFramebuffer(i.FRAMEBUFFER,null)}function Vt(b,v,F){const K=n.get(b);v!==void 0&&_t(K.__webglFramebuffer,b,b.texture,i.COLOR_ATTACHMENT0,i.TEXTURE_2D,0),F!==void 0&&Lt(b)}function ie(b){const v=b.texture,F=n.get(b),K=n.get(v);b.addEventListener("dispose",C);const J=b.textures,j=b.isWebGLCubeRenderTarget===!0,Tt=J.length>1;if(Tt||(K.__webglTexture===void 0&&(K.__webglTexture=i.createTexture()),K.__version=v.version,a.memory.textures++),j){F.__webglFramebuffer=[];for(let dt=0;dt<6;dt++)if(v.mipmaps&&v.mipmaps.length>0){F.__webglFramebuffer[dt]=[];for(let V=0;V0){F.__webglFramebuffer=[];for(let dt=0;dt0&&jt(b)===!1){F.__webglMultisampledFramebuffer=i.createFramebuffer(),F.__webglColorRenderbuffer=[],e.bindFramebuffer(i.FRAMEBUFFER,F.__webglMultisampledFramebuffer);for(let dt=0;dt0)for(let V=0;V0)for(let V=0;V0){if(jt(b)===!1){const v=b.textures,F=b.width,K=b.height;let J=i.COLOR_BUFFER_BIT;const j=b.stencilBuffer?i.DEPTH_STENCIL_ATTACHMENT:i.DEPTH_ATTACHMENT,Tt=n.get(b),dt=v.length>1;if(dt)for(let V=0;V0&&t.has("WEBGL_multisampled_render_to_texture")===!0&&v.__useRenderToTexture!==!1}function At(b){const v=a.render.frame;h.get(b)!==v&&(h.set(b,v),b.update())}function le(b,v){const F=b.colorSpace,K=b.format,J=b.type;return b.isCompressedTexture===!0||b.isVideoTexture===!0||F!==Ji&&F!==Vn&&(Qt.getTransfer(F)===se?(K!==dn||J!==Ln)&&console.warn("THREE.WebGLTextures: sRGB encoded textures have to use RGBAFormat and UnsignedByteType."):console.error("THREE.WebGLTextures: Unsupported texture color space:",F)),v}function Rt(b){return typeof HTMLImageElement<"u"&&b instanceof HTMLImageElement?(c.width=b.naturalWidth||b.width,c.height=b.naturalHeight||b.height):typeof VideoFrame<"u"&&b instanceof VideoFrame?(c.width=b.displayWidth,c.height=b.displayHeight):(c.width=b.width,c.height=b.height),c}this.allocateTextureUnit=k,this.resetTextureUnits=W,this.setTexture2D=Q,this.setTexture2DArray=X,this.setTexture3D=tt,this.setTextureCube=H,this.rebindTextures=Vt,this.setupRenderTarget=ie,this.updateRenderTargetMipmap=Wt,this.updateMultisampleRenderTarget=ye,this.setupDepthRenderbuffer=Lt,this.setupFrameBufferTexture=_t,this.useMultisampledRTT=jt}function Qm(i,t){function e(n,s=Vn){let r;const a=Qt.getTransfer(s);if(n===Ln)return i.UNSIGNED_BYTE;if(n===Mo)return i.UNSIGNED_SHORT_4_4_4_4;if(n===So)return i.UNSIGNED_SHORT_5_5_5_1;if(n===rc)return i.UNSIGNED_INT_5_9_9_9_REV;if(n===ic)return i.BYTE;if(n===sc)return i.SHORT;if(n===Ms)return i.UNSIGNED_SHORT;if(n===xo)return i.INT;if(n===li)return i.UNSIGNED_INT;if(n===xn)return i.FLOAT;if(n===Pn)return i.HALF_FLOAT;if(n===ac)return i.ALPHA;if(n===oc)return i.RGB;if(n===dn)return i.RGBA;if(n===lc)return i.LUMINANCE;if(n===cc)return i.LUMINANCE_ALPHA;if(n===Gi)return i.DEPTH_COMPONENT;if(n===$i)return i.DEPTH_STENCIL;if(n===yo)return i.RED;if(n===Eo)return i.RED_INTEGER;if(n===hc)return i.RG;if(n===bo)return i.RG_INTEGER;if(n===To)return i.RGBA_INTEGER;if(n===ur||n===dr||n===fr||n===pr)if(a===se)if(r=t.get("WEBGL_compressed_texture_s3tc_srgb"),r!==null){if(n===ur)return r.COMPRESSED_SRGB_S3TC_DXT1_EXT;if(n===dr)return r.COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT;if(n===fr)return r.COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT;if(n===pr)return r.COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT}else return null;else if(r=t.get("WEBGL_compressed_texture_s3tc"),r!==null){if(n===ur)return r.COMPRESSED_RGB_S3TC_DXT1_EXT;if(n===dr)return r.COMPRESSED_RGBA_S3TC_DXT1_EXT;if(n===fr)return r.COMPRESSED_RGBA_S3TC_DXT3_EXT;if(n===pr)return r.COMPRESSED_RGBA_S3TC_DXT5_EXT}else return null;if(n===Na||n===Fa||n===Oa||n===Ba)if(r=t.get("WEBGL_compressed_texture_pvrtc"),r!==null){if(n===Na)return r.COMPRESSED_RGB_PVRTC_4BPPV1_IMG;if(n===Fa)return r.COMPRESSED_RGB_PVRTC_2BPPV1_IMG;if(n===Oa)return r.COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;if(n===Ba)return r.COMPRESSED_RGBA_PVRTC_2BPPV1_IMG}else return null;if(n===za||n===ka||n===Ha)if(r=t.get("WEBGL_compressed_texture_etc"),r!==null){if(n===za||n===ka)return a===se?r.COMPRESSED_SRGB8_ETC2:r.COMPRESSED_RGB8_ETC2;if(n===Ha)return a===se?r.COMPRESSED_SRGB8_ALPHA8_ETC2_EAC:r.COMPRESSED_RGBA8_ETC2_EAC}else return null;if(n===Va||n===Ga||n===Wa||n===Xa||n===Ya||n===qa||n===ja||n===Za||n===Ka||n===$a||n===Ja||n===Qa||n===to||n===eo)if(r=t.get("WEBGL_compressed_texture_astc"),r!==null){if(n===Va)return a===se?r.COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR:r.COMPRESSED_RGBA_ASTC_4x4_KHR;if(n===Ga)return a===se?r.COMPRESSED_SRGB8_ALPHA8_ASTC_5x4_KHR:r.COMPRESSED_RGBA_ASTC_5x4_KHR;if(n===Wa)return a===se?r.COMPRESSED_SRGB8_ALPHA8_ASTC_5x5_KHR:r.COMPRESSED_RGBA_ASTC_5x5_KHR;if(n===Xa)return a===se?r.COMPRESSED_SRGB8_ALPHA8_ASTC_6x5_KHR:r.COMPRESSED_RGBA_ASTC_6x5_KHR;if(n===Ya)return a===se?r.COMPRESSED_SRGB8_ALPHA8_ASTC_6x6_KHR:r.COMPRESSED_RGBA_ASTC_6x6_KHR;if(n===qa)return a===se?r.COMPRESSED_SRGB8_ALPHA8_ASTC_8x5_KHR:r.COMPRESSED_RGBA_ASTC_8x5_KHR;if(n===ja)return a===se?r.COMPRESSED_SRGB8_ALPHA8_ASTC_8x6_KHR:r.COMPRESSED_RGBA_ASTC_8x6_KHR;if(n===Za)return a===se?r.COMPRESSED_SRGB8_ALPHA8_ASTC_8x8_KHR:r.COMPRESSED_RGBA_ASTC_8x8_KHR;if(n===Ka)return a===se?r.COMPRESSED_SRGB8_ALPHA8_ASTC_10x5_KHR:r.COMPRESSED_RGBA_ASTC_10x5_KHR;if(n===$a)return a===se?r.COMPRESSED_SRGB8_ALPHA8_ASTC_10x6_KHR:r.COMPRESSED_RGBA_ASTC_10x6_KHR;if(n===Ja)return a===se?r.COMPRESSED_SRGB8_ALPHA8_ASTC_10x8_KHR:r.COMPRESSED_RGBA_ASTC_10x8_KHR;if(n===Qa)return a===se?r.COMPRESSED_SRGB8_ALPHA8_ASTC_10x10_KHR:r.COMPRESSED_RGBA_ASTC_10x10_KHR;if(n===to)return a===se?r.COMPRESSED_SRGB8_ALPHA8_ASTC_12x10_KHR:r.COMPRESSED_RGBA_ASTC_12x10_KHR;if(n===eo)return a===se?r.COMPRESSED_SRGB8_ALPHA8_ASTC_12x12_KHR:r.COMPRESSED_RGBA_ASTC_12x12_KHR}else return null;if(n===mr||n===no||n===io)if(r=t.get("EXT_texture_compression_bptc"),r!==null){if(n===mr)return a===se?r.COMPRESSED_SRGB_ALPHA_BPTC_UNORM_EXT:r.COMPRESSED_RGBA_BPTC_UNORM_EXT;if(n===no)return r.COMPRESSED_RGB_BPTC_SIGNED_FLOAT_EXT;if(n===io)return r.COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_EXT}else return null;if(n===uc||n===so||n===ro||n===ao)if(r=t.get("EXT_texture_compression_rgtc"),r!==null){if(n===mr)return r.COMPRESSED_RED_RGTC1_EXT;if(n===so)return r.COMPRESSED_SIGNED_RED_RGTC1_EXT;if(n===ro)return r.COMPRESSED_RED_GREEN_RGTC2_EXT;if(n===ao)return r.COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT}else return null;return n===Ki?i.UNSIGNED_INT_24_8:i[n]!==void 0?i[n]:null}return{convert:e}}const tg={type:"move"};class ga{constructor(){this._targetRay=null,this._grip=null,this._hand=null}getHandSpace(){return this._hand===null&&(this._hand=new zi,this._hand.matrixAutoUpdate=!1,this._hand.visible=!1,this._hand.joints={},this._hand.inputState={pinching:!1}),this._hand}getTargetRaySpace(){return this._targetRay===null&&(this._targetRay=new zi,this._targetRay.matrixAutoUpdate=!1,this._targetRay.visible=!1,this._targetRay.hasLinearVelocity=!1,this._targetRay.linearVelocity=new P,this._targetRay.hasAngularVelocity=!1,this._targetRay.angularVelocity=new P),this._targetRay}getGripSpace(){return this._grip===null&&(this._grip=new zi,this._grip.matrixAutoUpdate=!1,this._grip.visible=!1,this._grip.hasLinearVelocity=!1,this._grip.linearVelocity=new P,this._grip.hasAngularVelocity=!1,this._grip.angularVelocity=new P),this._grip}dispatchEvent(t){return this._targetRay!==null&&this._targetRay.dispatchEvent(t),this._grip!==null&&this._grip.dispatchEvent(t),this._hand!==null&&this._hand.dispatchEvent(t),this}connect(t){if(t&&t.hand){const e=this._hand;if(e)for(const n of t.hand.values())this._getHandJoint(e,n)}return this.dispatchEvent({type:"connected",data:t}),this}disconnect(t){return this.dispatchEvent({type:"disconnected",data:t}),this._targetRay!==null&&(this._targetRay.visible=!1),this._grip!==null&&(this._grip.visible=!1),this._hand!==null&&(this._hand.visible=!1),this}update(t,e,n){let s=null,r=null,a=null;const o=this._targetRay,l=this._grip,c=this._hand;if(t&&e.session.visibilityState!=="visible-blurred"){if(c&&t.hand){a=!0;for(const _ of t.hand.values()){const m=e.getJointPose(_,n),f=this._getHandJoint(c,_);m!==null&&(f.matrix.fromArray(m.transform.matrix),f.matrix.decompose(f.position,f.rotation,f.scale),f.matrixWorldNeedsUpdate=!0,f.jointRadius=m.radius),f.visible=m!==null}const h=c.joints["index-finger-tip"],d=c.joints["thumb-tip"],p=h.position.distanceTo(d.position),u=.02,g=.005;c.inputState.pinching&&p>u+g?(c.inputState.pinching=!1,this.dispatchEvent({type:"pinchend",handedness:t.handedness,target:this})):!c.inputState.pinching&&p<=u-g&&(c.inputState.pinching=!0,this.dispatchEvent({type:"pinchstart",handedness:t.handedness,target:this}))}else l!==null&&t.gripSpace&&(r=e.getPose(t.gripSpace,n),r!==null&&(l.matrix.fromArray(r.transform.matrix),l.matrix.decompose(l.position,l.rotation,l.scale),l.matrixWorldNeedsUpdate=!0,r.linearVelocity?(l.hasLinearVelocity=!0,l.linearVelocity.copy(r.linearVelocity)):l.hasLinearVelocity=!1,r.angularVelocity?(l.hasAngularVelocity=!0,l.angularVelocity.copy(r.angularVelocity)):l.hasAngularVelocity=!1));o!==null&&(s=e.getPose(t.targetRaySpace,n),s===null&&r!==null&&(s=r),s!==null&&(o.matrix.fromArray(s.transform.matrix),o.matrix.decompose(o.position,o.rotation,o.scale),o.matrixWorldNeedsUpdate=!0,s.linearVelocity?(o.hasLinearVelocity=!0,o.linearVelocity.copy(s.linearVelocity)):o.hasLinearVelocity=!1,s.angularVelocity?(o.hasAngularVelocity=!0,o.angularVelocity.copy(s.angularVelocity)):o.hasAngularVelocity=!1,this.dispatchEvent(tg)))}return o!==null&&(o.visible=s!==null),l!==null&&(l.visible=r!==null),c!==null&&(c.visible=a!==null),this}_getHandJoint(t,e){if(t.joints[e.jointName]===void 0){const n=new zi;n.matrixAutoUpdate=!1,n.visible=!1,t.joints[e.jointName]=n,t.add(n)}return t.joints[e.jointName]}}const eg=` +void main() { + + gl_Position = vec4( position, 1.0 ); + +}`,ng=` +uniform sampler2DArray depthColor; +uniform float depthWidth; +uniform float depthHeight; + +void main() { + + vec2 coord = vec2( gl_FragCoord.x / depthWidth, gl_FragCoord.y / depthHeight ); + + if ( coord.x >= 1.0 ) { + + gl_FragDepth = texture( depthColor, vec3( coord.x - 1.0, coord.y, 1 ) ).r; + + } else { + + gl_FragDepth = texture( depthColor, vec3( coord.x, coord.y, 0 ) ).r; + + } + +}`;class ig{constructor(){this.texture=null,this.mesh=null,this.depthNear=0,this.depthFar=0}init(t,e,n){if(this.texture===null){const s=new Pe,r=t.properties.get(s);r.__webglTexture=e.texture,(e.depthNear!==n.depthNear||e.depthFar!==n.depthFar)&&(this.depthNear=e.depthNear,this.depthFar=e.depthFar),this.texture=s}}getMesh(t){if(this.texture!==null&&this.mesh===null){const e=t.cameras[0].viewport,n=new He({vertexShader:eg,fragmentShader:ng,uniforms:{depthColor:{value:this.texture},depthWidth:{value:e.z},depthHeight:{value:e.w}}});this.mesh=new be(new ws(20,20),n)}return this.mesh}reset(){this.texture=null,this.mesh=null}getDepthTexture(){return this.texture}}class sg extends hi{constructor(t,e){super();const n=this;let s=null,r=1,a=null,o="local-floor",l=1,c=null,h=null,d=null,p=null,u=null,g=null;const _=new ig,m=e.getContextAttributes();let f=null,T=null;const E=[],y=[],D=new St;let w=null;const C=new $e;C.viewport=new oe;const I=new $e;I.viewport=new oe;const S=[C,I],M=new Su;let A=null,W=null;this.cameraAutoUpdate=!0,this.enabled=!1,this.isPresenting=!1,this.getController=function(Y){let nt=E[Y];return nt===void 0&&(nt=new ga,E[Y]=nt),nt.getTargetRaySpace()},this.getControllerGrip=function(Y){let nt=E[Y];return nt===void 0&&(nt=new ga,E[Y]=nt),nt.getGripSpace()},this.getHand=function(Y){let nt=E[Y];return nt===void 0&&(nt=new ga,E[Y]=nt),nt.getHandSpace()};function k(Y){const nt=y.indexOf(Y.inputSource);if(nt===-1)return;const _t=E[nt];_t!==void 0&&(_t.update(Y.inputSource,Y.frame,c||a),_t.dispatchEvent({type:Y.type,data:Y.inputSource}))}function q(){s.removeEventListener("select",k),s.removeEventListener("selectstart",k),s.removeEventListener("selectend",k),s.removeEventListener("squeeze",k),s.removeEventListener("squeezestart",k),s.removeEventListener("squeezeend",k),s.removeEventListener("end",q),s.removeEventListener("inputsourceschange",Q);for(let Y=0;Y=0&&(y[lt]=null,E[lt].disconnect(_t))}for(let nt=0;nt=y.length){y.push(_t),lt=Lt;break}else if(y[Lt]===null){y[Lt]=_t,lt=Lt;break}if(lt===-1)break}const Ct=E[lt];Ct&&Ct.connect(_t)}}const X=new P,tt=new P;function H(Y,nt,_t){X.setFromMatrixPosition(nt.matrixWorld),tt.setFromMatrixPosition(_t.matrixWorld);const lt=X.distanceTo(tt),Ct=nt.projectionMatrix.elements,Lt=_t.projectionMatrix.elements,Vt=Ct[14]/(Ct[10]-1),ie=Ct[14]/(Ct[10]+1),Wt=(Ct[9]+1)/Ct[5],ue=(Ct[9]-1)/Ct[5],R=(Ct[8]-1)/Ct[0],ye=(Lt[8]+1)/Lt[0],qt=Vt*R,jt=Vt*ye,At=lt/(-R+ye),le=At*-R;if(nt.matrixWorld.decompose(Y.position,Y.quaternion,Y.scale),Y.translateX(le),Y.translateZ(At),Y.matrixWorld.compose(Y.position,Y.quaternion,Y.scale),Y.matrixWorldInverse.copy(Y.matrixWorld).invert(),Ct[10]===-1)Y.projectionMatrix.copy(nt.projectionMatrix),Y.projectionMatrixInverse.copy(nt.projectionMatrixInverse);else{const Rt=Vt+At,b=ie+At,v=qt-le,F=jt+(lt-le),K=Wt*ie/b*Rt,J=ue*ie/b*Rt;Y.projectionMatrix.makePerspective(v,F,K,J,Rt,b),Y.projectionMatrixInverse.copy(Y.projectionMatrix).invert()}}function st(Y,nt){nt===null?Y.matrixWorld.copy(Y.matrix):Y.matrixWorld.multiplyMatrices(nt.matrixWorld,Y.matrix),Y.matrixWorldInverse.copy(Y.matrixWorld).invert()}this.updateCamera=function(Y){if(s===null)return;let nt=Y.near,_t=Y.far;_.texture!==null&&(_.depthNear>0&&(nt=_.depthNear),_.depthFar>0&&(_t=_.depthFar)),M.near=I.near=C.near=nt,M.far=I.far=C.far=_t,(A!==M.near||W!==M.far)&&(s.updateRenderState({depthNear:M.near,depthFar:M.far}),A=M.near,W=M.far),C.layers.mask=Y.layers.mask|2,I.layers.mask=Y.layers.mask|4,M.layers.mask=C.layers.mask|I.layers.mask;const lt=Y.parent,Ct=M.cameras;st(M,lt);for(let Lt=0;Lt0&&(m.alphaTest.value=f.alphaTest);const T=t.get(f),E=T.envMap,y=T.envMapRotation;E&&(m.envMap.value=E,ti.copy(y),ti.x*=-1,ti.y*=-1,ti.z*=-1,E.isCubeTexture&&E.isRenderTargetTexture===!1&&(ti.y*=-1,ti.z*=-1),m.envMapRotation.value.setFromMatrix4(rg.makeRotationFromEuler(ti)),m.flipEnvMap.value=E.isCubeTexture&&E.isRenderTargetTexture===!1?-1:1,m.reflectivity.value=f.reflectivity,m.ior.value=f.ior,m.refractionRatio.value=f.refractionRatio),f.lightMap&&(m.lightMap.value=f.lightMap,m.lightMapIntensity.value=f.lightMapIntensity,e(f.lightMap,m.lightMapTransform)),f.aoMap&&(m.aoMap.value=f.aoMap,m.aoMapIntensity.value=f.aoMapIntensity,e(f.aoMap,m.aoMapTransform))}function a(m,f){m.diffuse.value.copy(f.color),m.opacity.value=f.opacity,f.map&&(m.map.value=f.map,e(f.map,m.mapTransform))}function o(m,f){m.dashSize.value=f.dashSize,m.totalSize.value=f.dashSize+f.gapSize,m.scale.value=f.scale}function l(m,f,T,E){m.diffuse.value.copy(f.color),m.opacity.value=f.opacity,m.size.value=f.size*T,m.scale.value=E*.5,f.map&&(m.map.value=f.map,e(f.map,m.uvTransform)),f.alphaMap&&(m.alphaMap.value=f.alphaMap,e(f.alphaMap,m.alphaMapTransform)),f.alphaTest>0&&(m.alphaTest.value=f.alphaTest)}function c(m,f){m.diffuse.value.copy(f.color),m.opacity.value=f.opacity,m.rotation.value=f.rotation,f.map&&(m.map.value=f.map,e(f.map,m.mapTransform)),f.alphaMap&&(m.alphaMap.value=f.alphaMap,e(f.alphaMap,m.alphaMapTransform)),f.alphaTest>0&&(m.alphaTest.value=f.alphaTest)}function h(m,f){m.specular.value.copy(f.specular),m.shininess.value=Math.max(f.shininess,1e-4)}function d(m,f){f.gradientMap&&(m.gradientMap.value=f.gradientMap)}function p(m,f){m.metalness.value=f.metalness,f.metalnessMap&&(m.metalnessMap.value=f.metalnessMap,e(f.metalnessMap,m.metalnessMapTransform)),m.roughness.value=f.roughness,f.roughnessMap&&(m.roughnessMap.value=f.roughnessMap,e(f.roughnessMap,m.roughnessMapTransform)),f.envMap&&(m.envMapIntensity.value=f.envMapIntensity)}function u(m,f,T){m.ior.value=f.ior,f.sheen>0&&(m.sheenColor.value.copy(f.sheenColor).multiplyScalar(f.sheen),m.sheenRoughness.value=f.sheenRoughness,f.sheenColorMap&&(m.sheenColorMap.value=f.sheenColorMap,e(f.sheenColorMap,m.sheenColorMapTransform)),f.sheenRoughnessMap&&(m.sheenRoughnessMap.value=f.sheenRoughnessMap,e(f.sheenRoughnessMap,m.sheenRoughnessMapTransform))),f.clearcoat>0&&(m.clearcoat.value=f.clearcoat,m.clearcoatRoughness.value=f.clearcoatRoughness,f.clearcoatMap&&(m.clearcoatMap.value=f.clearcoatMap,e(f.clearcoatMap,m.clearcoatMapTransform)),f.clearcoatRoughnessMap&&(m.clearcoatRoughnessMap.value=f.clearcoatRoughnessMap,e(f.clearcoatRoughnessMap,m.clearcoatRoughnessMapTransform)),f.clearcoatNormalMap&&(m.clearcoatNormalMap.value=f.clearcoatNormalMap,e(f.clearcoatNormalMap,m.clearcoatNormalMapTransform),m.clearcoatNormalScale.value.copy(f.clearcoatNormalScale),f.side===Xe&&m.clearcoatNormalScale.value.negate())),f.dispersion>0&&(m.dispersion.value=f.dispersion),f.iridescence>0&&(m.iridescence.value=f.iridescence,m.iridescenceIOR.value=f.iridescenceIOR,m.iridescenceThicknessMinimum.value=f.iridescenceThicknessRange[0],m.iridescenceThicknessMaximum.value=f.iridescenceThicknessRange[1],f.iridescenceMap&&(m.iridescenceMap.value=f.iridescenceMap,e(f.iridescenceMap,m.iridescenceMapTransform)),f.iridescenceThicknessMap&&(m.iridescenceThicknessMap.value=f.iridescenceThicknessMap,e(f.iridescenceThicknessMap,m.iridescenceThicknessMapTransform))),f.transmission>0&&(m.transmission.value=f.transmission,m.transmissionSamplerMap.value=T.texture,m.transmissionSamplerSize.value.set(T.width,T.height),f.transmissionMap&&(m.transmissionMap.value=f.transmissionMap,e(f.transmissionMap,m.transmissionMapTransform)),m.thickness.value=f.thickness,f.thicknessMap&&(m.thicknessMap.value=f.thicknessMap,e(f.thicknessMap,m.thicknessMapTransform)),m.attenuationDistance.value=f.attenuationDistance,m.attenuationColor.value.copy(f.attenuationColor)),f.anisotropy>0&&(m.anisotropyVector.value.set(f.anisotropy*Math.cos(f.anisotropyRotation),f.anisotropy*Math.sin(f.anisotropyRotation)),f.anisotropyMap&&(m.anisotropyMap.value=f.anisotropyMap,e(f.anisotropyMap,m.anisotropyMapTransform))),m.specularIntensity.value=f.specularIntensity,m.specularColor.value.copy(f.specularColor),f.specularColorMap&&(m.specularColorMap.value=f.specularColorMap,e(f.specularColorMap,m.specularColorMapTransform)),f.specularIntensityMap&&(m.specularIntensityMap.value=f.specularIntensityMap,e(f.specularIntensityMap,m.specularIntensityMapTransform))}function g(m,f){f.matcap&&(m.matcap.value=f.matcap)}function _(m,f){const T=t.get(f).light;m.referencePosition.value.setFromMatrixPosition(T.matrixWorld),m.nearDistance.value=T.shadow.camera.near,m.farDistance.value=T.shadow.camera.far}return{refreshFogUniforms:n,refreshMaterialUniforms:s}}function og(i,t,e,n){let s={},r={},a=[];const o=i.getParameter(i.MAX_UNIFORM_BUFFER_BINDINGS);function l(T,E){const y=E.program;n.uniformBlockBinding(T,y)}function c(T,E){let y=s[T.id];y===void 0&&(g(T),y=h(T),s[T.id]=y,T.addEventListener("dispose",m));const D=E.program;n.updateUBOMapping(T,D);const w=t.render.frame;r[T.id]!==w&&(p(T),r[T.id]=w)}function h(T){const E=d();T.__bindingPointIndex=E;const y=i.createBuffer(),D=T.__size,w=T.usage;return i.bindBuffer(i.UNIFORM_BUFFER,y),i.bufferData(i.UNIFORM_BUFFER,D,w),i.bindBuffer(i.UNIFORM_BUFFER,null),i.bindBufferBase(i.UNIFORM_BUFFER,E,y),y}function d(){for(let T=0;T0&&(y+=D-w),T.__size=y,T.__cache={},this}function _(T){const E={boundary:0,storage:0};return typeof T=="number"||typeof T=="boolean"?(E.boundary=4,E.storage=4):T.isVector2?(E.boundary=8,E.storage=8):T.isVector3||T.isColor?(E.boundary=16,E.storage=12):T.isVector4?(E.boundary=16,E.storage=16):T.isMatrix3?(E.boundary=48,E.storage=48):T.isMatrix4?(E.boundary=64,E.storage=64):T.isTexture?console.warn("THREE.WebGLRenderer: Texture samplers can not be part of an uniforms group."):console.warn("THREE.WebGLRenderer: Unsupported uniform value type.",T),E}function m(T){const E=T.target;E.removeEventListener("dispose",m);const y=a.indexOf(E.__bindingPointIndex);a.splice(y,1),i.deleteBuffer(s[E.id]),delete s[E.id],delete r[E.id]}function f(){for(const T in s)i.deleteBuffer(s[T]);a=[],s={},r={}}return{bind:l,update:c,dispose:f}}class lg{constructor(t={}){const{canvas:e=Oh(),context:n=null,depth:s=!0,stencil:r=!1,alpha:a=!1,antialias:o=!1,premultipliedAlpha:l=!0,preserveDrawingBuffer:c=!1,powerPreference:h="default",failIfMajorPerformanceCaveat:d=!1,reverseDepthBuffer:p=!1}=t;this.isWebGLRenderer=!0;let u;if(n!==null){if(typeof WebGLRenderingContext<"u"&&n instanceof WebGLRenderingContext)throw new Error("THREE.WebGLRenderer: WebGL 1 is not supported since r163.");u=n.getContextAttributes().alpha}else u=a;const g=new Uint32Array(4),_=new Int32Array(4);let m=null,f=null;const T=[],E=[];this.domElement=e,this.debug={checkShaderErrors:!0,onShaderError:null},this.autoClear=!0,this.autoClearColor=!0,this.autoClearDepth=!0,this.autoClearStencil=!0,this.sortObjects=!0,this.clippingPlanes=[],this.localClippingEnabled=!1,this._outputColorSpace=an,this.toneMapping=Wn,this.toneMappingExposure=1;const y=this;let D=!1,w=0,C=0,I=null,S=-1,M=null;const A=new oe,W=new oe;let k=null;const q=new ot(0);let Q=0,X=e.width,tt=e.height,H=1,st=null,gt=null;const Et=new oe(0,0,X,tt),Ft=new oe(0,0,X,tt);let Xt=!1;const Y=new Ao;let nt=!1,_t=!1;this.transmissionResolutionScale=1;const lt=new ne,Ct=new ne,Lt=new P,Vt=new oe,ie={background:null,fog:null,environment:null,overrideMaterial:null,isScene:!0};let Wt=!1;function ue(){return I===null?H:1}let R=n;function ye(x,U){return e.getContext(x,U)}try{const x={alpha:!0,depth:s,stencil:r,antialias:o,premultipliedAlpha:l,preserveDrawingBuffer:c,powerPreference:h,failIfMajorPerformanceCaveat:d};if("setAttribute"in e&&e.setAttribute("data-engine",`three.js r${vo}`),e.addEventListener("webglcontextlost",$,!1),e.addEventListener("webglcontextrestored",ft,!1),e.addEventListener("webglcontextcreationerror",ut,!1),R===null){const U="webgl2";if(R=ye(U,x),R===null)throw ye(U)?new Error("Error creating WebGL context with your selected attributes."):new Error("Error creating WebGL context.")}}catch(x){throw console.error("THREE.WebGLRenderer: "+x.message),x}let qt,jt,At,le,Rt,b,v,F,K,J,j,Tt,dt,V,at,Z,it,pt,wt,ht,Bt,Ut,Zt,L;function rt(){qt=new _p(R),qt.init(),Ut=new Qm(R,qt),jt=new up(R,qt,t,Ut),At=new $m(R,qt),jt.reverseDepthBuffer&&p&&At.buffers.depth.setReversed(!0),le=new Mp(R),Rt=new Bm,b=new Jm(R,qt,At,Rt,jt,Ut,le),v=new fp(y),F=new gp(y),K=new wu(R),Zt=new cp(R,K),J=new vp(R,K,le,Zt),j=new yp(R,J,K,le),wt=new Sp(R,jt,b),Z=new dp(Rt),Tt=new Om(y,v,F,qt,jt,Zt,Z),dt=new ag(y,Rt),V=new km,at=new Ym(qt),pt=new lp(y,v,F,At,j,u,l),it=new Zm(y,j,jt),L=new og(R,le,jt,At),ht=new hp(R,qt,le),Bt=new xp(R,qt,le),le.programs=Tt.programs,y.capabilities=jt,y.extensions=qt,y.properties=Rt,y.renderLists=V,y.shadowMap=it,y.state=At,y.info=le}rt();const G=new sg(y,R);this.xr=G,this.getContext=function(){return R},this.getContextAttributes=function(){return R.getContextAttributes()},this.forceContextLoss=function(){const x=qt.get("WEBGL_lose_context");x&&x.loseContext()},this.forceContextRestore=function(){const x=qt.get("WEBGL_lose_context");x&&x.restoreContext()},this.getPixelRatio=function(){return H},this.setPixelRatio=function(x){x!==void 0&&(H=x,this.setSize(X,tt,!1))},this.getSize=function(x){return x.set(X,tt)},this.setSize=function(x,U,O=!0){if(G.isPresenting){console.warn("THREE.WebGLRenderer: Can't change size while VR device is presenting.");return}X=x,tt=U,e.width=Math.floor(x*H),e.height=Math.floor(U*H),O===!0&&(e.style.width=x+"px",e.style.height=U+"px"),this.setViewport(0,0,x,U)},this.getDrawingBufferSize=function(x){return x.set(X*H,tt*H).floor()},this.setDrawingBufferSize=function(x,U,O){X=x,tt=U,H=O,e.width=Math.floor(x*O),e.height=Math.floor(U*O),this.setViewport(0,0,x,U)},this.getCurrentViewport=function(x){return x.copy(A)},this.getViewport=function(x){return x.copy(Et)},this.setViewport=function(x,U,O,z){x.isVector4?Et.set(x.x,x.y,x.z,x.w):Et.set(x,U,O,z),At.viewport(A.copy(Et).multiplyScalar(H).round())},this.getScissor=function(x){return x.copy(Ft)},this.setScissor=function(x,U,O,z){x.isVector4?Ft.set(x.x,x.y,x.z,x.w):Ft.set(x,U,O,z),At.scissor(W.copy(Ft).multiplyScalar(H).round())},this.getScissorTest=function(){return Xt},this.setScissorTest=function(x){At.setScissorTest(Xt=x)},this.setOpaqueSort=function(x){st=x},this.setTransparentSort=function(x){gt=x},this.getClearColor=function(x){return x.copy(pt.getClearColor())},this.setClearColor=function(){pt.setClearColor.apply(pt,arguments)},this.getClearAlpha=function(){return pt.getClearAlpha()},this.setClearAlpha=function(){pt.setClearAlpha.apply(pt,arguments)},this.clear=function(x=!0,U=!0,O=!0){let z=0;if(x){let N=!1;if(I!==null){const et=I.texture.format;N=et===To||et===bo||et===Eo}if(N){const et=I.texture.type,mt=et===Ln||et===li||et===Ms||et===Ki||et===Mo||et===So,vt=pt.getClearColor(),Mt=pt.getClearAlpha(),It=vt.r,Nt=vt.g,Pt=vt.b;mt?(g[0]=It,g[1]=Nt,g[2]=Pt,g[3]=Mt,R.clearBufferuiv(R.COLOR,0,g)):(_[0]=It,_[1]=Nt,_[2]=Pt,_[3]=Mt,R.clearBufferiv(R.COLOR,0,_))}else z|=R.COLOR_BUFFER_BIT}U&&(z|=R.DEPTH_BUFFER_BIT),O&&(z|=R.STENCIL_BUFFER_BIT,this.state.buffers.stencil.setMask(4294967295)),R.clear(z)},this.clearColor=function(){this.clear(!0,!1,!1)},this.clearDepth=function(){this.clear(!1,!0,!1)},this.clearStencil=function(){this.clear(!1,!1,!0)},this.dispose=function(){e.removeEventListener("webglcontextlost",$,!1),e.removeEventListener("webglcontextrestored",ft,!1),e.removeEventListener("webglcontextcreationerror",ut,!1),pt.dispose(),V.dispose(),at.dispose(),Rt.dispose(),v.dispose(),F.dispose(),j.dispose(),Zt.dispose(),L.dispose(),Tt.dispose(),G.dispose(),G.removeEventListener("sessionstart",Rs),G.removeEventListener("sessionend",fi),pn.stop()};function $(x){x.preventDefault(),console.log("THREE.WebGLRenderer: Context Lost."),D=!0}function ft(){console.log("THREE.WebGLRenderer: Context Restored."),D=!1;const x=le.autoReset,U=it.enabled,O=it.autoUpdate,z=it.needsUpdate,N=it.type;rt(),le.autoReset=x,it.enabled=U,it.autoUpdate=O,it.needsUpdate=z,it.type=N}function ut(x){console.error("THREE.WebGLRenderer: A WebGL context could not be created. Reason: ",x.statusMessage)}function Ot(x){const U=x.target;U.removeEventListener("dispose",Ot),ce(U)}function ce(x){Ae(x),Rt.remove(x)}function Ae(x){const U=Rt.get(x).programs;U!==void 0&&(U.forEach(function(O){Tt.releaseProgram(O)}),x.isShaderMaterial&&Tt.releaseShaderCache(x))}this.renderBufferDirect=function(x,U,O,z,N,et){U===null&&(U=ie);const mt=N.isMesh&&N.matrixWorld.determinant()<0,vt=Ds(x,U,O,z,N);At.setMaterial(z,mt);let Mt=O.index,It=1;if(z.wireframe===!0){if(Mt=J.getWireframeAttribute(O),Mt===void 0)return;It=2}const Nt=O.drawRange,Pt=O.attributes.position;let Kt=Nt.start*It,te=(Nt.start+Nt.count)*It;et!==null&&(Kt=Math.max(Kt,et.start*It),te=Math.min(te,(et.start+et.count)*It)),Mt!==null?(Kt=Math.max(Kt,0),te=Math.min(te,Mt.count)):Pt!=null&&(Kt=Math.max(Kt,0),te=Math.min(te,Pt.count));const xe=te-Kt;if(xe<0||xe===1/0)return;Zt.setup(N,z,vt,O,Mt);let pe,Jt=ht;if(Mt!==null&&(pe=K.get(Mt),Jt=Bt,Jt.setIndex(pe)),N.isMesh)z.wireframe===!0?(At.setLineWidth(z.wireframeLinewidth*ue()),Jt.setMode(R.LINES)):Jt.setMode(R.TRIANGLES);else if(N.isLine){let Dt=z.linewidth;Dt===void 0&&(Dt=1),At.setLineWidth(Dt*ue()),N.isLineSegments?Jt.setMode(R.LINES):N.isLineLoop?Jt.setMode(R.LINE_LOOP):Jt.setMode(R.LINE_STRIP)}else N.isPoints?Jt.setMode(R.POINTS):N.isSprite&&Jt.setMode(R.TRIANGLES);if(N.isBatchedMesh)if(N._multiDrawInstances!==null)Jt.renderMultiDrawInstances(N._multiDrawStarts,N._multiDrawCounts,N._multiDrawCount,N._multiDrawInstances);else if(qt.get("WEBGL_multi_draw"))Jt.renderMultiDraw(N._multiDrawStarts,N._multiDrawCounts,N._multiDrawCount);else{const Dt=N._multiDrawStarts,De=N._multiDrawCounts,ee=N._multiDrawCount,ln=Mt?K.get(Mt).bytesPerElement:1,mi=Rt.get(z).currentProgram.getUniforms();for(let qe=0;qe{function et(){if(z.forEach(function(mt){Rt.get(mt).currentProgram.isReady()&&z.delete(mt)}),z.size===0){N(x);return}setTimeout(et,10)}qt.get("KHR_parallel_shader_compile")!==null?et():setTimeout(et,10)})};let Ye=null;function Qe(x){Ye&&Ye(x)}function Rs(){pn.stop()}function fi(){pn.start()}const pn=new Rc;pn.setAnimationLoop(Qe),typeof self<"u"&&pn.setContext(self),this.setAnimationLoop=function(x){Ye=x,G.setAnimationLoop(x),x===null?pn.stop():pn.start()},G.addEventListener("sessionstart",Rs),G.addEventListener("sessionend",fi),this.render=function(x,U){if(U!==void 0&&U.isCamera!==!0){console.error("THREE.WebGLRenderer.render: camera is not an instance of THREE.Camera.");return}if(D===!0)return;if(x.matrixWorldAutoUpdate===!0&&x.updateMatrixWorld(),U.parent===null&&U.matrixWorldAutoUpdate===!0&&U.updateMatrixWorld(),G.enabled===!0&&G.isPresenting===!0&&(G.cameraAutoUpdate===!0&&G.updateCamera(U),U=G.getCamera()),x.isScene===!0&&x.onBeforeRender(y,x,U,I),f=at.get(x,E.length),f.init(U),E.push(f),Ct.multiplyMatrices(U.projectionMatrix,U.matrixWorldInverse),Y.setFromProjectionMatrix(Ct),_t=this.localClippingEnabled,nt=Z.init(this.clippingPlanes,_t),m=V.get(x,T.length),m.init(),T.push(m),G.enabled===!0&&G.isPresenting===!0){const et=y.xr.getDepthSensingMesh();et!==null&&ns(et,U,-1/0,y.sortObjects)}ns(x,U,0,y.sortObjects),m.finish(),y.sortObjects===!0&&m.sort(st,gt),Wt=G.enabled===!1||G.isPresenting===!1||G.hasDepthSensing()===!1,Wt&&pt.addToRenderList(m,x),this.info.render.frame++,nt===!0&&Z.beginShadows();const O=f.state.shadowsArray;it.render(O,x,U),nt===!0&&Z.endShadows(),this.info.autoReset===!0&&this.info.reset();const z=m.opaque,N=m.transmissive;if(f.setupLights(),U.isArrayCamera){const et=U.cameras;if(N.length>0)for(let mt=0,vt=et.length;mt0&&Ve(z,N,x,U),Wt&&pt.render(x),Cs(m,x,U);I!==null&&C===0&&(b.updateMultisampleRenderTarget(I),b.updateRenderTargetMipmap(I)),x.isScene===!0&&x.onAfterRender(y,x,U),Zt.resetDefaultState(),S=-1,M=null,E.pop(),E.length>0?(f=E[E.length-1],nt===!0&&Z.setGlobalState(y.clippingPlanes,f.state.camera)):f=null,T.pop(),T.length>0?m=T[T.length-1]:m=null};function ns(x,U,O,z){if(x.visible===!1)return;if(x.layers.test(U.layers)){if(x.isGroup)O=x.renderOrder;else if(x.isLOD)x.autoUpdate===!0&&x.update(U);else if(x.isLight)f.pushLight(x),x.castShadow&&f.pushShadow(x);else if(x.isSprite){if(!x.frustumCulled||Y.intersectsSprite(x)){z&&Vt.setFromMatrixPosition(x.matrixWorld).applyMatrix4(Ct);const mt=j.update(x),vt=x.material;vt.visible&&m.push(x,mt,vt,O,Vt.z,null)}}else if((x.isMesh||x.isLine||x.isPoints)&&(!x.frustumCulled||Y.intersectsObject(x))){const mt=j.update(x),vt=x.material;if(z&&(x.boundingSphere!==void 0?(x.boundingSphere===null&&x.computeBoundingSphere(),Vt.copy(x.boundingSphere.center)):(mt.boundingSphere===null&&mt.computeBoundingSphere(),Vt.copy(mt.boundingSphere.center)),Vt.applyMatrix4(x.matrixWorld).applyMatrix4(Ct)),Array.isArray(vt)){const Mt=mt.groups;for(let It=0,Nt=Mt.length;It0&&Re(N,U,O),et.length>0&&Re(et,U,O),mt.length>0&&Re(mt,U,O),At.buffers.depth.setTest(!0),At.buffers.depth.setMask(!0),At.buffers.color.setMask(!0),At.setPolygonOffset(!1)}function Ve(x,U,O,z){if((O.isScene===!0?O.overrideMaterial:null)!==null)return;f.state.transmissionRenderTarget[z.id]===void 0&&(f.state.transmissionRenderTarget[z.id]=new fn(1,1,{generateMipmaps:!0,type:qt.has("EXT_color_buffer_half_float")||qt.has("EXT_color_buffer_float")?Pn:Ln,minFilter:ai,samples:4,stencilBuffer:r,resolveDepthBuffer:!1,resolveStencilBuffer:!1,colorSpace:Qt.workingColorSpace}));const et=f.state.transmissionRenderTarget[z.id],mt=z.viewport||A;et.setSize(mt.z*y.transmissionResolutionScale,mt.w*y.transmissionResolutionScale);const vt=y.getRenderTarget();y.setRenderTarget(et),y.getClearColor(q),Q=y.getClearAlpha(),Q<1&&y.setClearColor(16777215,.5),y.clear(),Wt&&pt.render(O);const Mt=y.toneMapping;y.toneMapping=Wn;const It=z.viewport;if(z.viewport!==void 0&&(z.viewport=void 0),f.setupLightsView(z),nt===!0&&Z.setGlobalState(y.clippingPlanes,z),Re(x,O,z),b.updateMultisampleRenderTarget(et),b.updateRenderTargetMipmap(et),qt.has("WEBGL_multisampled_render_to_texture")===!1){let Nt=!1;for(let Pt=0,Kt=U.length;Pt0),Pt=!!O.morphAttributes.position,Kt=!!O.morphAttributes.normal,te=!!O.morphAttributes.color;let xe=Wn;z.toneMapped&&(I===null||I.isXRRenderTarget===!0)&&(xe=y.toneMapping);const pe=O.morphAttributes.position||O.morphAttributes.normal||O.morphAttributes.color,Jt=pe!==void 0?pe.length:0,Dt=Rt.get(z),De=f.state.lights;if(nt===!0&&(_t===!0||x!==M)){const Be=x===M&&z.id===S;Z.setState(z,x,Be)}let ee=!1;z.version===Dt.__version?(Dt.needsLights&&Dt.lightsStateVersion!==De.state.version||Dt.outputColorSpace!==vt||N.isBatchedMesh&&Dt.batching===!1||!N.isBatchedMesh&&Dt.batching===!0||N.isBatchedMesh&&Dt.batchingColor===!0&&N.colorTexture===null||N.isBatchedMesh&&Dt.batchingColor===!1&&N.colorTexture!==null||N.isInstancedMesh&&Dt.instancing===!1||!N.isInstancedMesh&&Dt.instancing===!0||N.isSkinnedMesh&&Dt.skinning===!1||!N.isSkinnedMesh&&Dt.skinning===!0||N.isInstancedMesh&&Dt.instancingColor===!0&&N.instanceColor===null||N.isInstancedMesh&&Dt.instancingColor===!1&&N.instanceColor!==null||N.isInstancedMesh&&Dt.instancingMorph===!0&&N.morphTexture===null||N.isInstancedMesh&&Dt.instancingMorph===!1&&N.morphTexture!==null||Dt.envMap!==Mt||z.fog===!0&&Dt.fog!==et||Dt.numClippingPlanes!==void 0&&(Dt.numClippingPlanes!==Z.numPlanes||Dt.numIntersection!==Z.numIntersection)||Dt.vertexAlphas!==It||Dt.vertexTangents!==Nt||Dt.morphTargets!==Pt||Dt.morphNormals!==Kt||Dt.morphColors!==te||Dt.toneMapping!==xe||Dt.morphTargetsCount!==Jt)&&(ee=!0):(ee=!0,Dt.__version=z.version);let ln=Dt.currentProgram;ee===!0&&(ln=en(z,U,N));let mi=!1,qe=!1,is=!1;const de=ln.getUniforms(),nn=Dt.uniforms;if(At.useProgram(ln.program)&&(mi=!0,qe=!0,is=!0),z.id!==S&&(S=z.id,qe=!0),mi||M!==x){At.buffers.depth.getReversed()?(lt.copy(x.projectionMatrix),zh(lt),kh(lt),de.setValue(R,"projectionMatrix",lt)):de.setValue(R,"projectionMatrix",x.projectionMatrix),de.setValue(R,"viewMatrix",x.matrixWorldInverse);const Ge=de.map.cameraPosition;Ge!==void 0&&Ge.setValue(R,Lt.setFromMatrixPosition(x.matrixWorld)),jt.logarithmicDepthBuffer&&de.setValue(R,"logDepthBufFC",2/(Math.log(x.far+1)/Math.LN2)),(z.isMeshPhongMaterial||z.isMeshToonMaterial||z.isMeshLambertMaterial||z.isMeshBasicMaterial||z.isMeshStandardMaterial||z.isShaderMaterial)&&de.setValue(R,"isOrthographic",x.isOrthographicCamera===!0),M!==x&&(M=x,qe=!0,is=!0)}if(N.isSkinnedMesh){de.setOptional(R,N,"bindMatrix"),de.setOptional(R,N,"bindMatrixInverse");const Be=N.skeleton;Be&&(Be.boneTexture===null&&Be.computeBoneTexture(),de.setValue(R,"boneTexture",Be.boneTexture,b))}N.isBatchedMesh&&(de.setOptional(R,N,"batchingTexture"),de.setValue(R,"batchingTexture",N._matricesTexture,b),de.setOptional(R,N,"batchingIdTexture"),de.setValue(R,"batchingIdTexture",N._indirectTexture,b),de.setOptional(R,N,"batchingColorTexture"),N._colorsTexture!==null&&de.setValue(R,"batchingColorTexture",N._colorsTexture,b));const sn=O.morphAttributes;if((sn.position!==void 0||sn.normal!==void 0||sn.color!==void 0)&&wt.update(N,O,ln),(qe||Dt.receiveShadow!==N.receiveShadow)&&(Dt.receiveShadow=N.receiveShadow,de.setValue(R,"receiveShadow",N.receiveShadow)),z.isMeshGouraudMaterial&&z.envMap!==null&&(nn.envMap.value=Mt,nn.flipEnvMap.value=Mt.isCubeTexture&&Mt.isRenderTargetTexture===!1?-1:1),z.isMeshStandardMaterial&&z.envMap===null&&U.environment!==null&&(nn.envMapIntensity.value=U.environmentIntensity),qe&&(de.setValue(R,"toneMappingExposure",y.toneMappingExposure),Dt.needsLights&&Ir(nn,is),et&&z.fog===!0&&dt.refreshFogUniforms(nn,et),dt.refreshMaterialUniforms(nn,z,H,tt,f.state.transmissionRenderTarget[x.id]),_r.upload(R,pi(Dt),nn,b)),z.isShaderMaterial&&z.uniformsNeedUpdate===!0&&(_r.upload(R,pi(Dt),nn,b),z.uniformsNeedUpdate=!1),z.isSpriteMaterial&&de.setValue(R,"center",N.center),de.setValue(R,"modelViewMatrix",N.modelViewMatrix),de.setValue(R,"normalMatrix",N.normalMatrix),de.setValue(R,"modelMatrix",N.matrixWorld),z.isShaderMaterial||z.isRawShaderMaterial){const Be=z.uniformsGroups;for(let Ge=0,Or=Be.length;Ge0&&b.useMultisampledRTT(x)===!1?N=Rt.get(x).__webglMultisampledFramebuffer:Array.isArray(Nt)?N=Nt[O]:N=Nt,A.copy(x.viewport),W.copy(x.scissor),k=x.scissorTest}else A.copy(Et).multiplyScalar(H).floor(),W.copy(Ft).multiplyScalar(H).floor(),k=Xt;if(O!==0&&(N=Nr),At.bindFramebuffer(R.FRAMEBUFFER,N)&&z&&At.drawBuffers(x,N),At.viewport(A),At.scissor(W),At.setScissorTest(k),et){const Mt=Rt.get(x.texture);R.framebufferTexture2D(R.FRAMEBUFFER,R.COLOR_ATTACHMENT0,R.TEXTURE_CUBE_MAP_POSITIVE_X+U,Mt.__webglTexture,O)}else if(mt){const Mt=Rt.get(x.texture),It=U;R.framebufferTextureLayer(R.FRAMEBUFFER,R.COLOR_ATTACHMENT0,Mt.__webglTexture,O,It)}else if(x!==null&&O!==0){const Mt=Rt.get(x.texture);R.framebufferTexture2D(R.FRAMEBUFFER,R.COLOR_ATTACHMENT0,R.TEXTURE_2D,Mt.__webglTexture,O)}S=-1},this.readRenderTargetPixels=function(x,U,O,z,N,et,mt){if(!(x&&x.isWebGLRenderTarget)){console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not THREE.WebGLRenderTarget.");return}let vt=Rt.get(x).__webglFramebuffer;if(x.isWebGLCubeRenderTarget&&mt!==void 0&&(vt=vt[mt]),vt){At.bindFramebuffer(R.FRAMEBUFFER,vt);try{const Mt=x.texture,It=Mt.format,Nt=Mt.type;if(!jt.textureFormatReadable(It)){console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not in RGBA or implementation defined format.");return}if(!jt.textureTypeReadable(Nt)){console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not in UnsignedByteType or implementation defined type.");return}U>=0&&U<=x.width-z&&O>=0&&O<=x.height-N&&R.readPixels(U,O,z,N,Ut.convert(It),Ut.convert(Nt),et)}finally{const Mt=I!==null?Rt.get(I).__webglFramebuffer:null;At.bindFramebuffer(R.FRAMEBUFFER,Mt)}}},this.readRenderTargetPixelsAsync=async function(x,U,O,z,N,et,mt){if(!(x&&x.isWebGLRenderTarget))throw new Error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not THREE.WebGLRenderTarget.");let vt=Rt.get(x).__webglFramebuffer;if(x.isWebGLCubeRenderTarget&&mt!==void 0&&(vt=vt[mt]),vt){const Mt=x.texture,It=Mt.format,Nt=Mt.type;if(!jt.textureFormatReadable(It))throw new Error("THREE.WebGLRenderer.readRenderTargetPixelsAsync: renderTarget is not in RGBA or implementation defined format.");if(!jt.textureTypeReadable(Nt))throw new Error("THREE.WebGLRenderer.readRenderTargetPixelsAsync: renderTarget is not in UnsignedByteType or implementation defined type.");if(U>=0&&U<=x.width-z&&O>=0&&O<=x.height-N){At.bindFramebuffer(R.FRAMEBUFFER,vt);const Pt=R.createBuffer();R.bindBuffer(R.PIXEL_PACK_BUFFER,Pt),R.bufferData(R.PIXEL_PACK_BUFFER,et.byteLength,R.STREAM_READ),R.readPixels(U,O,z,N,Ut.convert(It),Ut.convert(Nt),0);const Kt=I!==null?Rt.get(I).__webglFramebuffer:null;At.bindFramebuffer(R.FRAMEBUFFER,Kt);const te=R.fenceSync(R.SYNC_GPU_COMMANDS_COMPLETE,0);return R.flush(),await Bh(R,te,4),R.bindBuffer(R.PIXEL_PACK_BUFFER,Pt),R.getBufferSubData(R.PIXEL_PACK_BUFFER,0,et),R.deleteBuffer(Pt),R.deleteSync(te),et}else throw new Error("THREE.WebGLRenderer.readRenderTargetPixelsAsync: requested read bounds are out of range.")}},this.copyFramebufferToTexture=function(x,U=null,O=0){x.isTexture!==!0&&(Fi("WebGLRenderer: copyFramebufferToTexture function signature has changed."),U=arguments[0]||null,x=arguments[1]);const z=Math.pow(2,-O),N=Math.floor(x.image.width*z),et=Math.floor(x.image.height*z),mt=U!==null?U.x:0,vt=U!==null?U.y:0;b.setTexture2D(x,0),R.copyTexSubImage2D(R.TEXTURE_2D,O,0,0,mt,vt,N,et),At.unbindTexture()};const Fr=R.createFramebuffer(),Oc=R.createFramebuffer();this.copyTextureToTexture=function(x,U,O=null,z=null,N=0,et=null){x.isTexture!==!0&&(Fi("WebGLRenderer: copyTextureToTexture function signature has changed."),z=arguments[0]||null,x=arguments[1],U=arguments[2],et=arguments[3]||0,O=null),et===null&&(N!==0?(Fi("WebGLRenderer: copyTextureToTexture function signature has changed to support src and dst mipmap levels."),et=N,N=0):et=0);let mt,vt,Mt,It,Nt,Pt,Kt,te,xe;const pe=x.isCompressedTexture?x.mipmaps[et]:x.image;if(O!==null)mt=O.max.x-O.min.x,vt=O.max.y-O.min.y,Mt=O.isBox3?O.max.z-O.min.z:1,It=O.min.x,Nt=O.min.y,Pt=O.isBox3?O.min.z:0;else{const sn=Math.pow(2,-N);mt=Math.floor(pe.width*sn),vt=Math.floor(pe.height*sn),x.isDataArrayTexture?Mt=pe.depth:x.isData3DTexture?Mt=Math.floor(pe.depth*sn):Mt=1,It=0,Nt=0,Pt=0}z!==null?(Kt=z.x,te=z.y,xe=z.z):(Kt=0,te=0,xe=0);const Jt=Ut.convert(U.format),Dt=Ut.convert(U.type);let De;U.isData3DTexture?(b.setTexture3D(U,0),De=R.TEXTURE_3D):U.isDataArrayTexture||U.isCompressedArrayTexture?(b.setTexture2DArray(U,0),De=R.TEXTURE_2D_ARRAY):(b.setTexture2D(U,0),De=R.TEXTURE_2D),R.pixelStorei(R.UNPACK_FLIP_Y_WEBGL,U.flipY),R.pixelStorei(R.UNPACK_PREMULTIPLY_ALPHA_WEBGL,U.premultiplyAlpha),R.pixelStorei(R.UNPACK_ALIGNMENT,U.unpackAlignment);const ee=R.getParameter(R.UNPACK_ROW_LENGTH),ln=R.getParameter(R.UNPACK_IMAGE_HEIGHT),mi=R.getParameter(R.UNPACK_SKIP_PIXELS),qe=R.getParameter(R.UNPACK_SKIP_ROWS),is=R.getParameter(R.UNPACK_SKIP_IMAGES);R.pixelStorei(R.UNPACK_ROW_LENGTH,pe.width),R.pixelStorei(R.UNPACK_IMAGE_HEIGHT,pe.height),R.pixelStorei(R.UNPACK_SKIP_PIXELS,It),R.pixelStorei(R.UNPACK_SKIP_ROWS,Nt),R.pixelStorei(R.UNPACK_SKIP_IMAGES,Pt);const de=x.isDataArrayTexture||x.isData3DTexture,nn=U.isDataArrayTexture||U.isData3DTexture;if(x.isDepthTexture){const sn=Rt.get(x),Be=Rt.get(U),Ge=Rt.get(sn.__renderTarget),Or=Rt.get(Be.__renderTarget);At.bindFramebuffer(R.READ_FRAMEBUFFER,Ge.__webglFramebuffer),At.bindFramebuffer(R.DRAW_FRAMEBUFFER,Or.__webglFramebuffer);for(let jn=0;jnMath.PI&&(n-=We),s<-Math.PI?s+=We:s>Math.PI&&(s-=We),n<=s?this._spherical.theta=Math.max(n,Math.min(s,this._spherical.theta)):this._spherical.theta=this._spherical.theta>(n+s)/2?Math.max(n,this._spherical.theta):Math.min(s,this._spherical.theta)),this._spherical.phi=Math.max(this.minPolarAngle,Math.min(this.maxPolarAngle,this._spherical.phi)),this._spherical.makeSafe(),this.enableDamping===!0?this.target.addScaledVector(this._panOffset,this.dampingFactor):this.target.add(this._panOffset),this.target.sub(this.cursor),this.target.clampLength(this.minTargetRadius,this.maxTargetRadius),this.target.add(this.cursor);let r=!1;if(this.zoomToCursor&&this._performCursorZoom||this.object.isOrthographicCamera)this._spherical.radius=this._clampDistance(this._spherical.radius);else{const a=this._spherical.radius;this._spherical.radius=this._clampDistance(this._spherical.radius*this._scale),r=a!=this._spherical.radius}if(Ee.setFromSpherical(this._spherical),Ee.applyQuaternion(this._quatInverse),e.copy(this.target).add(Ee),this.object.lookAt(this.target),this.enableDamping===!0?(this._sphericalDelta.theta*=1-this.dampingFactor,this._sphericalDelta.phi*=1-this.dampingFactor,this._panOffset.multiplyScalar(1-this.dampingFactor)):(this._sphericalDelta.set(0,0,0),this._panOffset.set(0,0,0)),this.zoomToCursor&&this._performCursorZoom){let a=null;if(this.object.isPerspectiveCamera){const o=Ee.length();a=this._clampDistance(o*this._scale);const l=o-a;this.object.position.addScaledVector(this._dollyDirection,l),this.object.updateMatrixWorld(),r=!!l}else if(this.object.isOrthographicCamera){const o=new P(this._mouse.x,this._mouse.y,0);o.unproject(this.object);const l=this.object.zoom;this.object.zoom=Math.max(this.minZoom,Math.min(this.maxZoom,this.object.zoom/this._scale)),this.object.updateProjectionMatrix(),r=l!==this.object.zoom;const c=new P(this._mouse.x,this._mouse.y,0);c.unproject(this.object),this.object.position.sub(c).add(o),this.object.updateMatrixWorld(),a=Ee.length()}else console.warn("WARNING: OrbitControls.js encountered an unknown camera type - zoom to cursor disabled."),this.zoomToCursor=!1;a!==null&&(this.screenSpacePanning?this.target.set(0,0,-1).transformDirection(this.object.matrix).multiplyScalar(a).add(this.object.position):(cr.origin.copy(this.object.position),cr.direction.set(0,0,-1).transformDirection(this.object.matrix),Math.abs(this.object.up.dot(cr.direction))_a||8*(1-this._lastQuaternion.dot(this.object.quaternion))>_a||this._lastTargetPosition.distanceToSquared(this.target)>_a?(this.dispatchEvent(Vl),this._lastPosition.copy(this.object.position),this._lastQuaternion.copy(this.object.quaternion),this._lastTargetPosition.copy(this.target),!0):!1}_getAutoRotationAngle(t){return t!==null?We/60*this.autoRotateSpeed*t:We/60/60*this.autoRotateSpeed}_getZoomScale(t){const e=Math.abs(t*.01);return Math.pow(.95,this.zoomSpeed*e)}_rotateLeft(t){this._sphericalDelta.theta-=t}_rotateUp(t){this._sphericalDelta.phi-=t}_panLeft(t,e){Ee.setFromMatrixColumn(e,0),Ee.multiplyScalar(-t),this._panOffset.add(Ee)}_panUp(t,e){this.screenSpacePanning===!0?Ee.setFromMatrixColumn(e,1):(Ee.setFromMatrixColumn(e,0),Ee.crossVectors(this.object.up,Ee)),Ee.multiplyScalar(t),this._panOffset.add(Ee)}_pan(t,e){const n=this.domElement;if(this.object.isPerspectiveCamera){const s=this.object.position;Ee.copy(s).sub(this.target);let r=Ee.length();r*=Math.tan(this.object.fov/2*Math.PI/180),this._panLeft(2*t*r/n.clientHeight,this.object.matrix),this._panUp(2*e*r/n.clientHeight,this.object.matrix)}else this.object.isOrthographicCamera?(this._panLeft(t*(this.object.right-this.object.left)/this.object.zoom/n.clientWidth,this.object.matrix),this._panUp(e*(this.object.top-this.object.bottom)/this.object.zoom/n.clientHeight,this.object.matrix)):(console.warn("WARNING: OrbitControls.js encountered an unknown camera type - pan disabled."),this.enablePan=!1)}_dollyOut(t){this.object.isPerspectiveCamera||this.object.isOrthographicCamera?this._scale/=t:(console.warn("WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled."),this.enableZoom=!1)}_dollyIn(t){this.object.isPerspectiveCamera||this.object.isOrthographicCamera?this._scale*=t:(console.warn("WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled."),this.enableZoom=!1)}_updateZoomParameters(t,e){if(!this.zoomToCursor)return;this._performCursorZoom=!0;const n=this.domElement.getBoundingClientRect(),s=t-n.left,r=e-n.top,a=n.width,o=n.height;this._mouse.x=s/a*2-1,this._mouse.y=-(r/o)*2+1,this._dollyDirection.set(this._mouse.x,this._mouse.y,1).unproject(this.object).sub(this.object.position).normalize()}_clampDistance(t){return Math.max(this.minDistance,Math.min(this.maxDistance,t))}_handleMouseDownRotate(t){this._rotateStart.set(t.clientX,t.clientY)}_handleMouseDownDolly(t){this._updateZoomParameters(t.clientX,t.clientX),this._dollyStart.set(t.clientX,t.clientY)}_handleMouseDownPan(t){this._panStart.set(t.clientX,t.clientY)}_handleMouseMoveRotate(t){this._rotateEnd.set(t.clientX,t.clientY),this._rotateDelta.subVectors(this._rotateEnd,this._rotateStart).multiplyScalar(this.rotateSpeed);const e=this.domElement;this._rotateLeft(We*this._rotateDelta.x/e.clientHeight),this._rotateUp(We*this._rotateDelta.y/e.clientHeight),this._rotateStart.copy(this._rotateEnd),this.update()}_handleMouseMoveDolly(t){this._dollyEnd.set(t.clientX,t.clientY),this._dollyDelta.subVectors(this._dollyEnd,this._dollyStart),this._dollyDelta.y>0?this._dollyOut(this._getZoomScale(this._dollyDelta.y)):this._dollyDelta.y<0&&this._dollyIn(this._getZoomScale(this._dollyDelta.y)),this._dollyStart.copy(this._dollyEnd),this.update()}_handleMouseMovePan(t){this._panEnd.set(t.clientX,t.clientY),this._panDelta.subVectors(this._panEnd,this._panStart).multiplyScalar(this.panSpeed),this._pan(this._panDelta.x,this._panDelta.y),this._panStart.copy(this._panEnd),this.update()}_handleMouseWheel(t){this._updateZoomParameters(t.clientX,t.clientY),t.deltaY<0?this._dollyIn(this._getZoomScale(t.deltaY)):t.deltaY>0&&this._dollyOut(this._getZoomScale(t.deltaY)),this.update()}_handleKeyDown(t){let e=!1;switch(t.code){case this.keys.UP:t.ctrlKey||t.metaKey||t.shiftKey?this.enableRotate&&this._rotateUp(We*this.keyRotateSpeed/this.domElement.clientHeight):this.enablePan&&this._pan(0,this.keyPanSpeed),e=!0;break;case this.keys.BOTTOM:t.ctrlKey||t.metaKey||t.shiftKey?this.enableRotate&&this._rotateUp(-We*this.keyRotateSpeed/this.domElement.clientHeight):this.enablePan&&this._pan(0,-this.keyPanSpeed),e=!0;break;case this.keys.LEFT:t.ctrlKey||t.metaKey||t.shiftKey?this.enableRotate&&this._rotateLeft(We*this.keyRotateSpeed/this.domElement.clientHeight):this.enablePan&&this._pan(this.keyPanSpeed,0),e=!0;break;case this.keys.RIGHT:t.ctrlKey||t.metaKey||t.shiftKey?this.enableRotate&&this._rotateLeft(-We*this.keyRotateSpeed/this.domElement.clientHeight):this.enablePan&&this._pan(-this.keyPanSpeed,0),e=!0;break}e&&(t.preventDefault(),this.update())}_handleTouchStartRotate(t){if(this._pointers.length===1)this._rotateStart.set(t.pageX,t.pageY);else{const e=this._getSecondPointerPosition(t),n=.5*(t.pageX+e.x),s=.5*(t.pageY+e.y);this._rotateStart.set(n,s)}}_handleTouchStartPan(t){if(this._pointers.length===1)this._panStart.set(t.pageX,t.pageY);else{const e=this._getSecondPointerPosition(t),n=.5*(t.pageX+e.x),s=.5*(t.pageY+e.y);this._panStart.set(n,s)}}_handleTouchStartDolly(t){const e=this._getSecondPointerPosition(t),n=t.pageX-e.x,s=t.pageY-e.y,r=Math.sqrt(n*n+s*s);this._dollyStart.set(0,r)}_handleTouchStartDollyPan(t){this.enableZoom&&this._handleTouchStartDolly(t),this.enablePan&&this._handleTouchStartPan(t)}_handleTouchStartDollyRotate(t){this.enableZoom&&this._handleTouchStartDolly(t),this.enableRotate&&this._handleTouchStartRotate(t)}_handleTouchMoveRotate(t){if(this._pointers.length==1)this._rotateEnd.set(t.pageX,t.pageY);else{const n=this._getSecondPointerPosition(t),s=.5*(t.pageX+n.x),r=.5*(t.pageY+n.y);this._rotateEnd.set(s,r)}this._rotateDelta.subVectors(this._rotateEnd,this._rotateStart).multiplyScalar(this.rotateSpeed);const e=this.domElement;this._rotateLeft(We*this._rotateDelta.x/e.clientHeight),this._rotateUp(We*this._rotateDelta.y/e.clientHeight),this._rotateStart.copy(this._rotateEnd)}_handleTouchMovePan(t){if(this._pointers.length===1)this._panEnd.set(t.pageX,t.pageY);else{const e=this._getSecondPointerPosition(t),n=.5*(t.pageX+e.x),s=.5*(t.pageY+e.y);this._panEnd.set(n,s)}this._panDelta.subVectors(this._panEnd,this._panStart).multiplyScalar(this.panSpeed),this._pan(this._panDelta.x,this._panDelta.y),this._panStart.copy(this._panEnd)}_handleTouchMoveDolly(t){const e=this._getSecondPointerPosition(t),n=t.pageX-e.x,s=t.pageY-e.y,r=Math.sqrt(n*n+s*s);this._dollyEnd.set(0,r),this._dollyDelta.set(0,Math.pow(this._dollyEnd.y/this._dollyStart.y,this.zoomSpeed)),this._dollyOut(this._dollyDelta.y),this._dollyStart.copy(this._dollyEnd);const a=(t.pageX+e.x)*.5,o=(t.pageY+e.y)*.5;this._updateZoomParameters(a,o)}_handleTouchMoveDollyPan(t){this.enableZoom&&this._handleTouchMoveDolly(t),this.enablePan&&this._handleTouchMovePan(t)}_handleTouchMoveDollyRotate(t){this.enableZoom&&this._handleTouchMoveDolly(t),this.enableRotate&&this._handleTouchMoveRotate(t)}_addPointer(t){this._pointers.push(t.pointerId)}_removePointer(t){delete this._pointerPositions[t.pointerId];for(let e=0;e + varying vec2 vUv; + uniform sampler2D colorTexture; + uniform vec2 invSize; + uniform vec2 direction; + uniform float gaussianCoefficients[KERNEL_RADIUS]; + + void main() { + float weightSum = gaussianCoefficients[0]; + vec3 diffuseSum = texture2D( colorTexture, vUv ).rgb * weightSum; + for( int i = 1; i < KERNEL_RADIUS; i ++ ) { + float x = float(i); + float w = gaussianCoefficients[i]; + vec2 uvOffset = direction * invSize * x; + vec3 sample1 = texture2D( colorTexture, vUv + uvOffset ).rgb; + vec3 sample2 = texture2D( colorTexture, vUv - uvOffset ).rgb; + diffuseSum += (sample1 + sample2) * w; + weightSum += 2.0 * w; + } + gl_FragColor = vec4(diffuseSum/weightSum, 1.0); + }`})}getCompositeMaterial(t){return new He({defines:{NUM_MIPS:t},uniforms:{blurTexture1:{value:null},blurTexture2:{value:null},blurTexture3:{value:null},blurTexture4:{value:null},blurTexture5:{value:null},bloomStrength:{value:1},bloomFactors:{value:null},bloomTintColors:{value:null},bloomRadius:{value:0}},vertexShader:`varying vec2 vUv; + void main() { + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); + }`,fragmentShader:`varying vec2 vUv; + uniform sampler2D blurTexture1; + uniform sampler2D blurTexture2; + uniform sampler2D blurTexture3; + uniform sampler2D blurTexture4; + uniform sampler2D blurTexture5; + uniform float bloomStrength; + uniform float bloomRadius; + uniform float bloomFactors[NUM_MIPS]; + uniform vec3 bloomTintColors[NUM_MIPS]; + + float lerpBloomFactor(const in float factor) { + float mirrorFactor = 1.2 - factor; + return mix(factor, mirrorFactor, bloomRadius); + } + + void main() { + gl_FragColor = bloomStrength * ( lerpBloomFactor(bloomFactors[0]) * vec4(bloomTintColors[0], 1.0) * texture2D(blurTexture1, vUv) + + lerpBloomFactor(bloomFactors[1]) * vec4(bloomTintColors[1], 1.0) * texture2D(blurTexture2, vUv) + + lerpBloomFactor(bloomFactors[2]) * vec4(bloomTintColors[2], 1.0) * texture2D(blurTexture3, vUv) + + lerpBloomFactor(bloomFactors[3]) * vec4(bloomTintColors[3], 1.0) * texture2D(blurTexture4, vUv) + + lerpBloomFactor(bloomFactors[4]) * vec4(bloomTintColors[4], 1.0) * texture2D(blurTexture5, vUv) ); + }`})}}ts.BlurDirectionX=new St(1,0);ts.BlurDirectionY=new St(0,1);function Pg(){const t=new Float32Array(6e3),e=new Float32Array(2e3*3);for(let r=0;r<2e3;r++){const a=Math.random()*Math.PI*2,o=Math.acos(2*Math.random()-1),l=600+Math.random()*400;t[r*3]=l*Math.sin(o)*Math.cos(a),t[r*3+1]=l*Math.sin(o)*Math.sin(a),t[r*3+2]=l*Math.cos(o);const c=Math.random();e[r*3]=.55+c*.25,e[r*3+1]=.55+c*.15,e[r*3+2]=.75+c*.25}const n=new ge;n.setAttribute("position",new he(t,3)),n.setAttribute("color",new he(e,3));const s=new oi({size:1.6,sizeAttenuation:!0,vertexColors:!0,transparent:!0,opacity:.6,depthWrite:!1,blending:Le});return new Yi(n,s)}function Dg(i){const t=new lu;t.background=new ot(328975),t.fog=new Dr(657946,.0035);const e=new $e(60,i.clientWidth/i.clientHeight,.1,2e3);e.position.set(0,30,80);const n=new lg({antialias:!0,alpha:!0,powerPreference:"high-performance"});n.setSize(i.clientWidth,i.clientHeight),n.setPixelRatio(Math.min(window.devicePixelRatio,2)),n.toneMapping=ec,n.toneMappingExposure=1.25,i.appendChild(n.domElement);const s=new hg(e,n.domElement);s.enableDamping=!0,s.dampingFactor=.05,s.rotateSpeed=.5,s.zoomSpeed=.8,s.minDistance=12,s.maxDistance=180,s.autoRotate=!0,s.autoRotateSpeed=.3;const r=new Ag(n);r.addPass(new Rg(t,e));const a=new ts(new St(i.clientWidth,i.clientHeight),.55,.6,.2);r.addPass(a);const o=new Mu(2763354,.7);t.add(o);const l=new dl(6514417,1.8,240);l.position.set(50,50,50),t.add(l);const c=new dl(11032055,1.2,240);c.position.set(-50,-30,-50),t.add(c);const h=Pg();t.add(h);const d=new Eu;d.params.Points={threshold:2};const p=new St;return{scene:t,camera:e,renderer:n,controls:s,composer:r,bloomPass:a,raycaster:d,mouse:p,lights:{ambient:o,point1:l,point2:c},starfield:h}}function Lg(i,t){const e=t.clientWidth,n=t.clientHeight;i.camera.aspect=e/n,i.camera.updateProjectionMatrix(),i.renderer.setSize(e,n),i.composer.setSize(e,n)}function Ug(i){i.scene.traverse(t=>{var e;(t instanceof be||t instanceof du)&&((e=t.geometry)==null||e.dispose(),Array.isArray(t.material)?t.material.forEach(n=>n.dispose()):t.material&&t.material.dispose())}),i.renderer.dispose(),i.composer.dispose()}class Ig{constructor(t){kt(this,"positions");kt(this,"velocities");kt(this,"running",!0);kt(this,"step",0);kt(this,"repulsionStrength",500);kt(this,"attractionStrength",.01);kt(this,"dampening",.9);kt(this,"baseMaxSteps",300);kt(this,"maxSteps",300);kt(this,"cooldownExtension",0);this.positions=t,this.velocities=new Map;for(const e of t.keys())this.velocities.set(e,new P)}addNode(t,e){this.positions.set(t,e.clone()),this.velocities.set(t,new P),this.cooldownExtension=100,this.maxSteps=Math.max(this.maxSteps,this.step+this.cooldownExtension),this.running=!0}removeNode(t){this.positions.delete(t),this.velocities.delete(t)}tick(t){if(!this.running)return;if(this.step>this.maxSteps){this.cooldownExtension>0&&(this.cooldownExtension=0,this.maxSteps=this.baseMaxSteps);return}this.step++;const e=Math.max(.001,1-this.step/this.maxSteps),n=Array.from(this.positions.keys());for(let s=0;s=.7?"active":i>=.4?"dormant":i>=.1?"silent":"unavailable"}const po={active:"#10b981",dormant:"#f59e0b",silent:"#8b5cf6",unavailable:"#6b7280"},Fg={active:"Easily retrievable (retention ≥ 70%)",dormant:"Retrievable with effort (40–70%)",silent:"Difficult, needs cues (10–40%)",unavailable:"Needs reinforcement (< 10%)"},xr={aha:"#FFD700",confusion:"#EF4444",failure:"#9CA3AF"},Og={aha:"Aha moments and breakthroughs",confusion:"Confusions and weak spots",failure:"Failures and guardrails"};function Xl(i,t){return t==="state"?po[Ng(i.retention)]:t==="ahagraph"?Bg(i)??Sa[i.type]??"#8B95A5":Sa[i.type]||"#8B95A5"}function Bg(i){const t=new Set((i.tags??[]).map(e=>e.toLowerCase()));return t.has("aha")?xr.aha:t.has("confusion")||t.has("weak-spot")?xr.confusion:t.has("failure")||t.has("guardrail")?xr.failure:null}let ms=null;function mo(){if(ms)return ms;const i=128,t=document.createElement("canvas");t.width=i,t.height=i;const e=t.getContext("2d");if(!e)return ms=new Pe,ms;const n=e.createRadialGradient(i/2,i/2,0,i/2,i/2,i/2);n.addColorStop(0,"rgba(255, 255, 255, 1.0)"),n.addColorStop(.25,"rgba(255, 255, 255, 0.7)"),n.addColorStop(.55,"rgba(255, 255, 255, 0.2)"),n.addColorStop(1,"rgba(255, 255, 255, 0.0)"),e.fillStyle=n,e.fillRect(0,0,i,i);const s=new bc(t);return s.needsUpdate=!0,ms=s,s}function Yl(i){if(i===0||i===1)return i;const t=.3;return Math.pow(2,-10*i)*Math.sin((i-t/4)*(2*Math.PI)/t)+1}function zg(i){return i*i*((1.70158+1)*i-1.70158)}class kg{constructor(){kt(this,"group");kt(this,"meshMap",new Map);kt(this,"glowMap",new Map);kt(this,"positions",new Map);kt(this,"labelSprites",new Map);kt(this,"hoveredNode",null);kt(this,"selectedNode",null);kt(this,"colorMode","type");kt(this,"materializingNodes",[]);kt(this,"dissolvingNodes",[]);kt(this,"growingNodes",[]);this.group=new zi}setColorMode(t){if(this.colorMode!==t){this.colorMode=t;for(const[e,n]of this.meshMap){const s=n.userData.retention??0,r=n.userData.type??"fact",a=Array.isArray(n.userData.tags)?n.userData.tags:[],l=Xl({type:r,retention:s,tags:a},t),c=new ot(l),h=n.material;h.color.copy(c),h.emissive.copy(c);const d=this.glowMap.get(e);d&&d.material.color.copy(c)}}}createNodes(t){const e=(1+Math.sqrt(5))/2,n=t.length;for(let s=0;s0,o=new Lr(s,16,16),l=new mu({color:new ot(r),emissive:new ot(r),emissiveIntensity:a?0:.3+t.retention*.5,roughness:.3,metalness:.1,transparent:!0,opacity:a?.2:.3+t.retention*.7}),c=new be(o,l);c.position.copy(e),c.scale.setScalar(n),c.userData={nodeId:t.id,type:t.type,retention:t.retention,tags:t.tags},this.meshMap.set(t.id,c),this.group.add(c);const h=new Xi({map:mo(),color:new ot(r),transparent:!0,opacity:n>0?a?.1:.3+t.retention*.35:0,blending:Le,depthWrite:!1}),d=new Bi(h);d.scale.set(s*6*n,s*6*n,1),d.position.copy(e),d.userData={isGlow:!0,nodeId:t.id},this.glowMap.set(t.id,d),this.group.add(d);const p=t.label||t.type,u=this.createTextSprite(p,"#94a3b8");return u.position.copy(e),u.position.y+=s*2+1.5,u.userData={isLabel:!0,nodeId:t.id,offset:s*2+1.5},this.group.add(u),this.labelSprites.set(t.id,u),{mesh:c,glow:d,label:u,size:s}}addNode(t,e,n={}){const s=(e==null?void 0:e.clone())??new P((Math.random()-.5)*40,(Math.random()-.5)*40,(Math.random()-.5)*40);this.positions.set(t.id,s);const{mesh:r,glow:a,label:o}=this.createNodeMeshes(t,s,0);return r.scale.setScalar(.001),a.scale.set(.001,.001,1),a.material.opacity=0,o.material.opacity=0,n.isBirthRitual?(r.visible=!1,a.visible=!1,o.visible=!1,r.userData.birthRitualPending={totalFrames:30,targetScale:.5+t.retention*2}):this.materializingNodes.push({id:t.id,frame:0,totalFrames:30,mesh:r,glow:a,label:o,targetScale:.5+t.retention*2}),s}igniteNode(t){const e=this.meshMap.get(t),n=this.glowMap.get(t),s=this.labelSprites.get(t);if(!e||!n||!s)return;const r=e.userData.birthRitualPending;r&&(e.visible=!0,n.visible=!0,s.visible=!0,delete e.userData.birthRitualPending,this.materializingNodes.push({id:t,frame:0,totalFrames:r.totalFrames,mesh:e,glow:n,label:s,targetScale:r.targetScale}))}removeNode(t){const e=this.meshMap.get(t),n=this.glowMap.get(t),s=this.labelSprites.get(t);!e||!n||!s||(this.materializingNodes=this.materializingNodes.filter(r=>r.id!==t),this.dissolvingNodes.push({id:t,frame:0,totalFrames:60,mesh:e,glow:n,label:s,originalScale:e.scale.x}))}growNode(t,e){const n=this.meshMap.get(t);if(!n)return;const s=n.scale.x,r=.5+e*2;n.userData.retention=e,this.growingNodes.push({id:t,frame:0,totalFrames:30,startScale:s,targetScale:r})}createTextSprite(t,e){const n=document.createElement("canvas"),s=n.getContext("2d");if(!s){const f=new Pe;return new Bi(new Xi({map:f,transparent:!0,opacity:0}))}n.width=512,n.height=64;const r=t.length>40?t.slice(0,37)+"...":t;s.clearRect(0,0,n.width,n.height),s.font='600 22px -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif';const o=s.measureText(r).width,c=Math.min(o+14*2,n.width-4),h=40,d=(n.width-c)/2,p=(n.height-h)/2,u=h/2;s.fillStyle="rgba(10, 16, 28, 0.82)",s.beginPath(),s.moveTo(d+u,p),s.lineTo(d+c-u,p),s.quadraticCurveTo(d+c,p,d+c,p+u),s.lineTo(d+c,p+h-u),s.quadraticCurveTo(d+c,p+h,d+c-u,p+h),s.lineTo(d+u,p+h),s.quadraticCurveTo(d,p+h,d,p+h-u),s.lineTo(d,p+u),s.quadraticCurveTo(d,p,d+u,p),s.closePath(),s.fill(),s.strokeStyle="rgba(148, 163, 184, 0.18)",s.lineWidth=1,s.stroke(),s.textAlign="center",s.textBaseline="middle",s.fillStyle=e,s.fillText(r,n.width/2,n.height/2+1);const g=new bc(n);g.needsUpdate=!0;const _=new Xi({map:g,transparent:!0,opacity:0,depthTest:!1,sizeAttenuation:!0}),m=new Bi(_);return m.scale.set(9,1.2,1),m}updatePositions(){this.group.children.forEach(t=>{if(t.userData.nodeId){const e=this.positions.get(t.userData.nodeId);if(!e)return;t.userData.isGlow?t.position.copy(e):t.userData.isLabel?(t.position.copy(e),t.position.y+=t.userData.offset):t instanceof be&&t.position.copy(e)}})}animate(t,e,n,s=1){var a,o;for(let l=this.materializingNodes.length-1;l>=0;l--){const c=this.materializingNodes[l];c.frame++;const h=Math.min(c.frame/c.totalFrames,1),d=Yl(h);if(c.mesh.scale.setScalar(Math.max(.001,d)),c.frame>=5){const p=Math.min((c.frame-5)/5,1),u=c.glow.material;u.opacity=p*.4;const g=c.targetScale*6*d;c.glow.scale.set(g,g,1)}if(c.frame>=40){const p=Math.min((c.frame-40)/20,1);c.label.material.opacity=p*.9}c.frame>=60&&this.materializingNodes.splice(l,1)}for(let l=this.dissolvingNodes.length-1;l>=0;l--){const c=this.dissolvingNodes[l];c.frame++;const h=Math.min(c.frame/c.totalFrames,1),d=1-zg(h),p=Math.max(.001,c.originalScale*d);c.mesh.scale.setScalar(p);const u=p*6;c.glow.scale.set(u,u,1);const g=c.mesh.material;g.opacity*=.97,c.glow.material.opacity*=.95,c.label.material.opacity*=.93,c.frame>=c.totalFrames&&(this.group.remove(c.mesh),this.group.remove(c.glow),this.group.remove(c.label),c.mesh.geometry.dispose(),c.mesh.material.dispose(),(a=c.glow.material.map)==null||a.dispose(),c.glow.material.dispose(),(o=c.label.material.map)==null||o.dispose(),c.label.material.dispose(),this.meshMap.delete(c.id),this.glowMap.delete(c.id),this.labelSprites.delete(c.id),this.positions.delete(c.id),this.dissolvingNodes.splice(l,1))}for(let l=this.growingNodes.length-1;l>=0;l--){const c=this.growingNodes[l];c.frame++;const h=Math.min(c.frame/c.totalFrames,1),d=c.startScale+(c.targetScale-c.startScale)*Yl(h),p=this.meshMap.get(c.id);p&&p.scale.setScalar(d);const u=this.glowMap.get(c.id);if(u){const g=d*6;u.scale.set(g,g,1)}c.frame>=c.totalFrames&&this.growingNodes.splice(l,1)}const r=new Set([...this.materializingNodes.map(l=>l.id),...this.dissolvingNodes.map(l=>l.id),...this.growingNodes.map(l=>l.id)]);this.meshMap.forEach((l,c)=>{if(r.has(c))return;const h=e.find(T=>T.id===c);if(!h)return;const d=1+Math.sin(t*1.5+e.indexOf(h)*.5)*.15*h.retention;l.scale.setScalar(d);const p=this.positions.get(c),u=p?n.position.distanceTo(p):0,g=1+Math.min(1.4,Math.max(0,(u-60)/100)),_=l.material;if(c===this.hoveredNode)_.emissiveIntensity=1*s;else if(c===this.selectedNode)_.emissiveIntensity=.8*s;else{const E=.3+h.retention*.5+Math.sin(t*(.8+h.retention*.7))*.1*h.retention;_.emissiveIntensity=E*s*g}const m=.3+h.retention*.7;_.opacity=Math.min(1,m*s*g);const f=this.glowMap.get(c);if(f){const T=f.material,E=.3+h.retention*.35;T.opacity=Math.min(.95,E*s*g)}}),this.labelSprites.forEach((l,c)=>{if(r.has(c))return;const h=this.positions.get(c);if(!h)return;const d=n.position.distanceTo(h),p=l.material,u=c===this.hoveredNode||c===this.selectedNode?1:d<40?.9:d<80?.9*(1-(d-40)/40):0;p.opacity+=(u-p.opacity)*.1})}getMeshes(){return Array.from(this.meshMap.values())}dispose(){this.group.traverse(t=>{var e,n,s,r,a;t instanceof be?((e=t.geometry)==null||e.dispose(),(n=t.material)==null||n.dispose()):t instanceof Bi&&((r=(s=t.material)==null?void 0:s.map)==null||r.dispose(),(a=t.material)==null||a.dispose())}),this.materializingNodes=[],this.dissolvingNodes=[],this.growingNodes=[]}}function Hg(i){return 1-Math.pow(1-i,3)}class Vg{constructor(){kt(this,"group");kt(this,"growingEdges",[]);kt(this,"dissolvingEdges",[]);this.group=new zi}createEdges(t,e){for(const n of t){const s=e.get(n.source),r=e.get(n.target);if(!s||!r)continue;const a=[s,r],o=new ge().setFromPoints(a),l=new Ar({color:9133302,transparent:!0,opacity:Math.min(.25+n.weight*.5,.8),blending:Le,depthWrite:!1}),c=new co(o,l);c.userData={source:n.source,target:n.target},this.group.add(c)}}addEdge(t,e){const n=e.get(t.source),s=e.get(t.target);if(!n||!s)return;const r=[n.clone(),n.clone()],a=new ge().setFromPoints(r),o=new Ar({color:9133302,transparent:!0,opacity:0,blending:Le,depthWrite:!1}),l=new co(a,o);l.userData={source:t.source,target:t.target},this.group.add(l),this.growingEdges.push({line:l,source:t.source,target:t.target,frame:0,totalFrames:45})}removeEdgesForNode(t){const e=[];this.group.children.forEach(n=>{const s=n;(s.userData.source===t||s.userData.target===t)&&e.push(s)});for(const n of e)this.growingEdges=this.growingEdges.filter(s=>s.line!==n),this.dissolvingEdges.push({line:n,frame:0,totalFrames:40})}animateEdges(t){for(let e=this.growingEdges.length-1;e>=0;e--){const n=this.growingEdges[e];n.frame++;const s=Hg(Math.min(n.frame/n.totalFrames,1)),r=t.get(n.source),a=t.get(n.target);if(!r||!a)continue;const o=r.clone().lerp(a,s),l=n.line.geometry.attributes.position;l.setXYZ(0,r.x,r.y,r.z),l.setXYZ(1,o.x,o.y,o.z),l.needsUpdate=!0;const c=n.line.material;c.opacity=s*.65,n.frame>=n.totalFrames&&(c.opacity=.65,this.growingEdges.splice(e,1))}for(let e=this.dissolvingEdges.length-1;e>=0;e--){const n=this.dissolvingEdges[e];n.frame++;const s=n.frame/n.totalFrames,r=n.line.material;r.opacity=Math.max(0,.65*(1-s)),n.frame>=n.totalFrames&&(this.group.remove(n.line),n.line.geometry.dispose(),n.line.material.dispose(),this.dissolvingEdges.splice(e,1))}}updatePositions(t){this.group.children.forEach(e=>{const n=e;if(this.growingEdges.some(a=>a.line===n)||this.dissolvingEdges.some(a=>a.line===n))return;const s=t.get(n.userData.source),r=t.get(n.userData.target);if(s&&r){const a=n.geometry.attributes.position;a.setXYZ(0,s.x,s.y,s.z),a.setXYZ(1,r.x,r.y,r.z),a.needsUpdate=!0}})}dispose(){this.group.children.forEach(t=>{var n,s;const e=t;(n=e.geometry)==null||n.dispose(),(s=e.material)==null||s.dispose()}),this.growingEdges=[],this.dissolvingEdges=[]}}class Gg{constructor(t){kt(this,"starField");kt(this,"neuralParticles");this.starField=this.createStarField(),this.neuralParticles=this.createNeuralParticles(),t.add(this.starField),t.add(this.neuralParticles)}createStarField(){const e=new ge,n=new Float32Array(3e3*3),s=new Float32Array(3e3);for(let a=0;a<3e3;a++)n[a*3]=(Math.random()-.5)*1e3,n[a*3+1]=(Math.random()-.5)*1e3,n[a*3+2]=(Math.random()-.5)*1e3,s[a]=Math.random()*1.5;e.setAttribute("position",new he(n,3)),e.setAttribute("size",new he(s,1));const r=new oi({color:6514417,size:.5,transparent:!0,opacity:.4,sizeAttenuation:!0,blending:Le});return new Yi(e,r)}createNeuralParticles(){const e=new ge,n=new Float32Array(500*3),s=new Float32Array(500*3);for(let a=0;a<500;a++)n[a*3]=(Math.random()-.5)*100,n[a*3+1]=(Math.random()-.5)*100,n[a*3+2]=(Math.random()-.5)*100,s[a*3]=.4+Math.random()*.3,s[a*3+1]=.3+Math.random()*.2,s[a*3+2]=.8+Math.random()*.2;e.setAttribute("position",new he(n,3)),e.setAttribute("color",new he(s,3));const r=new oi({size:.3,vertexColors:!0,transparent:!0,opacity:.4,blending:Le,sizeAttenuation:!0});return new Yi(e,r)}animate(t){this.starField.rotation.y+=1e-4,this.starField.rotation.x+=5e-5;const e=this.neuralParticles.geometry.attributes.position;for(let n=0;n=0;s--){const r=this.pulseEffects[s];if(r.intensity-=r.decay,r.intensity<=0){this.pulseEffects.splice(s,1);continue}const a=t.get(r.nodeId);if(a){const o=a.material;o.emissive.lerp(r.color,r.intensity*.3),o.emissiveIntensity=Math.max(o.emissiveIntensity,r.intensity)}}for(let s=this.spawnBursts.length-1;s>=0;s--){const r=this.spawnBursts[s];if(r.age++,r.age>120){this.scene.remove(r.particles),r.particles.geometry.dispose(),r.particles.material.dispose(),this.spawnBursts.splice(s,1);continue}const a=r.particles.geometry.attributes.position,o=r.particles.geometry.attributes.velocity;for(let c=0;c=0;s--){const r=this.rainbowBursts[s];if(r.age++,r.age>r.maxAge){this.scene.remove(r.particles),r.particles.geometry.dispose(),r.particles.material.dispose(),this.rainbowBursts.splice(s,1);continue}const a=r.particles.geometry.attributes.position,o=r.particles.geometry.attributes.velocity;for(let p=0;p=0;s--){const r=this.rippleWaves[s];if(r.age++,r.radius+=r.speed,r.age>r.maxAge){this.rippleWaves.splice(s,1);continue}const a=r.radius,o=3;n.forEach((l,c)=>{if(r.pulsedNodes.has(c))return;const h=l.distanceTo(r.origin);h>=a-o&&h<=a+o&&(r.pulsedNodes.add(c),this.addPulse(c,.8,new ot(65489),.03))})}for(let s=this.implosions.length-1;s>=0;s--){const r=this.implosions[s];if(r.age++,r.age>r.maxAge+20){this.scene.remove(r.particles),r.particles.geometry.dispose(),r.particles.material.dispose(),r.flash&&(this.scene.remove(r.flash),r.flash.geometry.dispose(),r.flash.material.dispose()),this.implosions.splice(s,1);continue}if(r.age<=r.maxAge){const a=r.particles.geometry.attributes.position,o=r.particles.geometry.attributes.velocity,l=1+r.age*.02;for(let h=0;hr.maxAge){const a=(r.age-r.maxAge)/20;r.flash.material.opacity=Math.max(0,1-a),r.flash.scale.setScalar(1+a*3)}}for(let s=this.shockwaves.length-1;s>=0;s--){const r=this.shockwaves[s];if(r.age++,r.age>r.maxAge){this.scene.remove(r.mesh),r.mesh.geometry.dispose(),r.mesh.material.dispose(),this.shockwaves.splice(s,1);continue}const a=r.age/r.maxAge;r.mesh.scale.setScalar(1+a*20),r.mesh.material.opacity=.8*(1-a),r.mesh.lookAt(e.position)}for(let s=this.connectionFlashes.length-1;s>=0;s--){const r=this.connectionFlashes[s];if(r.intensity-=.015,r.intensity<=0){this.scene.remove(r.line),r.line.geometry.dispose(),r.line.material.dispose(),this.connectionFlashes.splice(s,1);continue}r.line.material.opacity=r.intensity}for(let s=this.birthOrbs.length-1;s>=0;s--){const r=this.birthOrbs[s];r.age++;const a=r.gestationFrames+r.flightFrames,o=r.sprite.material,l=r.core.material,c=r.getTargetPos();if(c)r.lastTargetPos.copy(c);else if(r.age>r.gestationFrames&&!r.aborted){r.aborted=!0;const h=r.sprite.position;o.color.setRGB(1,.15,.2),l.color.setRGB(1,.6,.6),this.createImplosion(h,new ot(16721203)),r.arriveFired=!0,r.age=a+1}if(r.age<=r.gestationFrames){const h=r.age/r.gestationFrames,d=1-Math.pow(1-h,3),p=.85+Math.sin(r.age*.35)*.15,u=.5+d*4.5*p,g=.2+d*1.8*p;r.sprite.scale.set(u,u,1),r.core.scale.set(g,g,1),o.opacity=d*.95,l.opacity=d,o.color.copy(r.color).multiplyScalar(.7+d*.3),r.sprite.position.copy(r.startPos),r.core.position.copy(r.startPos)}else if(r.age<=a){const h=(r.age-r.gestationFrames)/r.flightFrames,d=h<.5?2*h*h:1-Math.pow(-2*h+2,2)/2,p=r.startPos,u=r.lastTargetPos,g=u.x-p.x,_=u.y-p.y,m=u.z-p.z,f=Math.sqrt(g*g+_*_+m*m),T=(p.x+u.x)*.5,E=(p.y+u.y)*.5+30+f*.15,y=(p.z+u.z)*.5,D=1-d,w=D*D,C=2*D*d,I=d*d,S=w*p.x+C*T+I*u.x,M=w*p.y+C*E+I*u.y,A=w*p.z+C*y+I*u.z;r.sprite.position.set(S,M,A),r.core.position.set(S,M,A);const W=1-d*.35;r.sprite.scale.setScalar(5*W),r.core.scale.setScalar(2*W),o.opacity=.95,l.opacity=1,o.color.copy(r.color)}else if(r.arriveFired){const h=r.age-a,d=Math.max(0,1-h/8);o.opacity=.95*d,l.opacity=1*d,r.sprite.scale.setScalar(5*(1+(1-d)*2)),d<=0&&(this.scene.remove(r.sprite),this.scene.remove(r.core),o.dispose(),l.dispose(),this.birthOrbs.splice(s,1))}else{r.arriveFired=!0;try{r.onArrive()}catch(h){console.warn("[birth-orb] onArrive threw",h)}}}}dispose(){for(const t of this.spawnBursts)this.scene.remove(t.particles),t.particles.geometry.dispose(),t.particles.material.dispose();for(const t of this.rainbowBursts)this.scene.remove(t.particles),t.particles.geometry.dispose(),t.particles.material.dispose();for(const t of this.implosions)this.scene.remove(t.particles),t.particles.geometry.dispose(),t.particles.material.dispose(),t.flash&&(this.scene.remove(t.flash),t.flash.geometry.dispose(),t.flash.material.dispose());for(const t of this.shockwaves)this.scene.remove(t.mesh),t.mesh.geometry.dispose(),t.mesh.material.dispose();for(const t of this.connectionFlashes)this.scene.remove(t.line),t.line.geometry.dispose(),t.line.material.dispose();for(const t of this.birthOrbs)this.scene.remove(t.sprite),this.scene.remove(t.core),t.sprite.material.dispose(),t.core.material.dispose();this.pulseEffects=[],this.spawnBursts=[],this.rainbowBursts=[],this.rippleWaves=[],this.implosions=[],this.shockwaves=[],this.connectionFlashes=[],this.birthOrbs=[]}}const wn={bloomStrength:.8,rotateSpeed:.3,fogColor:328976,fogDensity:.008,nebulaIntensity:0,chromaticIntensity:.002,vignetteRadius:.9,breatheAmplitude:1},zn={bloomStrength:1.8,rotateSpeed:.08,fogColor:656672,fogDensity:.006,nebulaIntensity:1,chromaticIntensity:.005,vignetteRadius:.7,breatheAmplitude:2};class Xg{constructor(){kt(this,"active",!1);kt(this,"transition",0);kt(this,"transitionSpeed",.008);kt(this,"current");kt(this,"auroraHue",0);this.current={...wn}}setActive(t){this.active=t}update(t,e,n,s,r){const a=this.active?1:0;this.transition+=(a-this.transition)*this.transitionSpeed*60*(1/60),this.transition=Math.max(0,Math.min(1,this.transition));const o=this.transition;this.current.bloomStrength=this.lerp(wn.bloomStrength,zn.bloomStrength,o),this.current.rotateSpeed=this.lerp(wn.rotateSpeed,zn.rotateSpeed,o),this.current.fogDensity=this.lerp(wn.fogDensity,zn.fogDensity,o),this.current.nebulaIntensity=this.lerp(wn.nebulaIntensity,zn.nebulaIntensity,o),this.current.chromaticIntensity=this.lerp(wn.chromaticIntensity,zn.chromaticIntensity,o),this.current.vignetteRadius=this.lerp(wn.vignetteRadius,zn.vignetteRadius,o),this.current.breatheAmplitude=this.lerp(wn.breatheAmplitude,zn.breatheAmplitude,o),e.strength=this.current.bloomStrength,n.autoRotateSpeed=this.current.rotateSpeed;const l=new ot(wn.fogColor),c=new ot(zn.fogColor),h=l.clone().lerp(c,o);if(t.fog=new Dr(h,this.current.fogDensity),o>.01){this.auroraHue=r*.1%1;const d=new ot().setHSL(.75+this.auroraHue*.15,.8,.5),p=new ot().setHSL(.55+this.auroraHue*.2,.7,.4);s.point1.color.lerp(d,o*.3),s.point2.color.lerp(p,o*.3)}else s.point1.color.set(6514417),s.point2.color.set(11032055)}lerp(t,e,n){return t+(e-t)*n}}const Yg=50,xs=[];function qg(i,t,e){const n=i.tags??[],s=i.type??"";let r=null,a=0;for(const o of t){let l=0;o.type===s&&(l+=2);for(const c of o.tags)n.includes(c)&&(l+=1);l>a&&(a=l,r=o.id)}if(r&&a>0){const o=e.get(r);if(o)return new P(o.x+(Math.random()-.5)*10,o.y+(Math.random()-.5)*10,o.z+(Math.random()-.5)*10)}return new P((Math.random()-.5)*40,(Math.random()-.5)*40,(Math.random()-.5)*40)}function jg(i,t){if(xs.length<=Yg)return;const e=xs.shift();i.edgeManager.removeEdgesForNode(e),i.nodeManager.removeNode(e),i.forceSim.removeNode(e),i.onMutation({type:"edgesRemoved",nodeId:e}),i.onMutation({type:"nodeRemoved",nodeId:e});const n=t.findIndex(s=>s.id===e);n!==-1&&t.splice(n,1)}function Zg(i,t,e){var d,p;const{effects:n,nodeManager:s,edgeManager:r,forceSim:a,camera:o,onMutation:l}=t,c=s.positions,h=s.meshMap;switch(i.type){case"MemoryCreated":{const u=i.data;if(!u.id)break;const g={id:u.id,label:(u.content??"").slice(0,60),type:u.node_type??"fact",retention:Math.max(0,Math.min(1,u.retention??.9)),tags:u.tags??[],createdAt:new Date().toISOString(),updatedAt:new Date().toISOString(),isCenter:!1},_=qg(g,e,c),m=s.addNode(g,_,{isBirthRitual:!0});a.addNode(u.id,m),xs.push(u.id),jg(t,e);const f=new ot(Sa[g.type]||"#00ffd1"),T=f.clone();T.offsetHSL(.15,0,0),n.createBirthOrb(o,f,()=>s.positions.get(g.id),()=>{s.igniteNode(g.id);const E=s.positions.get(g.id)??_,y=s.meshMap.get(g.id);y&&y.scale.multiplyScalar(1.8),n.createRainbowBurst(E,f),n.createShockwave(E,f,o),n.createShockwave(E,T,o),n.createRippleWave(E)}),l({type:"nodeAdded",node:g});break}case"ConnectionDiscovered":{const u=i.data;if(!u.source_id||!u.target_id)break;const g=c.get(u.source_id),_=c.get(u.target_id),m={source:u.source_id,target:u.target_id,weight:u.weight??.5,type:u.connection_type??"semantic"};r.addEdge(m,c),g&&_&&n.createConnectionFlash(g,_,new ot(54527)),u.source_id&&h.has(u.source_id)&&n.addPulse(u.source_id,1,new ot(54527),.02),u.target_id&&h.has(u.target_id)&&n.addPulse(u.target_id,1,new ot(54527),.02),l({type:"edgeAdded",edge:m});break}case"MemoryDeleted":{const u=i.data;if(!u.id)break;const g=c.get(u.id);if(g){const m=new ot(16729943);n.createImplosion(g,m)}r.removeEdgesForNode(u.id),s.removeNode(u.id),a.removeNode(u.id);const _=xs.indexOf(u.id);_!==-1&&xs.splice(_,1),l({type:"edgesRemoved",nodeId:u.id}),l({type:"nodeRemoved",nodeId:u.id});break}case"MemoryPromoted":{const u=i.data,g=u==null?void 0:u.id;if(!g)break;const _=u.new_retention??.95;if(h.has(g)){s.growNode(g,_),n.addPulse(g,1.2,new ot(65416),.01);const m=c.get(g);m&&(n.createShockwave(m,new ot(65416),o),n.createSpawnBurst(m,new ot(65416))),l({type:"nodeUpdated",nodeId:g,retention:_})}break}case"MemoryDemoted":{const u=i.data,g=u==null?void 0:u.id;if(!g)break;const _=u.new_retention??.3;h.has(g)&&(s.growNode(g,_),n.addPulse(g,.8,new ot(16729943),.03),l({type:"nodeUpdated",nodeId:g,retention:_}));break}case"MemoryUpdated":{const u=i.data,g=u==null?void 0:u.id;if(!g||!h.has(g))break;n.addPulse(g,.6,new ot(8490232),.02),u.retention!==void 0&&(s.growNode(g,u.retention),l({type:"nodeUpdated",nodeId:g,retention:u.retention}));break}case"SearchPerformed":{h.forEach((u,g)=>{n.addPulse(g,.6+Math.random()*.4,new ot(8490232),.02)});break}case"DreamStarted":{h.forEach((u,g)=>{n.addPulse(g,1,new ot(11032055),.005)});break}case"DreamProgress":{const u=(d=i.data)==null?void 0:d.memory_id;u&&h.has(u)&&n.addPulse(u,1.5,new ot(12616956),.01);break}case"DreamCompleted":{n.createSpawnBurst(new P(0,0,0),new ot(11032055)),n.createShockwave(new P(0,0,0),new ot(11032055),o);break}case"RetentionDecayed":{const u=(p=i.data)==null?void 0:p.id;u&&h.has(u)&&n.addPulse(u,.8,new ot(16729943),.03);break}case"ConsolidationCompleted":{h.forEach((u,g)=>{n.addPulse(g,.4+Math.random()*.3,new ot(16758784),.015)});break}case"ActivationSpread":{const u=i.data;if(u.source_id&&u.target_ids){const g=c.get(u.source_id);if(g)for(const _ of u.target_ids){const m=c.get(_);m&&n.createConnectionFlash(g,m,new ot(1370310))}}break}case"MemorySuppressed":{const u=i.data;if(!u.id)break;const g=c.get(u.id);if(g){n.createImplosion(g,new ot(11032055));const _=Math.max(1,u.suppression_count??1),m=Math.min(.4+_*.15,1);n.addPulse(u.id,m,new ot(11032055),.04)}break}case"MemoryUnsuppressed":{const u=i.data;if(!u.id)break;const g=c.get(u.id);g&&h.has(u.id)&&(n.createRainbowBurst(g,new ot(65416)),n.addPulse(u.id,1,new ot(65416),.02));break}case"Rac1CascadeSwept":{const g=i.data.neighbors_affected??0;if(g===0)break;const _=Array.from(h.keys()),m=Math.min(g,_.length,12);for(let f=0;f"u")return Mr;const i=localStorage.getItem(Fc);if(i===null)return Mr;const t=Number(i);return Number.isFinite(t)?Math.min(_o,Math.max(go,t)):Mr}const ni=a_();function a_(){let i=me(!1),t=me(va(new Date)),e=me(!1),n=me(1),s=me(!1),r=me(va(r_()));return{get temporalEnabled(){return B(i)},set temporalEnabled(a){zt(i,a,!0)},get temporalDate(){return B(t)},set temporalDate(a){zt(t,a,!0)},get temporalPlaying(){return B(e)},set temporalPlaying(a){zt(e,a,!0)},get temporalSpeed(){return B(n)},set temporalSpeed(a){zt(n,a,!0)},get dreamMode(){return B(s)},set dreamMode(a){zt(s,a,!0)},get brightness(){return B(r)},set brightness(a){const o=Math.min(_o,Math.max(go,a));if(zt(r,o,!0),typeof localStorage<"u")try{localStorage.setItem(Fc,String(o))}catch{}},brightnessMin:go,brightnessMax:_o,brightnessDefault:Mr}}var o_=Se('
      ');function l_(i,t){ys(t,!0);let e=vs(t,"events",19,()=>[]),n=vs(t,"isDreaming",3,!1),s=vs(t,"colorMode",3,"type");kc(()=>{l==null||l.setColorMode(s())});let r,a,o,l,c,h,d,p,u,g,_,m=null,f=[];jl(()=>{a=Dg(r),g=Jg(a.scene).material,_=i_(a.composer),h=new Gg(a.scene),l=new kg,l.colorMode=s(),c=new Vg,d=new Wg(a.scene),u=new Xg;const M=l.createNodes(t.nodes);c.createEdges(t.edges,M),p=new Ig(M),f=[...t.nodes],a.scene.add(c.group),a.scene.add(l.group),E(),window.addEventListener("resize",D),r.addEventListener("pointermove",w),r.addEventListener("click",C)}),Zl(()=>{cancelAnimationFrame(o),window.removeEventListener("resize",D),r==null||r.removeEventListener("pointermove",w),r==null||r.removeEventListener("click",C),d==null||d.dispose(),h==null||h.dispose(),l==null||l.dispose(),c==null||c.dispose(),a&&Ug(a)});let T=0;function E(){o=requestAnimationFrame(E);const S=performance.now();T===0&&(T=S);const M=S-T;if(M<16)return;T=S-M%16;const A=S*.001;p.tick(t.edges),l.updatePositions(),c.updatePositions(l.positions),c.animateEdges(l.positions),h.animate(A),l.animate(A,f,a.camera,ni.brightness),u.setActive(n()),u.update(a.scene,a.bloomPass,a.controls,a.lights,A),Qg(g,A,u.current.nebulaIntensity,r.clientWidth,r.clientHeight),s_(_,A,u.current.nebulaIntensity),y(),d.update(l.meshMap,a.camera,l.positions),a.controls.update(),a.composer.render()}function y(){if(!e()||e().length===0)return;const S=[];for(const A of e()){if(A===m)break;S.push(A)}if(S.length===0)return;if(S.length===e().length&&e().length>=200){console.warn("[vestige] Event horizon overflow: dropping visuals for",S.length,"events"),m=e()[0];return}m=e()[0];const M={effects:d,nodeManager:l,edgeManager:c,forceSim:p,camera:a.camera,onMutation:A=>{var W;A.type==="nodeAdded"?f=[...f,A.node]:A.type==="nodeRemoved"&&(f=f.filter(k=>k.id!==A.nodeId)),(W=t.onGraphMutation)==null||W.call(t,A)}};for(let A=S.length-1;A>=0;A--)Zg(S[A],M,f)}function D(){!r||!a||Lg(a,r)}function w(S){const M=r.getBoundingClientRect();a.mouse.x=(S.clientX-M.left)/M.width*2-1,a.mouse.y=-((S.clientY-M.top)/M.height)*2+1,a.raycaster.setFromCamera(a.mouse,a.camera);const A=a.raycaster.intersectObjects(l.getMeshes());A.length>0?(l.hoveredNode=A[0].object.userData.nodeId,r.style.cursor="pointer"):(l.hoveredNode=null,r.style.cursor="grab")}function C(){var S;if(l.hoveredNode){l.selectedNode=l.hoveredNode,(S=t.onSelect)==null||S.call(t,l.hoveredNode);const M=l.positions.get(l.hoveredNode);M&&a.controls.target.lerp(M.clone(),.5)}}var I=o_();Yc(I,S=>r=S,()=>r),_e(i,I),Es()}var c_=Se('
      '),h_=Se('
      ');function u_(i,t){ys(t,!0);let e=vs(t,"width",3,240),n=vs(t,"height",3,80);function s(m){return t.stability<=0?0:Math.exp(-m/t.stability)}let r=Gn(()=>{const m=[],f=Math.max(t.stability*3,30),T=4,E=e()-T*2,y=n()-T*2;for(let D=0;D<=50;D++){const w=D/50*f,C=s(w),I=T+D/50*E,S=T+(1-C)*y;m.push(`${D===0?"M":"L"}${I.toFixed(1)},${S.toFixed(1)}`)}return m.join(" ")}),a=Gn(()=>[{label:"Now",days:0,value:t.retention},{label:"1d",days:1,value:s(1)},{label:"7d",days:7,value:s(7)},{label:"30d",days:30,value:s(30)}]);function o(m){return m>.7?"#10b981":m>.4?"#f59e0b":"#ef4444"}var l=h_(),c=yt(l),h=yt(c),d=bt(h),p=bt(d),u=bt(p),g=bt(u);Hc(),xt(c);var _=bt(c,2);_s(_,21,()=>B(a),hr,(m,f)=>{var T=c_(),E=yt(T),y=yt(E);xt(E);var D=bt(E,2),w=yt(D);xt(D),xt(T),Ke((C,I)=>{fe(y,`${B(f).label??""}:`),Sr(D,`color: ${C??""}`),fe(w,`${I??""}%`)},[()=>o(B(f).value),()=>(B(f).value*100).toFixed(0)]),_e(m,T)}),xt(_),xt(l),Ke(m=>{ve(c,"width",e()),ve(c,"height",n()),ve(c,"viewBox",`0 0 ${e()??""} ${n()??""}`),ve(h,"y1",4+(n()-8)*.5),ve(h,"x2",e()-4),ve(h,"y2",4+(n()-8)*.5),ve(d,"y1",4+(n()-8)*.8),ve(d,"x2",e()-4),ve(d,"y2",4+(n()-8)*.8),ve(p,"d",B(r)),ve(u,"d",`${B(r)??""} L${e()-4},${n()-4} L4,${n()-4} Z`),ve(g,"cy",4+(1-t.retention)*(n()-8)),ve(g,"fill",m)},[()=>o(t.retention)]),_e(i,l),Es()}function ql(i,t,e){const n=e.getTime(),s=new Set,r=new Map,a=i.filter(l=>{const c=new Date(l.createdAt).getTime();if(c<=n){s.add(l.id);const h=n-c,d=1440*60*1e3,p=hs.has(l.source)&&s.has(l.target));return{visibleNodes:a,visibleEdges:o,nodeOpacities:r}}function d_(i){if(i.length===0){const n=new Date;return{oldest:n,newest:n}}let t=1/0,e=-1/0;for(const n of i){const s=new Date(n.createdAt).getTime();se&&(e=s)}return{oldest:new Date(t),newest:new Date(e)}}var f_=Se(`
      `),p_=Se('');function m_(i,t){ys(t,!0);let e=me(!1),n=me(!1),s=me(1),r=me(100),a,o=0,l=Gn(()=>d_(t.nodes)),c=Gn(()=>{const E=B(l).oldest.getTime(),D=B(l).newest.getTime()-E||1;return new Date(E+B(r)/100*D)});function h(E){return E.toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"})}function d(){zt(e,!B(e)),t.onToggle(B(e)),B(e)&&(zt(r,100),t.onDateChange(B(c)))}function p(){zt(n,!B(n)),B(n)?(zt(r,0),o=performance.now(),u()):cancelAnimationFrame(a)}function u(){B(n)&&(a=requestAnimationFrame(E=>{const y=(E-o)/1e3;o=E;const D=B(l).oldest.getTime(),C=(B(l).newest.getTime()-D)/(1440*60*1e3)||1,I=B(s)/C*100;if(zt(r,Math.min(100,B(r)+I*y),!0),t.onDateChange(B(c)),B(r)>=100){zt(n,!1);return}u()}))}function g(){t.onDateChange(B(c))}Zl(()=>{zt(n,!1),cancelAnimationFrame(a)});var _=Gc(),m=Kl(_);{var f=E=>{var y=f_(),D=yt(y),w=yt(D),C=yt(w),I=yt(C),S=yt(I,!0);xt(I);var M=bt(I,2),A=yt(M);A.value=A.__value=1;var W=bt(A);W.value=W.__value=7;var k=bt(W);k.value=k.__value=30,xt(M),xt(C);var q=bt(C,2),Q=yt(q,!0);xt(q);var X=bt(q,2);xt(w);var tt=bt(w,2);xa(tt);var H=bt(tt,2),st=yt(H),gt=yt(st,!0);xt(st);var Et=bt(st,2),Ft=yt(Et,!0);xt(Et),xt(H),xt(D),xt(y),Ke((Xt,Y,nt)=>{fe(S,B(n)?"⏸":"▶"),fe(Q,Xt),fe(gt,Y),fe(Ft,nt)},[()=>h(B(c)),()=>h(B(l).oldest),()=>h(B(l).newest)]),Fe("click",I,p),Jl(M,()=>B(s),Xt=>zt(s,Xt)),Fe("click",X,d),Fe("input",tt,g),Ma(tt,()=>B(r),Xt=>zt(r,Xt)),_e(E,y)},T=E=>{var y=p_();Fe("click",y,d),_e(E,y)};kn(m,E=>{B(e)?E(f):E(T,!1)})}_e(i,_),Es()}$l(["click","input"]);var g_=Se('
      '),__=Se('
      FSRS accessibility
      ');function v_(i,t){ys(t,!1);const e=["active","dormant","silent","unavailable"];qc();var n=__(),s=bt(yt(n),2);_s(s,1,()=>e,r=>r,(r,a)=>{var o=g_(),l=yt(o),c=bt(l,2),h=yt(c,!0);xt(c);var d=bt(c,2),p=yt(d,!0);xt(d),xt(o),Ke(u=>{Sr(l,`background: ${po[B(a)]??""}; box-shadow: 0 0 6px ${po[B(a)]??""}55;`),fe(h,B(a)),fe(p,u)},[()=>{var u;return((u=Fg[B(a)].match(/\(([^)]+)\)/))==null?void 0:u[1])??""}]),_e(r,o)}),xt(n),_e(i,n),Es()}var x_=Se('

      Loading memory graph...

      '),M_=Se(`

      MCP Backend Offline

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

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

      Your Mind Awaits

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

      '),y_=Se('

      Your Mind Awaits

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

      Memory Detail

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

      '),ke=_('Weakest channel: ',1),Me=_('
      Skip

      '),Ae=_('
      Composite
      ',1),Se=_(`

      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%.

      `),Ce=_('
      '),Fe=_('
      '),Ie=_(`

      No standout memories yet

      As you capture decisions, wins, and discoveries, the most important ones - will rise to the top here — ranked by retention, reviews, and recency.

      `),Ne=_('· ',1),Pe=_(' '),Ee=_('
      '),Te=_(``),je=_('
      '),ze=_(`

      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 ea(o,C){Bt(C,!0);let f=H(""),p=H(null),$=H(!1),h=H(null),et=H(0);async function at(){const e=t(f).trim();if(!(!e||t($))){F($,!0),F(h,null);try{F(p,await Tt.importance(e),!0),Jt(et)}catch(a){F(h,a instanceof Error?a.message:String(a),!0),F(p,null)}finally{F($,!1)}}}function Pt(e){(e.metaKey||e.ctrlKey)&&e.key==="Enter"&&(e.preventDefault(),at())}const I={novelty:.25,arousal:.3,reward:.25,attention:.2},K={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 ot=g(()=>t(p)?Object.keys(I).map(a=>({key:a,contribution:t(p).channels[a]*I[a]})).sort((a,v)=>v.contribution-a.contribution)[0]:null),it=g(()=>t(p)?Object.keys(I).slice().sort((e,a)=>t(p).channels[e]-t(p).channels[a])[0]:null),L=H(zt([])),lt=H(!0),kt=zt({});function j(e){const a=Math.max(1,(Date.now()-new Date(e.createdAt).getTime())/864e5),v=e.reviewCount??0,m=1/Math.pow(a,.5);return e.retentionStrength*Math.log1p(v+1)*m}async function dt(){F(lt,!0);try{const a=(await Tt.memories.list({limit:"20"})).memories.slice().sort((v,m)=>j(m)-j(v)).slice(0,20);F(L,a,!0),t(L).forEach(async v=>{try{const m=await Tt.importance(v.content);kt[v.id]=m.channels}catch{}})}catch{F(L,[],!0)}finally{F(lt,!1)}}Lt(dt);function Mt(e){se(`${ne}/memories`)}var G=ze(),ct=n(G);le(ct,{icon:"importance",title:"Importance Radar",subtitle:"4-channel importance model: Novelty · Arousal · Reward · Attention",accent:"warning"});var rt=i(ct,2),At=i(n(rt),2),vt=n(At),l=n(vt);Qt(l);var d=i(l,2),s=n(d),c=n(s,!0);r(s);var w=i(s,4);{var y=e=>{var a=he(),v=n(a,!0);r(a),M(()=>k(v,t(h))),x(e,a)};R(w,e=>{t(h)&&e(y)})}r(d),r(vt);var N=i(vt,2),P=n(N);{var pt=e=>{var a=Ae(),v=Q(a),m=i(n(v),2),xt=n(m);de(xt,{get value(){return t(p).composite},scale:100,decimals:0,suffix:"%"}),r(m),r(v);var W=i(v,2);pe(W,()=>t(et),E=>{$t(E,{get novelty(){return t(p).channels.novelty},get arousal(){return t(p).channels.arousal},get reward(){return t(p).channels.reward},get attention(){return t(p).channels.attention},size:"lg"})});var X=i(W,2);{var ft=E=>{var T=be(),B=n(T),Z=n(B);It(Z,{name:"sparkle",size:18}),St(2),r(B);var U=i(B,2),V=n(U),yt=i(V);{var Y=A=>{var J=we(),D=i(Q(J)),st=n(D,!0);r(D);var _t=i(D);M(()=>{k(st,t(ot).key),k(_t,` — ${K[t(ot).key].high??""}.`)}),x(A,J)};R(yt,A=>{t(ot)&&A(Y)})}r(U),r(T),M(A=>k(V,`Composite ${A??""}% > 60% threshold. `),[()=>(t(p).composite*100).toFixed(0)]),x(E,T)},gt=E=>{var T=Me(),B=n(T),Z=n(B);It(Z,{name:"close",size:18}),St(2),r(B);var U=i(B,2),V=n(U),yt=i(V);{var Y=A=>{var J=ke(),D=i(Q(J)),st=n(D,!0);r(D);var _t=i(D);M(()=>{k(st,t(it)),k(_t,` — ${K[t(it)].low??""}.`)}),x(A,J)};R(yt,A=>{t(it)&&A(Y)})}r(U),r(T),M(A=>k(V,`Composite ${A??""}% < 60% threshold. `),[()=>(t(p).composite*100).toFixed(0)]),x(E,T)};R(X,E=>{t(p).composite>.6?E(ft):E(gt,!1)})}x(e,a)},q=e=>{var a=Se(),v=n(a),m=n(v);It(m,{name:"importance",size:36}),r(v),St(4),r(a),x(e,a)};R(P,e=>{t(p)?e(pt):e(q,!1)})}r(N),Ft(N,e=>{var a;return(a=ce)==null?void 0:a(e)}),r(At),r(rt),Ft(rt,e=>{var a;return(a=Et)==null?void 0:a(e)});var mt=i(rt,2),ut=n(mt),qt=i(n(ut),2);r(ut);var Ht=i(ut,2);{var Kt=e=>{var a=Fe();tt(a,20,()=>Array(6),nt,(v,m)=>{var xt=Ce();x(v,xt)}),r(a),x(e,a)},Gt=e=>{var a=Ie(),v=n(a),m=n(v);It(m,{name:"importance",size:40}),r(v),St(4),r(a),x(e,a)},Wt=e=>{var a=je();tt(a,23,()=>t(L),v=>v.id,(v,m,xt)=>{const W=g(()=>kt[t(m).id]);var X=Te(),ft=n(X),gt=n(ft),E=n(gt),T=i(E,2),B=n(T,!0);r(T);var Z=i(T,4),U=n(Z);r(Z);var V=i(Z,2);{var yt=S=>{var z=Ne(),O=i(Q(z),2),ht=n(O);r(O),M(()=>k(ht,`${t(m).reviewCount??""} reviews`)),x(S,z)};R(V,S=>{t(m).reviewCount&&S(yt)})}r(gt);var Y=i(gt,2),A=n(Y,!0);r(Y);var J=i(Y,2);{var D=S=>{var z=Ee();tt(z,21,()=>t(m).tags.slice(0,4),nt,(O,ht)=>{var b=Pe(),Xt=n(b,!0);r(b),M(()=>k(Xt,t(ht))),x(O,b)}),r(z),x(S,z)};R(J,S=>{t(m).tags.length>0&&S(D)})}r(ft);var st=i(ft,2),_t=n(st);{let S=g(()=>{var b;return((b=t(W))==null?void 0:b.novelty)??0}),z=g(()=>{var b;return((b=t(W))==null?void 0:b.arousal)??0}),O=g(()=>{var b;return((b=t(W))==null?void 0:b.reward)??0}),ht=g(()=>{var b;return((b=t(W))==null?void 0:b.attention)??0});$t(_t,{get novelty(){return t(S)},get arousal(){return t(z)},get reward(){return t(O)},get attention(){return t(ht)},size:"sm"})}r(st),r(X),Ft(X,(S,z)=>{var O;return(O=Et)==null?void 0:O(S,z)},()=>({delay:t(xt)*45})),M(S=>{ae(E,`background: ${(oe[t(m).nodeType]||"#8B95A5")??""}`),k(B,t(m).nodeType),k(U,`${S??""}% retention`),k(A,t(m).content)},[()=>(t(m).retentionStrength*100).toFixed(0)]),Ct("click",X,()=>Mt(t(m).id)),x(v,X)}),r(a),x(e,a)};R(Ht,e=>{t(lt)?e(Kt):t(L).length===0?e(Gt,1):e(Wt,!1)})}r(mt),Ft(mt,e=>{var a;return(a=Et)==null?void 0:a(e)}),r(G),M(e=>{s.disabled=e,k(c,t($)?"Scoring…":"Score Importance")},[()=>t($)||!t(f).trim()]),Ct("keydown",l,Pt),re(l,()=>t(f),e=>F(f,e)),Ct("click",s,at),Ct("click",qt,dt),x(o,G),Dt()}te(["keydown","click"]);export{ea as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/12.CO2CXIFj.js.br b/apps/dashboard/build/_app/immutable/nodes/12.CO2CXIFj.js.br deleted file mode 100644 index 6805664..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/12.CO2CXIFj.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/12.CO2CXIFj.js.gz b/apps/dashboard/build/_app/immutable/nodes/12.CO2CXIFj.js.gz deleted file mode 100644 index 2a3ee30..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/12.CO2CXIFj.js.gz and /dev/null 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..2606b86 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.BQoci-vM.js b/apps/dashboard/build/_app/immutable/nodes/13.BQoci-vM.js deleted file mode 100644 index 4b6d8fb..0000000 --- a/apps/dashboard/build/_app/immutable/nodes/13.BQoci-vM.js +++ /dev/null @@ -1 +0,0 @@ -import"../chunks/Bzak7iHL.js";import{o as ke}from"../chunks/TZu9D97Z.js";import{p as Ae,s as C,d as ae,a as o,b as De,g as r,j as d,e as i,i as D,h as l,n as X,r as a,t as y,u as Ie}from"../chunks/wpu9U-D0.js";import{s as u}from"../chunks/D8mhvFt8.js";import{i as S}from"../chunks/DKve45Wd.js";import{e as F,i as q,s as se}from"../chunks/60_R_Vbt.js";import{a as L,r as re}from"../chunks/P1-U_Xsj.js";import{a as ie}from"../chunks/CZfHMhLI.js";import{P as Ne}from"../chunks/BHDZZvku.js";import{D as Oe}from"../chunks/CmbJHhgy.js";import{I as M}from"../chunks/D7A-gG4Z.js";import{A as Te}from"../chunks/DcKTNC6e.js";import{s as ne}from"../chunks/DPdYG9yN.js";var je=d(' intentions'),Ce=d('
      '),Fe=d('
      '),Le=d('

      '),Me=d(' '),Ue=d(' '),Ve=d('

      '),We=d('
      '),Ye=d('
      '),Be=d('
      '),Ee=d(`

      No predictions yet — keep using Vestige and the predictive model will start surfacing what you'll reach for next.

      `),Ge=d(' '),He=d(' '),Je=d('

      '),Xe=d('
      '),qe=d('

      Prospective Memory

      "Remember to do X when Y happens"

      Predicted Needs

      What you might need next
      ');function vt(le,oe){Ae(oe,!0);let I=C(ae([])),U=C(ae([])),N=C(!0),R=C("active");const de={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"},ve={4:"critical",3:"high",2:"normal",1:"low"},ce={4:"text-decay",3:"text-amber-400",2:"text-dim",1:"text-muted"},pe={time:"schedule",context:"graph",event:"pulse",manual:"intentions"},me=[{value:"active",label:"Active",color:"#6366f1"},{value:"fulfilled",label:"Fulfilled",color:"#10b981"},{value:"snoozed",label:"Snoozed",color:"#a78bfa"},{value:"cancelled",label:"Cancelled",color:"#8B95A5"},{value:"all",label:"All",color:"#00D4FF"}];function xe(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 n=typeof e.filePattern=="string"&&e.filePattern?`/${e.filePattern}`:"";t=`${e.codebase}${n}`}else t=s.trigger_type}catch{t=s.trigger_type}return t.length>40?t.slice(0,37)+"...":t}ke(async()=>{await K()});async function K(){D(N,!0);try{const[s,t]=await Promise.all([ie.intentions(r(R)),ie.predict()]);D(I,s.intentions||[],!0),D(U,t.predictions||[],!0)}catch{}finally{D(N,!1)}}async function ue(s){D(R,s,!0),await K()}function V(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 W=qe(),Q=i(W);Ne(Q,{icon:"intentions",title:"Intentions & Predictions",subtitle:"Prospective memory and the needs Vestige sees coming",accent:"memory",children:(s,t)=>{var e=je(),n=i(e);Te(n,{get value(){return r(I).length}}),X(),a(e),o(s,e)},$$slots:{default:!0}});var Y=l(Q,2),B=i(Y),_e=l(i(B),2);Oe(_e,{get options(){return me},get value(){return r(R)},label:"Status",icon:"filter",onChange:ue}),a(B);var fe=l(B,2);{var ge=s=>{var t=Fe();F(t,20,()=>Array(4),q,(e,n)=>{var _=Ce();o(e,_)}),a(t),o(s,t)},ye=s=>{var t=Le(),e=i(t),n=i(e);M(n,{name:"intentions",size:44,strokeWidth:1.2}),a(e);var _=l(e,2),m=i(_);a(_),a(t),y(()=>u(m,`No ${r(R)==="all"?"":r(R)+" "}intentions yet — say "Remind me…" in conversation and Vestige will hold the thought for you.`)),o(s,t)},he=s=>{var t=We();F(t,23,()=>r(I),e=>e.id,(e,n,_)=>{var m=Ve(),$=i(m),h=i($),O=i(h);{let c=Ie(()=>pe[r(n).trigger_type]||"intentions");M(O,{get name(){return r(c)},size:18})}a(h);var g=l(h,2),k=i(g),T=i(k,!0);a(k);var b=l(k,2),w=i(b),j=i(w,!0);a(w);var z=l(w,2),G=i(z);a(z);var A=l(z,2),x=i(A);a(A);var v=l(A,2);{var f=c=>{var p=Me(),P=i(p);a(p),y(J=>u(P,`deadline: ${J??""}`),[()=>V(r(n).deadline)]),o(c,p)};S(v,c=>{r(n).deadline&&c(f)})}var H=l(v,2);{var Re=c=>{var p=Ue(),P=i(p);a(p),y(J=>u(P,`snoozed until ${J??""}`),[()=>V(r(n).snoozed_until)]),o(c,p)};S(H,c=>{r(n).snoozed_until&&c(Re)})}a(b),a(g);var te=l(g,2),$e=i(te,!0);a(te),a($),a(m),L(m,(c,p)=>{var P;return(P=re)==null?void 0:P(c,p)},()=>({delay:Math.min(r(_)*35,350),y:12})),L(m,c=>{var p;return(p=ne)==null?void 0:p(c)}),y((c,p)=>{u(T,r(n).content),se(w,1,`px-2 py-0.5 text-[10px] rounded-lg border ${(de[r(n).status]||"text-dim bg-white/[0.03] border-subtle/20")??""}`),u(j,r(n).status),se(z,1,`text-[10px] ${(ce[r(n).priority]||"text-muted")??""}`),u(G,`${(ve[r(n).priority]||"normal")??""} priority`),u(x,`${r(n).trigger_type??""}: ${c??""}`),u($e,p)},[()=>xe(r(n)),()=>V(r(n).created_at)]),o(e,m)}),a(t),o(s,t)};S(fe,s=>{r(N)?s(ge):r(I).length===0?s(ye,1):s(he,!1)})}a(Y);var Z=l(Y,2),E=i(Z),ee=i(E),be=i(ee);M(be,{name:"sparkle",size:16}),a(ee),X(4),a(E);var we=l(E,2);{var ze=s=>{var t=Be();F(t,20,()=>Array(3),q,(e,n)=>{var _=Ye();o(e,_)}),a(t),o(s,t)},Pe=s=>{var t=Ee(),e=i(t),n=i(e);M(n,{name:"sparkle",size:40,strokeWidth:1.2}),a(e),X(2),a(t),o(s,t)},Se=s=>{var t=Xe();F(t,21,()=>r(U),q,(e,n,_)=>{var m=Je(),$=i(m),h=i($);h.textContent=_+1;var O=l(h,2),g=i(O),k=i(g,!0);a(g);var T=l(g,2),b=i(T),w=i(b,!0);a(b);var j=l(b,2);{var z=x=>{var v=Ge(),f=i(v);a(v),y(H=>u(f,`${H??""}% retention`),[()=>(Number(r(n).retention)*100).toFixed(0)]),o(x,v)};S(j,x=>{r(n).retention&&x(z)})}var G=l(j,2);{var A=x=>{var v=He(),f=i(v);a(v),y(()=>u(f,`${r(n).predictedNeed??""} need`)),o(x,v)};S(G,x=>{r(n).predictedNeed&&x(A)})}a(T),a(O),a($),a(m),L(m,(x,v)=>{var f;return(f=re)==null?void 0:f(x,v)},()=>({delay:Math.min(_*35,350),y:12})),L(m,x=>{var v;return(v=ne)==null?void 0:v(x)}),y(()=>{u(k,r(n).content),u(w,r(n).nodeType)}),o(e,m)}),a(t),o(s,t)};S(we,s=>{r(N)?s(ze):r(U).length===0?s(Pe,1):s(Se,!1)})}a(Z),a(W),o(le,W),De()}export{vt as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/13.BQoci-vM.js.br b/apps/dashboard/build/_app/immutable/nodes/13.BQoci-vM.js.br deleted file mode 100644 index cecfd72..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/13.BQoci-vM.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/13.BQoci-vM.js.gz b/apps/dashboard/build/_app/immutable/nodes/13.BQoci-vM.js.gz deleted file mode 100644 index 0ffa50a..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/13.BQoci-vM.js.gz and /dev/null 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..0ee2fb1 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..9aaf624 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/14.CDp0vmq1.js b/apps/dashboard/build/_app/immutable/nodes/14.CDp0vmq1.js deleted file mode 100644 index 53090b2..0000000 --- a/apps/dashboard/build/_app/immutable/nodes/14.CDp0vmq1.js +++ /dev/null @@ -1,6 +0,0 @@ -import"../chunks/Bzak7iHL.js";import{o as We}from"../chunks/TZu9D97Z.js";import{p as Ye,s as B,d as $e,i as P,e as s,g as e,r as t,a as l,b as Ge,f as Je,h as v,t as k,u as oe,j as f,au as ne,n as st}from"../chunks/wpu9U-D0.js";import{d as Ke,s as D,a as y}from"../chunks/D8mhvFt8.js";import{i as q}from"../chunks/DKve45Wd.js";import{e as pe,i as Oe,a as V,r as ot,s as Ie}from"../chunks/60_R_Vbt.js";import{a as He,r as nt}from"../chunks/P1-U_Xsj.js";import{s as X}from"../chunks/EqHb-9AZ.js";import{b as it}from"../chunks/CnZzd20v.js";import{a as H}from"../chunks/CZfHMhLI.js";import{N as L}from"../chunks/CcUbQ_Wl.js";import{P as lt}from"../chunks/BHDZZvku.js";import{A as dt}from"../chunks/DcKTNC6e.js";import{D as qe}from"../chunks/CmbJHhgy.js";import{I as Qe}from"../chunks/D7A-gG4Z.js";import{s as ct}from"../chunks/DPdYG9yN.js";const vt={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"}},_e=15;function pt(g){let p=0;for(let m=0;m>>0;return p}function ut(g){let p=g>>>0;return()=>(p=p*1664525+1013904223>>>0,p/4294967296)}function ft(g,p=Date.now(),m){if(!g)return[];const x=ut(pt(g)),S=8+Math.floor(x()*8);if(S<=0)return[];const C=[],E=p-(14+x()*21)*864e5;C.push({action:"created",timestamp:new Date(E).toISOString(),reason:"smart_ingest · prediction-error gate opened",triggered_by:"smart_ingest"});let F=E,i=.5+x()*.2;const N=["accessed","accessed","accessed","accessed","promoted","demoted","edited","dreamed","reconsolidated","suppressed"];for(let z=1;z.5?"search":"deep_reference";break}case"promoted":{const I=i;i=Math.min(1,i+.1),d.old_value=I,d.new_value=i,d.reason="confirmed helpful by user",d.triggered_by="memory(action=promote)";break}case"demoted":{const I=i;i=Math.max(0,i-.15),d.old_value=I,d.new_value=i,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 I=i;i=Math.max(0,i-.08),d.old_value=I,d.new_value=i,d.reason="top-down inhibition (Anderson 2025)",d.triggered_by="suppress(dashboard)";break}case"dreamed":{const I=i;i=Math.min(1,i+.05),d.old_value=I,d.new_value=i,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}C.push(d)}return C.reverse()}function gt(g,p=Date.now()){const m=new Date(g).getTime(),x=Math.max(0,p-m),S=Math.floor(x/1e3);if(S<60)return`${S}s ago`;const C=Math.floor(S/60);if(C<60)return`${C}m ago`;const E=Math.floor(C/60);if(E<24)return`${E}h ago`;const F=Math.floor(E/24);if(F<30)return`${F}d ago`;const i=Math.floor(F/30);return i<12?`${i}mo ago`:`${Math.floor(i/12)}y ago`}function xt(g,p){const m=typeof g=="number"&&Number.isFinite(g),x=typeof p=="number"&&Number.isFinite(p);return!m&&!x?null:!m&&x?`set ${p.toFixed(2)}`:m&&!x?`was ${g.toFixed(2)}`:`${g.toFixed(2)} → ${p.toFixed(2)}`}function mt(g,p){return p||g.length<=_e?{visible:g,hiddenCount:Math.max(0,g.length-_e)}:{visible:g.slice(0,_e),hiddenCount:g.length-_e}}var bt=f('
          '),_t=f('
          '),ht=f('

          Audit trail failed to load.

          '),kt=f('

          No memory selected.

          '),yt=f('

          No audit events recorded yet.

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

            '),qt=f(' '),Qt=f('

            ',1),Wt=f('
            '),Yt=f('
            Content Audit Trail
            Promote Demote Suppress Delete
            '),Gt=f(''),Jt=f('
            '),Kt=f(`
            `);function pa(g,p){Ye(p,!0);let m=B($e([])),x=B(""),S=B(""),C="",E=B(0),F=B(!0),i=B(null),N=$e({}),z;We(()=>O());async function O(){P(F,!0);try{const c={};e(x)&&(c.q=e(x)),e(S)&&(c.node_type=e(S)),e(E)>0&&(c.min_retention=String(e(E)));const M=await H.memories.list(c);P(m,M.memories,!0)}catch{P(m,[],!0)}finally{P(F,!1)}}function d(){clearTimeout(z),z=setTimeout(O,300)}function I(c){return c>.7?"#10b981":c>.4?"#f59e0b":"#ef4444"}const he=[{value:"",label:"All types"},{value:"fact",label:"Fact",color:L.fact},{value:"concept",label:"Concept",color:L.concept},{value:"event",label:"Event",color:L.event},{value:"person",label:"Person",color:L.person},{value:"place",label:"Place",color:L.place},{value:"note",label:"Note",color:L.note},{value:"pattern",label:"Pattern",color:L.pattern},{value:"decision",label:"Decision",color:L.decision}],ke=[{value:"0",label:"Any retention"},{value:"0.3",label:"≥ 30% — fading & up"},{value:"0.5",label:"≥ 50% — half-strength"},{value:"0.7",label:"≥ 70% — well-retained"},{value:"0.9",label:"≥ 90% — core memories"}];let ue=B("0");function _(c){P(E,parseFloat(c),!0),O()}var w=Kt(),Q=s(w);lt(Q,{icon:"memories",title:"Memories",subtitle:"Search, filter, and curate everything Vestige remembers",accent:"memory",children:(c,M)=>{var A=Bt(),n=s(A);dt(n,{get value(){return e(m).length}}),st(),t(A),l(c,A)},$$slots:{default:!0}});var Z=v(Q,2),J=s(Z),R=s(J),h=s(R);Qe(h,{name:"search",size:16}),t(R);var K=v(R,2);ot(K),t(J);var u=v(J,2);qe(u,{get options(){return he},label:"Type",icon:"filter",placeholder:"All types",onChange:O,get value(){return e(S)},set value(c){P(S,c,!0)}});var fe=v(u,2);qe(fe,{get options(){return ke},label:"Retention",icon:"pulse",onChange:_,get value(){return e(ue)},set value(c){P(ue,c,!0)}}),t(Z);var ee=v(Z,2);{var te=c=>{var M=zt();pe(M,20,()=>Array(8),Oe,(A,n)=>{var W=Lt();l(A,W)}),t(M),l(c,M)},ye=c=>{var M=Ht(),A=s(M),n=s(A);Qe(n,{name:"memories",size:48,strokeWidth:1.2}),t(A);var W=v(A,2),j=s(W,!0);t(W),t(M),k(()=>D(j,e(x)||e(S)||e(E)>0?"No memories match these filters yet. Try widening your search.":"No memories yet — once Vestige starts remembering, they will surface here, alive and searchable.")),l(c,M)},we=c=>{var M=Jt();pe(M,23,()=>e(m),A=>A.id,(A,n,W)=>{var j=Gt(),ie=s(j),ae=s(ie),U=s(ae),re=s(U),Y=v(re,2),Me=s(Y,!0);t(Y);var Pe=v(Y,2);pe(Pe,17,()=>e(n).tags.slice(0,3),Oe,(b,T)=>{var $=qt(),de=s($,!0);t($),k(()=>D(de,e(T))),l(b,$)}),t(U);var ge=v(U,2),le=s(ge,!0);t(ge),t(ae);var xe=v(ae,2),se=s(xe),Se=s(se);t(se);var me=v(se,2),Ae=s(me);t(me),t(xe),t(ie);var a=v(ie,2);{var r=b=>{const T=oe(()=>N[e(n).id]??"content");var $=Yt(),de=s($),be=s(de),Te=v(be,2);t(de);var Re=v(de,2);{var Ue=o=>{var G=Qt(),ce=Je(G),ve=s(ce,!0);t(ce);var Le=v(ce,2),Fe=s(Le),Xe=s(Fe);t(Fe);var Ne=v(Fe,2),Ze=s(Ne);t(Ne);var ze=v(Ne,2),et=s(ze);t(ze),t(Le),k((tt,at,rt)=>{D(ve,e(n).content),D(Xe,`Storage: ${tt??""}%`),D(Ze,`Retrieval: ${at??""}%`),D(et,`Created: ${rt??""}`)},[()=>(e(n).storageStrength*100).toFixed(1),()=>(e(n).retrievalStrength*100).toFixed(1),()=>new Date(e(n).createdAt).toLocaleDateString()]),l(o,G)},Ve=o=>{var G=Wt(),ce=s(G);jt(ce,{get memoryId(){return e(n).id}}),t(G),y("click",G,ve=>ve.stopPropagation()),y("keydown",G,ve=>ve.stopPropagation()),l(o,G)};q(Re,o=>{e(T)==="content"?o(Ue):o(Ve,!1)})}var je=v(Re,2),De=s(je),Ce=v(De,2),Ee=v(Ce,2),Be=v(Ee,2);t(je),t($),k(()=>{Ie(be,1,`px-3 py-1.5 rounded-lg cursor-pointer select-none transition - ${e(T)==="content"?"bg-synapse/20 text-synapse-glow border border-synapse/40":"bg-white/[0.03] text-dim hover:text-text border border-transparent"}`),Ie(Te,1,`px-3 py-1.5 rounded-lg cursor-pointer select-none transition - ${e(T)==="audit"?"bg-synapse/20 text-synapse-glow border border-synapse/40":"bg-white/[0.03] text-dim hover:text-text border border-transparent"}`)}),y("click",be,o=>{o.stopPropagation(),N[e(n).id]="content"}),y("keydown",be,o=>{(o.key==="Enter"||o.key===" ")&&(o.stopPropagation(),N[e(n).id]="content")}),y("click",Te,o=>{o.stopPropagation(),N[e(n).id]="audit"}),y("keydown",Te,o=>{(o.key==="Enter"||o.key===" ")&&(o.stopPropagation(),N[e(n).id]="audit")}),y("click",De,o=>{o.stopPropagation(),H.memories.promote(e(n).id)}),y("keydown",De,o=>{o.key==="Enter"&&(o.stopPropagation(),H.memories.promote(e(n).id))}),y("click",Ce,o=>{o.stopPropagation(),H.memories.demote(e(n).id)}),y("keydown",Ce,o=>{o.key==="Enter"&&(o.stopPropagation(),H.memories.demote(e(n).id))}),y("click",Ee,async o=>{o.stopPropagation(),await H.memories.suppress(e(n).id,"dashboard trigger")}),y("keydown",Ee,async o=>{o.key==="Enter"&&(o.stopPropagation(),await H.memories.suppress(e(n).id,"dashboard trigger"))}),y("click",Be,async o=>{o.stopPropagation(),await H.memories.delete(e(n).id),O()}),y("keydown",Be,async o=>{o.key==="Enter"&&(o.stopPropagation(),await H.memories.delete(e(n).id),O())}),l(b,$)};q(a,b=>{var T;((T=e(i))==null?void 0:T.id)===e(n).id&&b(r)})}t(j),He(j,(b,T)=>{var $;return($=nt)==null?void 0:$(b,T)},()=>({delay:Math.min(e(W)*35,350),y:12})),He(j,b=>{var T;return(T=ct)==null?void 0:T(b)}),k((b,T)=>{var $;Ie(j,1,`spotlight-surface text-left p-4 glass-subtle rounded-xl hover:bg-white/[0.04] - transition-all duration-200 group - ${(($=e(i))==null?void 0:$.id)===e(n).id?"!border-synapse/40 glow-synapse live-border":""}`),X(re,`background: ${(L[e(n).nodeType]||"#8B95A5")??""}; box-shadow: 0 0 6px ${(L[e(n).nodeType]||"#8B95A5")??""}99`),D(Me,e(n).nodeType),D(le,e(n).content),X(Se,`width: ${e(n).retentionStrength*100}%; background: ${b??""}`),D(Ae,`${T??""}%`)},[()=>I(e(n).retentionStrength),()=>(e(n).retentionStrength*100).toFixed(0)]),y("click",j,()=>{var b;return P(i,((b=e(i))==null?void 0:b.id)===e(n).id?null:e(n),!0)}),l(A,j)}),t(M),l(c,M)};q(ee,c=>{e(F)?c(te):e(m).length===0?c(ye,1):c(we,!1)})}t(w),y("input",K,d),it(K,()=>e(x),c=>P(x,c)),l(g,w),Ge()}Ke(["input","click","keydown"]);export{pa as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/14.CDp0vmq1.js.br b/apps/dashboard/build/_app/immutable/nodes/14.CDp0vmq1.js.br deleted file mode 100644 index 459d062..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/14.CDp0vmq1.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/14.CDp0vmq1.js.gz b/apps/dashboard/build/_app/immutable/nodes/14.CDp0vmq1.js.gz deleted file mode 100644 index 291263d..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/14.CDp0vmq1.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/15.C05K0kWE.js b/apps/dashboard/build/_app/immutable/nodes/15.C05K0kWE.js deleted file mode 100644 index da9ffef..0000000 --- a/apps/dashboard/build/_app/immutable/nodes/15.C05K0kWE.js +++ /dev/null @@ -1,3 +0,0 @@ -import"../chunks/Bzak7iHL.js";import{o as Qe}from"../chunks/TZu9D97Z.js";import{p as Be,s as M,d as Je,o as Ke,t as m,a as c,b as Ue,i as w,g as e,j as u,e as i,h as v,n as Xe,r as a,m as we,u as ye}from"../chunks/wpu9U-D0.js";import{d as Ye,a as j,s as b}from"../chunks/D8mhvFt8.js";import{i as k}from"../chunks/DKve45Wd.js";import{e as C,s as P,a as Ze}from"../chunks/60_R_Vbt.js";import{a as G,r as H}from"../chunks/P1-U_Xsj.js";import{s as et,a as tt}from"../chunks/ByYB047u.js";import{P as st}from"../chunks/BHDZZvku.js";import{I as at}from"../chunks/D7A-gG4Z.js";import{A as rt}from"../chunks/DcKTNC6e.js";import{t as ie}from"../chunks/CqMQEF-F.js";import{a as U}from"../chunks/CZfHMhLI.js";import{m as it}from"../chunks/BhIgFntf.js";var lt=u(" pending"),ot=u(''),nt=u(""),dt=u('

            Loading…

            '),vt=u('

            '),ct=u('
            '),ut=u('
          1. '),pt=u('
              '),_t=u('
              Select a Memory PR to review the diff.
              '),bt=u(' '),ft=u(' '),mt=u(' '),gt=u('
              + 
              '),wt=u('
              '),yt=u('
              Why this opened
              '),zt=u('
              '),ht=u(`
              Agent's reasoning
              `),xt=u(""),kt=u('
              '),Pt=u(' '),$t=u('
              Decided:
              '),Mt=u('

              '),Rt=u(`
              Vestige auto-remembers ordinary context, but opens a Memory PR when the agent tries to rewrite its own brain. Risky writes are quarantine-reviewed: recorded for audit, but held - out of retrieval until you decide — influence suspended, history preserved.
              `);function Gt(ze,he){Be(he,!0);const xe=()=>et(it,"$memoryPrEvents",ke),[ke,Pe]=tt();let O=M(Je([])),X=M(0),Y=M("risk_gated"),R=M("pending"),n=M(null),E=M(null),Z=M(!1),ee=M(null);const le=[{id:"fast",label:"Fast",blurb:"Every write auto-lands. No review."},{id:"risk_gated",label:"Risk-Gated",blurb:"Ordinary writes land; risky ones open a PR."},{id:"paranoid",label:"Paranoid",blurb:"Every write waits for approval."}],$e=["pending","promoted","merged","superseded","quarantined","forgotten"],oe={new_fact:"New fact",strengthened_fact:"Strengthened",contradiction_detected:"Contradiction",memory_superseded:"Supersede",edge_added:"New edge",node_decayed:"Decayed",dream_consolidation:"Dream merge"},Me=[{id:"promote",label:"Promote",cls:"promote"},{id:"merge",label:"Merge",cls:"merge"},{id:"supersede",label:"Supersede",cls:"supersede"},{id:"quarantine",label:"Quarantine",cls:"quarantine"},{id:"forget",label:"Forget",cls:"forget"},{id:"ask_agent_why",label:"Ask Agent Why",cls:"why"}];async function T(){w(Z,!0);try{const s=await U.memoryPrs.list(e(R)||void 0,100);w(O,s.prs,!0),w(X,s.pendingCount,!0),w(Y,s.mode,!0),e(n)&&w(n,e(O).find(t=>t.id===e(n).id)??null,!0)}finally{w(Z,!1)}}async function Re(s){var r;const t=await U.memoryPrs.setMode(s);w(Y,t.mode,!0),ie.push({type:"MemoryPromoted",title:"Review mode updated",body:`Memory PR gating is now ${(r=le.find(d=>d.id===s))==null?void 0:r.label}.`,color:"#6366f1",dwellMs:4e3})}async function Ae(s,t){w(ee,`${s.id}:${t}`);try{if(t==="ask_agent_why"){const r=await U.memoryPrs.act(s.id,t);w(E,r.why,!0),w(n,s,!0);return}await U.memoryPrs.act(s.id,t),ie.push({type:"MemoryPromoted",title:`PR ${t}d`,body:s.title,color:Ne(t),dwellMs:4500}),w(E,null),await T()}catch(r){ie.push({type:"MemoryDemoted",title:"Action failed",body:String(r),color:"#f43f5e",dwellMs:5e3})}finally{w(ee,null)}}function Ne(s){switch(s){case"promote":return"#10b981";case"merge":return"#6366f1";case"supersede":return"#38bdf8";case"quarantine":return"#f59e0b";case"forget":return"#f43f5e";default:return"#818cf8"}}function Se(s){w(n,s,!0),w(E,null)}Ke(()=>{xe().length&&T()}),Qe(T);function ne(s){var r;const t=(r=s.diff)==null?void 0:r.node;return(t==null?void 0:t.content)??""}function de(s){var r;const t=(r=s.diff)==null?void 0:r.node;return(t==null?void 0:t.nodeType)??""}function qe(s){var r;const t=(r=s.diff)==null?void 0:r.node;return(t==null?void 0:t.tags)??[]}var te=Rt(),ve=i(te);st(ve,{icon:"memorypr",title:"Memory PRs",subtitle:"Approve changes to the agent's brain like code.",accent:"synapse",children:(s,t)=>{var r=lt();let d;var y=i(r);rt(y,{get value(){return e(X)}}),Xe(),a(r),m(()=>d=P(r,1,"pending-badge svelte-c1wbiz",null,d,{has:e(X)>0})),c(s,r)},$$slots:{default:!0}});var ce=v(ve,2);G(ce,s=>{var t;return(t=H)==null?void 0:t(s)});var Q=v(ce,2);C(Q,21,()=>le,s=>s.id,(s,t)=>{var r=ot();let d;var y=i(r),f=i(y,!0);a(y);var z=v(y,2),A=i(z,!0);a(z),a(r),m(()=>{d=P(r,1,"mode svelte-c1wbiz",null,d,{on:e(Y)===e(t).id}),b(f,e(t).label),b(A,e(t).blurb)}),j("click",r,()=>Re(e(t).id)),c(s,r)}),a(Q),G(Q,s=>{var t;return(t=H)==null?void 0:t(s)});var B=v(Q,2),ue=i(B);C(ue,16,()=>$e,s=>s,(s,t)=>{var r=nt();let d;var y=i(r,!0);a(r),m(()=>{d=P(r,1,"filter svelte-c1wbiz",null,d,{on:e(R)===t}),b(y,t)}),j("click",r,()=>{w(R,t,!0),T()}),c(s,r)});var pe=v(ue,2);let _e;a(B),G(B,s=>{var t;return(t=H)==null?void 0:t(s)});var be=v(B,2),J=i(be),Ce=i(J);{var De=s=>{var t=dt();c(s,t)},Ee=s=>{var t=vt(),r=i(t);{var d=f=>{var z=we(`No pending Memory PRs. The brain is up to date — ordinary writes - are landing automatically.`);c(f,z)},y=f=>{var z=we();m(()=>b(z,`No ${(e(R)||"")??""} PRs.`)),c(f,z)};k(r,f=>{e(R)==="pending"?f(d):f(y,!1)})}a(t),c(s,t)},Te=s=>{var t=pt();C(t,21,()=>e(O),r=>r.id,(r,d)=>{var y=ut(),f=i(y);let z;var A=i(f),D=i(A),F=i(D,!0);a(D);var L=v(D,2),I=i(L,!0);a(L),a(A);var N=v(A,2),K=i(N,!0);a(N);var se=v(N,2);{var ae=$=>{var V=ct(),re=i(V);a(V),m(()=>b(re,`⚠ ${e(d).signals.length??""} risk signal${e(d).signals.length>1?"s":""}`)),c($,V)};k(se,$=>{e(d).signals.length&&$(ae)})}a(f),a(y),m(()=>{var $;z=P(f,1,"pr-row svelte-c1wbiz",null,z,{active:(($=e(n))==null?void 0:$.id)===e(d).id}),P(D,1,`pr-kind kind-${e(d).kind??""}`,"svelte-c1wbiz"),b(F,oe[e(d).kind]??e(d).kind),P(L,1,`pr-status st-${e(d).status??""}`,"svelte-c1wbiz"),b(I,e(d).status),b(K,e(d).title)}),j("click",f,()=>Se(e(d))),c(r,y)}),a(t),c(s,t)};k(Ce,s=>{e(Z)?s(De):e(O).length===0?s(Ee,1):s(Te,!1)})}a(J),G(J,s=>{var t;return(t=H)==null?void 0:t(s)});var fe=v(J,2),Fe=i(fe);{var Le=s=>{var t=_t();c(s,t)},Ie=s=>{var t=Mt(),r=i(t),d=i(r),y=i(d,!0);a(d);var f=v(d,2),z=i(f,!0);a(f);var A=v(f,2);{var D=o=>{var l=bt();Ze(l,"href","/blackbox");var p=i(l);at(p,{name:"blackbox",size:13});var _=v(p);a(l),m(g=>b(_,` ${g??""}`),[()=>e(n).run_id.replace("run_","").slice(0,8)]),c(o,l)};k(A,o=>{e(n).run_id&&o(D)})}a(r);var F=v(r,2),L=i(F,!0);a(F);var I=v(F,2),N=i(I),K=i(N);{var se=o=>{var l=ft(),p=i(l);a(l),m(_=>b(p,`type: ${_??""}`),[()=>de(e(n))]),c(o,l)},ae=ye(()=>de(e(n)));k(K,o=>{e(ae)&&o(se)})}var $=v(K,2);C($,16,()=>qe(e(n)),o=>o,(o,l)=>{var p=mt(),_=i(p);a(p),m(()=>b(_,`#${l??""}`)),c(o,p)}),a(N);var V=v(N,2);{var re=o=>{var l=gt(),p=v(i(l),1,!0);a(l),m(_=>b(p,_),[()=>ne(e(n))]),c(o,l)},Ve=ye(()=>ne(e(n)));k(V,o=>{e(Ve)&&o(re)})}a(I);var me=v(I,2);{var We=o=>{var l=yt(),p=v(i(l),2);C(p,17,()=>e(n).signals,_=>_.code,(_,g)=>{var h=wt(),x=i(h),S=i(x,!0);a(x);var q=v(x,2),W=i(q,!0);a(q),a(h),m(()=>{b(S,e(g).code),b(W,e(g).detail)}),c(_,h)}),a(l),c(o,l)};k(me,o=>{e(n).signals.length&&o(We)})}var ge=v(me,2);{var je=o=>{var l=ht(),p=v(i(l),2);C(p,17,()=>e(E),_=>_.code,(_,g)=>{var h=zt(),x=i(h),S=i(x,!0);a(x);var q=v(x,2),W=i(q,!0);a(q),a(h),m(()=>{b(S,e(g).code),b(W,e(g).detail)}),c(_,h)}),a(l),c(o,l)};k(ge,o=>{e(E)&&o(je)})}var Ge=v(ge,2);{var He=o=>{var l=kt();C(l,21,()=>Me,p=>p.id,(p,_)=>{var g=xt(),h=i(g,!0);a(g),m(()=>{P(g,1,`action ${e(_).cls??""}`,"svelte-c1wbiz"),g.disabled=e(ee)===`${e(n).id}:${e(_).id}`,b(h,e(_).label)}),j("click",g,()=>Ae(e(n),e(_).id)),c(p,g)}),a(l),c(o,l)},Oe=o=>{var l=$t(),p=v(i(l)),_=i(p,!0);a(p);var g=v(p,2);{var h=x=>{var S=Pt(),q=i(S);a(S),m(W=>b(q,`· ${W??""}`),[()=>new Date(e(n).decided_at).toLocaleString()]),c(x,S)};k(g,x=>{e(n).decided_at&&x(h)})}a(l),m(()=>b(_,e(n).decision??e(n).status)),c(o,l)};k(Ge,o=>{e(n).status==="pending"?o(He):o(Oe,!1)})}a(t),G(t,o=>{var l;return(l=H)==null?void 0:l(o)}),m(()=>{P(d,1,`pr-kind kind-${e(n).kind??""}`,"svelte-c1wbiz"),b(y,oe[e(n).kind]??e(n).kind),P(f,1,`pr-status st-${e(n).status??""}`,"svelte-c1wbiz"),b(z,e(n).status),b(L,e(n).title)}),c(s,t)};k(Fe,s=>{e(n)?s(Ie,!1):s(Le)})}a(fe),a(be),a(te),m(()=>_e=P(pe,1,"filter svelte-c1wbiz",null,_e,{on:e(R)===""})),j("click",pe,()=>{w(R,""),T()}),c(ze,te),Ue(),Pe()}Ye(["click"]);export{Gt as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/15.C05K0kWE.js.br b/apps/dashboard/build/_app/immutable/nodes/15.C05K0kWE.js.br deleted file mode 100644 index 58aed9f..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/15.C05K0kWE.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/15.C05K0kWE.js.gz b/apps/dashboard/build/_app/immutable/nodes/15.C05K0kWE.js.gz deleted file mode 100644 index a9ad2e6..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/15.C05K0kWE.js.gz and /dev/null 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..63b1159 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.CrQpRrFW.js b/apps/dashboard/build/_app/immutable/nodes/16.CrQpRrFW.js deleted file mode 100644 index bec5d28..0000000 --- a/apps/dashboard/build/_app/immutable/nodes/16.CrQpRrFW.js +++ /dev/null @@ -1,3 +0,0 @@ -import"../chunks/Bzak7iHL.js";import{o as lt}from"../chunks/TZu9D97Z.js";import{p as Ue,e as r,h as n,r as t,t as A,a as _,g as e,u as z,b as Ve,s as oe,i as S,c as Ye,f as Re,j as y,d as dt,n as Te}from"../chunks/wpu9U-D0.js";import{d as Je,s as d,a as ie,e as qe}from"../chunks/D8mhvFt8.js";import{i as q}from"../chunks/DKve45Wd.js";import{e as te,a as Ce,s as _e,i as pt}from"../chunks/60_R_Vbt.js";import{a as vt,r as ut}from"../chunks/P1-U_Xsj.js";import{s as me}from"../chunks/EqHb-9AZ.js";import{P as ft}from"../chunks/BHDZZvku.js";import{I as X}from"../chunks/D7A-gG4Z.js";import{A as ee}from"../chunks/DcKTNC6e.js";function gt(b,m,$=3){const v={};for(const u of b){v[u]={};for(const f of b)v[u][f]={count:0,topNames:[]}}for(const u of m){const f=u.origin_project;if(v[f])for(const F of u.transferred_to)v[f][F]&&(v[f][F].count+=1,v[f][F].topNames.push(u.name))}const p=Math.max(0,$);for(const u of b)for(const f of b)v[u][f].topNames=v[u][f].topNames.slice(0,p);return v}function xt(b,m){let $=0;for(const v of b){const p=m[v];if(p)for(const u of b){const f=p[u];f&&f.count>$&&($=f.count)}}return $}function _t(b,m){var v;const $=[];for(const p of b)for(const u of b){const f=(v=m[p])==null?void 0:v[u];f&&f.count>0&&$.push({from:p,to:u,count:f.count,topNames:f.topNames})}return $.sort((p,u)=>u.count-p.count)}function mt(b){return b?b.length>12?b.slice(0,11)+"…":b:""}var yt=y('
              '),ht=y(''),bt=y(' '),wt=y('
              '),jt=y('
              Top patterns
              '),kt=y('
              No transfers recorded
              '),Tt=y('
              '),Ct=y('
              No cross-project transfers recorded yet.
              '),At=y('
              '),$t=y(''),Zt=y('
              ');function Pt(b,m){Ue(m,!0);const $=z(()=>gt(m.projects,m.patterns)),v=z(()=>xt(m.projects,e($))||1);let p=oe(null);function u(a){if(a===0)return"background: rgba(255,255,255,0.02); border-color: rgba(99,102,241,0.05);";const s=a/e(v),o=.1+s*.7;if(s<.5)return`background: rgba(99, 102, 241, ${o.toFixed(3)}); border-color: rgba(129, 140, 248, ${(o*.6).toFixed(3)}); box-shadow: 0 0 ${(s*14).toFixed(1)}px rgba(129, 140, 248, ${(s*.45).toFixed(3)});`;{const c=(s-.5)*2,l=Math.round(99+69*c),x=Math.round(102+-17*c),g=Math.round(241+6*c);return`background: rgba(${l}, ${x}, ${g}, ${o.toFixed(3)}); border-color: rgba(192, 132, 252, ${(o*.7).toFixed(3)}); box-shadow: 0 0 ${(6+s*18).toFixed(1)}px rgba(192, 132, 252, ${(s*.55).toFixed(3)});`}}function f(a){if(a===0)return"text-muted";const s=a/e(v);return s>=.5?"text-bright font-semibold":s>=.2?"text-text":"text-dim"}function F(a,s,o){const l=a.currentTarget.getBoundingClientRect();S(p,{from:s,to:o,x:l.left+l.width/2,y:l.top},!0)}function k(){S(p,null)}const K=mt;function ce(a,s){return m.selectedCell!==null&&m.selectedCell.from===a&&m.selectedCell.to===s}const H=z(()=>_t(m.projects,e($)));var B=Zt(),Q=r(B),re=r(Q),U=n(r(re),2),le=n(r(U),4),Ae=r(le,!0);t(le),t(U),t(re);var de=n(re,2),ye=r(de),V=r(ye),pe=r(V),ve=n(r(pe));te(ve,16,()=>m.projects,a=>a,(a,s)=>{var o=yt(),c=r(o),l=r(c),x=r(l,!0);t(l),t(c),t(o),A(g=>{Ce(o,"title",s),d(x,g)},[()=>K(s)]),_(a,o)}),t(pe),t(V);var W=n(V);te(W,20,()=>m.projects,a=>a,(a,s)=>{var o=bt(),c=r(o),l=r(c,!0);t(c);var x=n(c);te(x,16,()=>m.projects,g=>g,(g,w)=>{const h=z(()=>e($)[s][w]),P=z(()=>s===w);var N=ht(),C=r(N),R=r(C),Y=r(R,!0);t(R),t(C),t(N),A((I,Z,T)=>{me(C,`${I??""} ${Z??""} ${e(P)&&e(h).count>0?"border-style: dashed;":""}`),Ce(C,"aria-label",`${e(h).count??""} patterns from ${s??""} to ${w??""}`),_e(R,1,`text-[11px] ${T??""}`),d(Y,e(h).count||"")},[()=>u(e(h).count),()=>ce(s,w)?"outline: 2px solid var(--color-dream-glow); outline-offset: 1px;":"",()=>f(e(h).count)]),ie("click",C,()=>m.onCellClick(s,w)),qe("mouseenter",C,I=>F(I,s,w)),qe("mouseleave",C,k),_(g,N)}),t(o),A(g=>{Ce(c,"title",s),d(l,g)},[()=>K(s)]),_(a,o)}),t(W),t(ye),t(de);var he=n(de,2);{var $e=a=>{const s=z(()=>e($)[e(p).from][e(p).to]);var o=Tt(),c=r(o),l=r(c),x=r(l,!0);t(l);var g=n(l,4),w=r(g,!0);t(g),t(c);var h=n(c,2),P=r(h),N=n(P),C=r(N);t(N),t(h);var R=n(h,2);{var Y=Z=>{var T=jt(),L=n(r(T),2);te(L,17,()=>e(s).topNames,pt,(D,J)=>{var fe=wt(),we=r(fe);t(fe),A(()=>d(we,`· ${e(J)??""}`)),_(D,fe)}),t(T),_(Z,T)},I=Z=>{var T=kt();_(Z,T)};q(R,Z=>{e(s).topNames.length>0?Z(Y):Z(I,!1)})}t(o),A((Z,T)=>{me(o,`left: ${e(p).x??""}px; top: ${e(p).y-12}px; transform: translate(-50%, -100%);`),d(x,Z),d(w,T),d(P,`${e(s).count??""} `),d(C,`${e(s).count===1?"pattern":"patterns"} transferred`)},[()=>K(e(p).from),()=>K(e(p).to)]),_(a,o)};q(he,a=>{e(p)&&a($e)})}t(Q);var be=n(Q,2),ue=r(be),Ze=r(ue);t(ue);var Pe=n(ue,2);{var Ne=a=>{var s=Ct();_(a,s)},i=a=>{var s=Ye(),o=Re(s);te(o,17,()=>e(H),c=>c.from+"->"+c.to,(c,l)=>{var x=$t(),g=r(x),w=r(g),h=r(w),P=r(h,!0);t(h);var N=n(h,4),C=r(N,!0);t(N),t(w);var R=n(w,2);{var Y=T=>{var L=At(),D=r(L,!0);t(L),A(J=>d(D,J),[()=>e(l).topNames.join(" · ")]),_(T,L)};q(R,T=>{e(l).topNames.length>0&&T(Y)})}t(g);var I=n(g,2),Z=r(I,!0);t(I),t(x),A((T,L,D)=>{_e(x,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??""}`),d(P,L),d(C,D),d(Z,e(l).count)},[()=>ce(e(l).from,e(l).to)?"ring-1 ring-dream-glow":"",()=>K(e(l).from),()=>K(e(l).to)]),ie("click",x,()=>m.onCellClick(e(l).from,e(l).to)),_(c,x)}),_(a,s)};q(Pe,a=>{e(H).length===0?a(Ne):a(i,!1)})}t(be),t(B),A(()=>{d(Ae,e(v)),d(Ze,`${e(H).length??""} transfer pair${e(H).length===1?"":"s"} · tap to filter`)}),_(b,B),Ve()}Je(["click"]);var Nt=y(' '),St=y(''),Ft=y(`
              Couldn't load pattern transfers
              `),Mt=y('
              '),Et=y('
              Filtered to
              '),zt=y('
              No matching patterns
              '),Ht=y('
            • '),Rt=y('
                '),It=y('
                ',1),Lt=y('
                ');function Jt(b,m){Ue(m,!0);const $=["ErrorHandling","AsyncConcurrency","Testing","Architecture","Performance","Security"],v={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 p=oe("All"),u=oe(dt({projects:[],patterns:[]})),f=oe(!0),F=oe(null),k=oe(null);async function K(){return await new Promise(s=>setTimeout(s,420)),{projects:["vestige","api-gateway","desktop-app","model-runner","game-sim","security-dashboard","benchmark-suite"],patterns:[{name:"Result with thiserror context",category:"ErrorHandling",origin_project:"vestige",transferred_to:["api-gateway","desktop-app","model-runner","security-dashboard"],transfer_count:4,last_used:"2026-04-18T14:22:00Z",confidence:.94},{name:"Axum error middleware with tower-http",category:"ErrorHandling",origin_project:"api-gateway",transferred_to:["vestige","security-dashboard"],transfer_count:2,last_used:"2026-04-17T09:10:00Z",confidence:.88},{name:"Graceful shutdown on SIGINT/SIGTERM",category:"ErrorHandling",origin_project:"vestige",transferred_to:["vestige","desktop-app","security-dashboard"],transfer_count:3,last_used:"2026-04-15T22:01:00Z",confidence:.82},{name:"Python try/except with contextual re-raise",category:"ErrorHandling",origin_project:"benchmark-suite",transferred_to:["model-runner"],transfer_count:1,last_used:"2026-04-10T11:30:00Z",confidence:.7},{name:"Arc> reader/writer split",category:"AsyncConcurrency",origin_project:"vestige",transferred_to:["api-gateway","desktop-app"],transfer_count:2,last_used:"2026-04-14T16:42:00Z",confidence:.91},{name:"tokio::select! for cancellation propagation",category:"AsyncConcurrency",origin_project:"desktop-app",transferred_to:["vestige","security-dashboard"],transfer_count:2,last_used:"2026-04-19T08:05:00Z",confidence:.86},{name:"Bounded mpsc channel with backpressure",category:"AsyncConcurrency",origin_project:"desktop-app",transferred_to:["vestige","api-gateway"],transfer_count:2,last_used:"2026-04-12T13:18:00Z",confidence:.83},{name:"asyncio.gather with return_exceptions",category:"AsyncConcurrency",origin_project:"model-runner",transferred_to:["benchmark-suite"],transfer_count:1,last_used:"2026-04-08T20:45:00Z",confidence:.72},{name:"Property-based tests with proptest",category:"Testing",origin_project:"vestige",transferred_to:["api-gateway","desktop-app"],transfer_count:2,last_used:"2026-04-11T10:22:00Z",confidence:.89},{name:"Snapshot testing with insta",category:"Testing",origin_project:"api-gateway",transferred_to:["vestige"],transfer_count:1,last_used:"2026-04-16T14:00:00Z",confidence:.81},{name:"Vitest + Playwright dashboard harness",category:"Testing",origin_project:"vestige",transferred_to:["api-gateway","desktop-app"],transfer_count:2,last_used:"2026-04-19T18:30:00Z",confidence:.87},{name:"One-variable-at-a-time Kaggle submission",category:"Testing",origin_project:"benchmark-suite",transferred_to:["model-runner","game-sim"],transfer_count:2,last_used:"2026-04-20T07:15:00Z",confidence:.95},{name:"Kaggle pre-flight Input-panel screenshot",category:"Testing",origin_project:"benchmark-suite",transferred_to:["model-runner","game-sim"],transfer_count:2,last_used:"2026-04-20T06:50:00Z",confidence:.98},{name:"SvelteKit 2 + Svelte 5 runes dashboard",category:"Architecture",origin_project:"vestige",transferred_to:["api-gateway","security-dashboard"],transfer_count:2,last_used:"2026-04-19T12:10:00Z",confidence:.92},{name:"glass-panel + cosmic-dark design system",category:"Architecture",origin_project:"vestige",transferred_to:["api-gateway","security-dashboard","desktop-app"],transfer_count:3,last_used:"2026-04-20T09:00:00Z",confidence:.9},{name:"Tauri 2 + Rust/Axum sidecar",category:"Architecture",origin_project:"desktop-app",transferred_to:["security-dashboard"],transfer_count:1,last_used:"2026-04-13T19:44:00Z",confidence:.78},{name:"MCP server with 23 stateful tools",category:"Architecture",origin_project:"vestige",transferred_to:["desktop-app"],transfer_count:1,last_used:"2026-04-17T11:05:00Z",confidence:.85},{name:"USearch HNSW index for vector search",category:"Performance",origin_project:"vestige",transferred_to:["api-gateway"],transfer_count:1,last_used:"2026-04-09T15:20:00Z",confidence:.88},{name:"SQLite WAL mode for concurrent reads",category:"Performance",origin_project:"vestige",transferred_to:["api-gateway","desktop-app","security-dashboard"],transfer_count:3,last_used:"2026-04-18T21:33:00Z",confidence:.93},{name:"vLLM prefix caching at 0.35 mem util",category:"Performance",origin_project:"benchmark-suite",transferred_to:["model-runner"],transfer_count:1,last_used:"2026-04-11T08:00:00Z",confidence:.84},{name:"Cross-encoder rerank at k=30",category:"Performance",origin_project:"vestige",transferred_to:["api-gateway"],transfer_count:1,last_used:"2026-04-14T17:55:00Z",confidence:.79},{name:"Rotated auth token in env var",category:"Security",origin_project:"vestige",transferred_to:["api-gateway","desktop-app","security-dashboard"],transfer_count:3,last_used:"2026-04-16T20:12:00Z",confidence:.96},{name:"Parameterized SQL via rusqlite params!",category:"Security",origin_project:"vestige",transferred_to:["api-gateway"],transfer_count:1,last_used:"2026-04-10T13:40:00Z",confidence:.89},{name:"664-pattern secret scanner",category:"Security",origin_project:"api-gateway",transferred_to:["vestige","security-dashboard","desktop-app"],transfer_count:3,last_used:"2026-04-20T05:30:00Z",confidence:.97},{name:"CSP header with nonce-based script allow",category:"Security",origin_project:"api-gateway",transferred_to:["security-dashboard"],transfer_count:1,last_used:"2026-04-05T16:08:00Z",confidence:.8}]}}async function ce(){S(f,!0),S(F,null);try{S(u,await K(),!0)}catch(i){S(F,i instanceof Error?i.message:"Failed to load pattern transfers",!0),S(u,{projects:[],patterns:[]},!0)}finally{S(f,!1)}}lt(()=>ce());const H=z(()=>e(p)==="All"?e(u).patterns:e(u).patterns.filter(i=>i.category===e(p))),B=z(()=>[...e(k)?e(H).filter(a=>a.origin_project===e(k).from&&a.transferred_to.includes(e(k).to)):e(H)].sort((a,s)=>s.transfer_count-a.transfer_count)),Q=z(()=>e(H).reduce((i,a)=>i+a.transferred_to.length,0)),re=z(()=>e(u).projects.length),U=z(()=>e(H).length);function le(i){S(p,i,!0),S(k,null)}function Ae(i,a){e(k)&&e(k).from===i&&e(k).to===a?S(k,null):S(k,{from:i,to:a},!0)}function de(){S(k,null)}function ye(i){const a=new Date(i).getTime(),s=Date.now(),o=Math.floor((s-a)/864e5);return o<=0?"today":o===1?"1d ago":o<30?`${o}d ago`:`${Math.floor(o/30)}mo ago`}var V=Lt(),pe=r(V);ft(pe,{icon:"patterns",title:"Cross-Project Intelligence",subtitle:"Patterns learned here, applied there — across every tracked project.",accent:"dream",children:(i,a)=>{var s=Ye(),o=Re(s);{var c=l=>{var x=Nt(),g=r(x),w=r(g);ee(w,{get value(){return e(U)}}),t(g);var h=n(g),P=n(h),N=r(P);ee(N,{get value(){return e(Q)}}),t(P);var C=n(P);t(x),A(()=>{d(h,` pattern${e(U)===1?"":"s"} - · `),d(C,` transfer${e(Q)===1?"":"s"}`)}),_(l,x)};q(o,l=>{!e(f)&&!e(F)&&l(c)})}_(i,s)},$$slots:{default:!0}});var ve=n(pe,2),W=r(ve),he=r(W),$e=r(he);X($e,{name:"sparkle",size:13}),Te(),t(he),t(W);var be=n(W,2);te(be,16,()=>$,i=>i,(i,a)=>{var s=St(),o=r(s),c=n(o);t(s),A(()=>{_e(s,1,`lift flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-medium transition ${e(p)===a?"bg-synapse/25 text-synapse-glow shadow-[0_0_16px_-4px_var(--color-synapse-glow)]":"text-dim hover:bg-white/[0.04] hover:text-text"}`),_e(o,1,`h-1.5 w-1.5 rounded-full transition-all duration-300 ${e(p)===a?"scale-150":""}`),me(o,`background: ${v[a]??""}; ${e(p)===a?`box-shadow: 0 0 8px ${v[a]};`:""}`),d(c,` ${a??""}`)}),ie("click",s,()=>le(a)),_(i,s)}),t(ve);var ue=n(ve,2);{var Ze=i=>{var a=Ft(),s=r(a),o=r(s);X(o,{name:"contradictions",size:28}),t(s);var c=n(s,4),l=r(c,!0);t(c);var x=n(c,2),g=r(x);X(g,{name:"pulse",size:14}),Te(),t(x),t(a),A(()=>d(l,e(F))),ie("click",x,ce),_(i,a)},Pe=i=>{var a=Mt();_(i,a)},Ne=i=>{var a=It(),s=Re(a),o=r(s),c=r(o);Pt(c,{get projects(){return e(u).projects},get patterns(){return e(H)},get selectedCell(){return e(k)},onCellClick:Ae});var l=n(c,2);{var x=G=>{var M=Et(),E=r(M),j=r(E),ae=r(j);X(ae,{name:"filter",size:13}),t(j);var O=n(j,4),je=r(O,!0);t(O);var se=n(O,4),ne=r(se,!0);t(se),t(E);var ge=n(E,2),xe=r(ge);X(xe,{name:"close",size:12}),Te(),t(ge),t(M),A(()=>{d(je,e(k).from),d(ne,e(k).to)}),ie("click",ge,de),_(G,M)};q(l,G=>{e(k)&&G(x)})}t(o);var g=n(o,2),w=r(g),h=r(w),P=r(h),N=r(P);X(N,{name:"patterns",size:15}),t(P),Te(),t(h);var C=n(h,2),R=r(C);ee(R,{get value(){return e(B).length}});var Y=n(R);t(C),t(w);var I=n(w,2);{var Z=G=>{var M=zt(),E=r(M),j=r(E);X(j,{name:"explore",size:32}),t(E);var ae=n(E,4),O=r(ae,!0);t(ae),t(M),A(()=>d(O,e(k)?"No patterns transferred from this origin to this destination yet — try another cell or clear the filter.":"Nothing in this category yet. Pick another category to explore what travels between projects.")),_(G,M)},T=G=>{var M=Rt();te(M,23,()=>e(B),E=>E.name,(E,j,ae)=>{var O=Ht(),je=r(O),se=r(je),ne=r(se),ge=r(ne,!0);t(ne);var xe=n(ne,2),ke=r(xe),at=r(ke,!0);t(ke);var Oe=n(ke,2),st=r(Oe,!0);t(Oe),t(xe);var De=n(xe,2),Me=r(De),nt=r(Me,!0);t(Me);var Ke=n(Me,4),ot=r(Ke);t(Ke),t(De),t(se);var Be=n(se,2),Ee=r(Be),it=r(Ee);ee(it,{get value(){return e(j).transfer_count}}),t(Ee);var Qe=n(Ee,2),ct=r(Qe);t(Qe),t(Be),t(je),t(O),vt(O,(ze,He)=>{var We;return(We=ut)==null?void 0:We(ze,He)},()=>({y:12,delay:Math.min(e(ae)*45,360)})),A((ze,He)=>{Ce(ne,"title",e(j).name),d(ge,e(j).name),me(ke,`border-color: ${v[e(j).category]??""}66; color: ${v[e(j).category]??""}; background: ${v[e(j).category]??""}1a;`),d(at,e(j).category),d(st,ze),d(nt,e(j).origin_project),d(ot,`${e(j).transferred_to.length??""} - ${e(j).transferred_to.length===1?"project":"projects"}`),d(ct,`${He??""}%`)},[()=>ye(e(j).last_used),()=>(e(j).confidence*100).toFixed(0)]),_(E,O)}),t(M),_(G,M)};q(I,G=>{e(B).length===0?G(Z):G(T,!1)})}t(g),t(s);var L=n(s,2),D=r(L),J=r(D),fe=r(J);ee(fe,{get value(){return e(U)}}),t(J);var we=n(J),Se=n(we),Xe=r(Se);ee(Xe,{get value(){return e(re)}}),t(Se);var Ie=n(Se),Fe=n(Ie),et=r(Fe);ee(et,{get value(){return e(Q)}}),t(Fe);var tt=n(Fe);t(D);var Le=n(D,2),Ge=r(Le),rt=n(Ge);t(Le),t(L),A(()=>{d(Y,` ${e(B).length===1?"pattern":"patterns"}`),d(we,` pattern${e(U)===1?"":"s"} across `),d(Ie,` project${e(re)===1?"":"s"}, `),d(tt,` total transfer${e(Q)===1?"":"s"}`),me(Ge,`background: ${(e(p)==="All"?"var(--color-synapse-glow)":v[e(p)])??""}`),d(rt,` ${(e(p)==="All"?"All categories":e(p))??""}`)}),_(i,a)};q(ue,i=>{e(F)?i(Ze):e(f)?i(Pe,1):i(Ne,!1)})}t(V),A(()=>_e(W,1,`lift rounded-lg px-3 py-1.5 text-xs font-medium transition ${e(p)==="All"?"bg-synapse/25 text-synapse-glow shadow-[0_0_16px_-4px_var(--color-synapse-glow)]":"text-dim hover:bg-white/[0.04] hover:text-text"}`)),ie("click",W,()=>le("All")),_(b,V),Ve()}Je(["click"]);export{Jt as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/16.CrQpRrFW.js.br b/apps/dashboard/build/_app/immutable/nodes/16.CrQpRrFW.js.br deleted file mode 100644 index 16c0afa..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/16.CrQpRrFW.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/16.CrQpRrFW.js.gz b/apps/dashboard/build/_app/immutable/nodes/16.CrQpRrFW.js.gz deleted file mode 100644 index b2eb33c..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/16.CrQpRrFW.js.gz and /dev/null 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..260ceac 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.3ASmJvJ6.js b/apps/dashboard/build/_app/immutable/nodes/17.3ASmJvJ6.js deleted file mode 100644 index deea646..0000000 --- a/apps/dashboard/build/_app/immutable/nodes/17.3ASmJvJ6.js +++ /dev/null @@ -1,2 +0,0 @@ -import"../chunks/Bzak7iHL.js";import{o as Gt}from"../chunks/TZu9D97Z.js";import{p as tt,e as s,g as e,h as a,r as t,n as Z,t as C,a as g,b as st,j as f,u as re,s as he,d as Ht,x as Wt,i as L,ar as Kt,f as ft,au as wt}from"../chunks/wpu9U-D0.js";import{s as o,d as Vt,a as Ye}from"../chunks/D8mhvFt8.js";import{i as D}from"../chunks/DKve45Wd.js";import{e as ce,s as at,a as Y,r as Qt,i as Ae}from"../chunks/60_R_Vbt.js";import{h as Ut}from"../chunks/DzesjbbJ.js";import{a as Xe,r as Yt}from"../chunks/P1-U_Xsj.js";import{s as w}from"../chunks/EqHb-9AZ.js";import{b as Xt}from"../chunks/CnZzd20v.js";import{b as _t}from"../chunks/g5OnrUYZ.js";import{a as Zt}from"../chunks/CZfHMhLI.js";import{p as ue}from"../chunks/ByYB047u.js";import{N as Jt}from"../chunks/CcUbQ_Wl.js";import{P as es}from"../chunks/BHDZZvku.js";import{I as be}from"../chunks/D7A-gG4Z.js";import{A as Ze}from"../chunks/DcKTNC6e.js";import{m as ts,s as ss}from"../chunks/DPdYG9yN.js";var as=f('
                '),ns=f('

                '),rs=f("
                ");function yt(l,v){tt(v,!0);let J=ue(v,"intent",3,"Synthesis"),B=ue(v,"memoriesAnalyzed",3,0),R=ue(v,"evidenceCount",3,0),i=ue(v,"contradictionCount",3,0),G=ue(v,"supersededCount",3,0),E=ue(v,"running",3,!1),H=ue(v,"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"}],ee=re(()=>({broad:B()?`Analyzed ${B()} memories · ${R()} survived ranking`:void 0,intent:J()?`Classified as ${J()}`:void 0,supersession:G()?`${G()} outdated memor${G()===1?"y":"ies"} superseded`:void 0,contradiction:i()?`${i()} real conflict${i()===1?"":"s"} between trusted memories`:"No conflicts between trusted memories"}));function te($,K){return H()[$]??e(ee)[$]??K}var W=rs();let qe;ce(W,23,()=>I,$=>$.key,($,K,V)=>{var O=ns(),se=s(O);{var pe=N=>{var oe=as();C(()=>w(oe,`animation-delay: ${e(V)*140+120}ms;`)),g(N,oe)};D(se,N=>{e(V){w(O,`animation-delay: ${e(V)*140}ms;`),w(M,`animation-delay: ${e(V)*140}ms;`),o(me,e(K).icon),o(Ee,`0${e(V)+1}`),o(we,e(K).label),o(ne,N)},[()=>te(e(K).key,e(K).base)]),g($,O)}),t(W),C(()=>qe=at(W,1,"reasoning-chain space-y-2 svelte-9hm057",null,qe,{running:E()})),g(l,W),st()}const is="#10b981",ls="#f59e0b",et="#ef4444",ht="#8B95A5",bt={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 os(l){return bt[l]??bt.supporting}function Oe(l){return Number.isFinite(l)?l>75?is:l>=40?ls:et:et}function qt(l){return Number.isFinite(l)?l>75?"HIGH CONFIDENCE":l>=40?"MIXED SIGNAL":"LOW CONFIDENCE":"LOW CONFIDENCE"}function Je(l){return Number.isFinite(l)?Oe(l*100):et}function vs(l){return!Number.isFinite(l)||l<0?0:l>1?1:l}function ds(l){return vs(l)*100}function cs(l){return l?Jt[l]??ht:ht}function us(l,v){if(l==null||typeof l!="string"||l.trim()==="")return"—";const J=new Date(l);if(Number.isNaN(J.getTime()))return l;try{return J.toLocaleDateString(v,{month:"short",day:"numeric",year:"numeric"})}catch{return l}}function ps(l,v=8){return l?l.length>v?l.slice(0,v):l:""}var ms=f(' '),xs=f('

                Trust
                FSRS · reps × retention
                ');function gs(l,v){tt(v,!0);let J=ue(v,"index",3,0);const B=re(()=>ds(v.trust)),R=re(()=>os(v.role)),i=re(()=>ps(v.id)),G=re(()=>cs(v.nodeType));var E=xs();let H;var I=s(E),ee=s(I),te=s(ee),W=s(te),qe=s(W,!0);t(W);var $=a(W,1,!0);t(te);var K=a(te,2);{var V=ne=>{var N=ms(),oe=s(N,!0);t(N),C(()=>{w(N,`color: ${e(G)??""}`),o(oe,v.nodeType)}),g(ne,N)};D(K,ne=>{v.nodeType&&ne(V)})}t(ee);var O=a(ee,2),se=s(O);t(O),t(I);var pe=a(I,2),M=s(pe,!0);t(pe);var P=a(pe,2),me=s(P),ie=a(s(me),2),xe=s(ie);t(ie),t(me);var ae=a(me,2),Ee=s(ae);t(ae),t(P);var le=a(P,2),we=s(le),ge=s(we,!0);t(we),Z(2),t(le),t(E),C((ne,N,oe,r,n)=>{H=at(E,1,"evidence-card glass rounded-xl p-4 space-y-3 transition relative svelte-ksja6x",null,H,{contradicting:v.role==="contradicting",primary:v.role==="primary",superseded:v.role==="superseded"}),w(E,`animation-delay: ${J()*80}ms;`),Y(E,"data-evidence-id",v.id),o(qe,e(R).icon),o($,e(R).label),Y(O,"title",v.id),o(se,`#${e(i)??""}`),o(M,v.preview),w(ie,`color: ${ne??""}`),o(xe,`${N??""}%`),w(Ee,`width: ${e(B)??""}%; background: ${oe??""}; box-shadow: 0 0 8px ${r??""}80;`),o(ge,n)},[()=>Je(v.trust),()=>e(B).toFixed(0),()=>Je(v.trust),()=>Je(v.trust),()=>us(v.date)]),g(l,E),st()}var fs=f(' deep_reference'),_s=f(''),ys=f('
                Try
                '),hs=f('
                Error:
                '),bs=f('
                Running cognitive pipeline
                '),qs=f('

                Reasoning

                intent: · analyzed ·
                '),ws=wt('',1),ks=wt(''),Ss=f('

                '),Cs=f('

                Contradictions Detected ()

                '),Rs=f('
                '),As=f('

                Superseded

                '),Es=f('
                '),Ns=f('

                Evolution

                '),Ts=f('

                '),js=f('

                Related Insights

                '),Fs=f('
                Confidence
                %
                intent: ·
                Primary Source

                ·

                Cognitive Pipeline

                Evidence

                primary supporting contradicting superseded
                ',1),zs=f(`

                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.

                `),Ls=f('
                ');function ea(l,v){tt(v,!0);async function J(r){var je,$e,Fe;const n=await Zt.deepReference(r,20),k=(Array.isArray(n.evidence)?n.evidence:[]).map(c=>{const j=typeof c.trust=="number"?c.trust:0,ze=j>1?j/100:j,Le=c.role||"supporting";return{id:String(c.id??""),trust:Math.max(0,Math.min(1,ze)),date:String(c.date??""),role:Le,preview:String(c.preview??""),nodeType:c.node_type?String(c.node_type):c.nodeType?String(c.nodeType):void 0}}),_=n.recommended,T={answer_preview:String((_==null?void 0:_.answer_preview)??((je=k[0])==null?void 0:je.preview)??""),memory_id:String((_==null?void 0:_.memory_id)??(($e=k[0])==null?void 0:$e.id)??""),trust_score:(()=>{var j;const c=_==null?void 0:_.trust_score;return typeof c=="number"?c>1?c/100:c:((j=k[0])==null?void 0:j.trust)??0})(),date:String((_==null?void 0:_.date)??((Fe=k[0])==null?void 0:Fe.date)??"")},ke=(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.")})),Ne=(Array.isArray(n.superseded)?n.superseded:[]).map(c=>({old_id:String(c.old_id??""),new_id:String(c.new_id??T.memory_id??""),reason:String(c.reason??"Superseded by newer memory with higher FSRS trust.")})),Se=(Array.isArray(n.evolution)?n.evolution:[]).map(c=>({date:String(c.date??""),summary:String(c.summary??c.preview??""),trust:(()=>{const j=c.trust;return typeof j=="number"?j>1?j/100:j:0})()})),Me=Array.isArray(n.related_insights)?n.related_insights:[],fe=typeof n.confidence=="number"?n.confidence:0,_e=fe>1?Math.round(fe):Math.round(fe*100),Pe=String(n.intent??"Synthesis"),Ie=String(n.reasoning??n.guidance??""),Te=typeof n.memoriesAnalyzed=="number"?n.memoriesAnalyzed:k.length;return{intent:Pe,reasoning:Ie,recommended:T,evidence:k,contradictions:ke,superseded:Ne,evolution:Se,related_insights:Me,confidence:_e,memoriesAnalyzed:Te}}let B=he(""),R=he(!1),i=he(null),G=he(null),E=he(null),H=he(null),I=he(Ht([]));async function ee(){const r=e(B).trim();if(!(!r||e(R))){L(R,!0),L(G,null),L(i,null),L(I,[],!0);try{L(i,await J(r),!0),requestAnimationFrame(()=>requestAnimationFrame(te))}catch(n){L(G,n instanceof Error?n.message:"Unknown error",!0)}finally{L(R,!1)}}}function te(){if(!e(i)||!e(H)||e(i).contradictions.length===0){L(I,[],!0);return}const r=e(H).getBoundingClientRect(),n=[];for(const p of e(i).contradictions){const k=e(H).querySelector(`[data-evidence-id="${p.a_id}"]`),_=e(H).querySelector(`[data-evidence-id="${p.b_id}"]`);if(!k||!_)continue;const T=k.getBoundingClientRect(),Q=_.getBoundingClientRect();n.push({x1:T.left-r.left+T.width/2,y1:T.top-r.top+T.height/2,x2:Q.left-r.left+Q.width/2,y2:Q.top-r.top+Q.height/2})}L(I,n,!0)}function W(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())}Gt(()=>{var r;return(r=e(E))==null||r.focus(),window.addEventListener("keydown",W),window.addEventListener("resize",te),()=>{window.removeEventListener("keydown",W),window.removeEventListener("resize",te)}});const qe=["What port does the dev server use?","Should I enable prefix caching with vLLM?","Why did the benchmark score drop after the parser change?","How does FSRS-6 trust scoring work?"];var $=Ls();Ut("q2v96u",r=>{Wt(()=>{Kt.title="Reasoning Theater · Vestige"})});var K=s($);es(K,{icon:"reasoning",title:"Reasoning Theater",subtitle:"Watch Vestige reason — the 8-stage cognitive pipeline runs locally and returns a pre-built answer with trust-scored evidence.",accent:"dream",children:(r,n)=>{var p=fs();g(r,p)},$$slots:{default:!0}});var V=a(K,2),O=s(V),se=s(O),pe=s(se);be(pe,{name:"reasoning",size:20}),t(se);var M=a(se,2);Qt(M),_t(M,r=>L(E,r),()=>e(E));var P=a(M,4),me=s(P,!0);t(P),Xe(P,r=>{var n;return(n=ts)==null?void 0:n(r)}),t(O);var ie=a(O,2);{var xe=r=>{var n=ys(),p=a(s(n),2);ce(p,17,()=>qe,Ae,(k,_)=>{var T=_s(),Q=s(T,!0);t(T),C(()=>o(Q,e(_))),Ye("click",T,()=>{L(B,e(_),!0),ee()}),g(k,T)}),t(n),g(r,n)};D(ie,r=>{!e(i)&&!e(R)&&r(xe)})}t(V);var ae=a(V,2);{var Ee=r=>{var n=hs(),p=a(s(n));t(n),C(()=>o(p,` ${e(G)??""}`)),g(r,n)};D(ae,r=>{e(G)&&r(Ee)})}var le=a(ae,2);{var we=r=>{var n=bs(),p=a(s(n),2);yt(p,{running:!0}),t(n),g(r,n)};D(le,r=>{e(R)&&r(we)})}var ge=a(le,2);{var ne=r=>{const n=re(()=>e(i).confidence),p=re(()=>Oe(e(n)));var k=Fs(),_=ft(k);{var T=u=>{var d=qs(),x=s(d),y=s(x),m=s(y),h=s(m);be(h,{name:"reasoning",size:16}),t(m),Z(),t(y);var F=a(y,2),S=s(F),b=a(s(S)),q=s(b,!0);t(b),t(S);var z=a(S,4),A=s(z);Ze(A,{get value(){return e(i).memoriesAnalyzed}}),Z(),t(z);var U=a(z,4),ve=s(U);t(U),t(F),t(x);var X=a(x,2),Ce=s(X),Re=s(Ce,!0);t(Ce),t(X),Xe(X,ye=>{var de;return(de=ss)==null?void 0:de(ye)}),t(d),Xe(d,ye=>{var de;return(de=Yt)==null?void 0:de(ye)}),C(ye=>{o(q,e(i).intent),w(U,`color: ${e(p)??""}`),o(ve,`${e(n)??""}% ${ye??""}`),w(X,`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;`),o(Re,e(i).reasoning)},[()=>qt(e(n))]),g(u,d)};D(_,u=>{e(i).reasoning&&u(T)})}var Q=a(_,2),ke=s(Q),De=a(s(ke),2),Ne=s(De),nt=s(Ne);Ze(nt,{get value(){return e(n)}}),Z(),t(Ne),t(De);var Se=a(De,2),Me=s(Se,!0);t(Se);var fe=a(Se,2),_e=a(s(fe)),Pe=s(_e);t(_e),t(fe);var Ie=a(fe,2),Te=s(Ie),je=a(s(Te)),$e=s(je,!0);t(je),t(Te);var Fe=a(Te,4),c=s(Fe);t(Fe),t(Ie),t(ke);var j=a(ke,2),ze=s(j),Le=a(s(ze),2),kt=s(Le);t(Le),t(ze);var Be=a(ze,2),St=s(Be,!0);t(Be);var rt=a(Be,2),Ge=s(rt),it=s(Ge),Ct=a(it);t(Ge);var lt=a(Ge,4),Rt=s(lt,!0);t(lt),t(rt),t(j),t(Q);var He=a(Q,2),We=s(He),ot=s(We),At=s(ot);be(At,{name:"activation",size:15}),t(ot),Z(),t(We);var vt=a(We,2),Et=s(vt);yt(Et,{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(vt),t(He);var Ke=a(He,2),Ve=s(Ke),dt=s(Ve),Qe=s(dt),Nt=s(Qe);be(Nt,{name:"memories",size:15}),t(Qe);var ct=a(Qe,2),Tt=s(ct);t(ct),t(dt),Z(2),t(Ve);var Ue=a(Ve,2),ut=s(Ue);ce(ut,19,()=>e(i).evidence,u=>u.id,(u,d,x)=>{gs(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(x)}})});var jt=a(ut,2);{var Ft=u=>{var d=ks(),x=a(s(d));ce(x,17,()=>e(I),Ae,(y,m,h)=>{const F=re(()=>(e(m).x1+e(m).x2)/2),S=re(()=>Math.min(e(m).y1,e(m).y2)-28);var b=ws(),q=ft(b);w(q,`animation-delay: ${h*120+600}ms;`);var z=a(q);w(z,`animation-delay: ${h*120+600}ms;`);var A=a(z);w(A,`animation-delay: ${h*120+700}ms;`),C(()=>{Y(q,"d",`M ${e(m).x1??""} ${e(m).y1??""} Q ${e(F)??""} ${e(S)??""} ${e(m).x2??""} ${e(m).y2??""}`),Y(z,"cx",e(m).x1),Y(z,"cy",e(m).y1),Y(A,"cx",e(m).x2),Y(A,"cy",e(m).y2)}),g(y,b)}),t(d),g(u,d)};D(jt,u=>{e(I).length>0&&u(Ft)})}t(Ue),_t(Ue,u=>L(H,u),()=>e(H)),t(Ke);var pt=a(Ke,2);{var zt=u=>{var d=Cs(),x=s(d),y=s(x),m=s(y);be(m,{name:"contradictions",size:15}),t(y);var h=a(y,2),F=a(s(h));Ze(F,{get value(){return e(i).contradictions.length}}),Z(),t(h),t(x);var S=a(x,2);ce(S,21,()=>e(i).contradictions,Ae,(b,q,z)=>{var A=Ss(),U=a(s(A),2),ve=s(U),X=s(ve),Ce=s(X);t(X);var Re=a(X,4),ye=s(Re);t(Re),t(ve);var de=a(ve,2),Ot=s(de,!0);t(de),t(U);var Mt=a(U,2);Mt.textContent=`pair ${z+1}`,t(A),C((Pt,Bt)=>{o(Ce,`#${Pt??""}`),o(ye,`#${Bt??""}`),o(Ot,e(q).summary)},[()=>e(q).a_id.slice(0,8),()=>e(q).b_id.slice(0,8)]),g(b,A)}),t(S),t(d),g(u,d)};D(pt,u=>{e(i).contradictions.length>0&&u(zt)})}var mt=a(pt,2);{var Lt=u=>{var d=As(),x=s(d),y=a(s(x),2),m=s(y);t(y),t(x);var h=a(x,2);ce(h,21,()=>e(i).superseded,Ae,(F,S)=>{var b=Rs(),q=s(b),z=s(q);t(q);var A=a(q,4),U=s(A);t(A);var ve=a(A,2),X=s(ve,!0);t(ve),t(b),C((Ce,Re)=>{o(z,`#${Ce??""}`),o(U,`#${Re??""}`),o(X,e(S).reason)},[()=>e(S).old_id.slice(0,8),()=>e(S).new_id.slice(0,8)]),g(F,b)}),t(h),t(d),C(()=>o(m,`(${e(i).superseded.length??""})`)),g(u,d)};D(mt,u=>{e(i).superseded.length>0&&u(Lt)})}var xt=a(mt,2),gt=s(xt);{var Dt=u=>{var d=Ns(),x=a(s(d),2);ce(x,21,()=>e(i).evolution,Ae,(y,m)=>{var h=Es(),F=s(h),S=s(F,!0);t(F);var b=a(F,2),q=a(b,2),z=s(q,!0);t(q),t(h),C((A,U)=>{o(S,A),w(b,`background: ${U??""}`),o(z,e(m).summary)},[()=>new Date(e(m).date).toLocaleDateString(void 0,{month:"short",day:"numeric"}),()=>Oe(e(m).trust*100)]),g(y,h)}),t(x),t(d),g(u,d)};D(gt,u=>{e(i).evolution.length>0&&u(Dt)})}var It=a(gt,2);{var $t=u=>{var d=js(),x=s(d),y=s(x),m=s(y);be(m,{name:"sparkle",size:15}),t(y),Z(),t(x);var h=a(x,2);ce(h,21,()=>e(i).related_insights,Ae,(F,S)=>{var b=Ts(),q=a(s(b),1,!0);t(b),C(()=>o(q,e(S))),g(F,b)}),t(h),t(d),g(u,d)};D(It,u=>{e(i).related_insights.length>0&&u($t)})}t(xt),C((u,d,x,y,m)=>{w(ke,`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;`),w(Ne,`color: ${e(p)??""}; text-shadow: 0 0 24px ${e(p)??""}80;`),w(Se,`color: ${e(p)??""}`),o(Me,u),Y(_e,"width",e(n)/100*220),Y(_e,"fill",e(p)),w(_e,`filter: drop-shadow(0 0 6px ${e(p)??""});`),Y(Pe,"to",e(n)/100*220),o($e,e(i).intent),o(c,`${e(i).memoriesAnalyzed??""} analyzed`),Y(Le,"title",e(i).recommended.memory_id),o(kt,`#${d??""}`),o(St,e(i).recommended.answer_preview),w(it,`background: ${x??""}`),o(Ct,` Trust ${y??""}%`),o(Rt,m),o(Tt,`(${e(i).evidence.length??""})`)},[()=>qt(e(n)),()=>e(i).recommended.memory_id.slice(0,8),()=>Oe(e(i).recommended.trust_score*100),()=>(e(i).recommended.trust_score*100).toFixed(0),()=>new Date(e(i).recommended.date).toLocaleDateString()]),g(r,k)};D(ge,r=>{e(i)&&!e(R)&&r(ne)})}var N=a(ge,2);{var oe=r=>{var n=zs(),p=s(n),k=s(p);be(k,{name:"reasoning",size:44,strokeWidth:1.2}),t(p),Z(4),t(n),g(r,n)};D(N,r=>{!e(i)&&!e(R)&&!e(G)&&r(oe)})}t($),C(r=>{at(se,1,`text-synapse-glow ${e(R)?"breathe":""}`,"svelte-q2v96u"),P.disabled=r,o(me,e(R)?"Reasoning…":"Reason")},[()=>!e(B).trim()||e(R)]),Ye("keydown",M,r=>r.key==="Enter"&&ee()),Xt(M,()=>e(B),r=>L(B,r)),Ye("click",P,ee),g(l,$),st()}Vt(["keydown","click"]);export{ea as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/17.3ASmJvJ6.js.br b/apps/dashboard/build/_app/immutable/nodes/17.3ASmJvJ6.js.br deleted file mode 100644 index 0c90528..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/17.3ASmJvJ6.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/17.3ASmJvJ6.js.gz b/apps/dashboard/build/_app/immutable/nodes/17.3ASmJvJ6.js.gz deleted file mode 100644 index 93b7671..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/17.3ASmJvJ6.js.gz and /dev/null 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..6734125 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/18.43xZFMsD.js b/apps/dashboard/build/_app/immutable/nodes/18.43xZFMsD.js deleted file mode 100644 index 115f2f2..0000000 --- a/apps/dashboard/build/_app/immutable/nodes/18.43xZFMsD.js +++ /dev/null @@ -1,8 +0,0 @@ -import"../chunks/Bzak7iHL.js";import{o as tt}from"../chunks/TZu9D97Z.js";import{p as Ue,e as s,h as n,g as e,c as at,f as st,a as u,t as w,r as t,u as k,b as Qe,s as ue,i as W,j as g,au as Oe,d as rt,n as nt}from"../chunks/wpu9U-D0.js";import{d as Xe,s as p,a as Pe}from"../chunks/D8mhvFt8.js";import{i as N}from"../chunks/DKve45Wd.js";import{a as C,e as X,i as he,s as Ie}from"../chunks/60_R_Vbt.js";import{a as Ke}from"../chunks/CZfHMhLI.js";import{s as Ae}from"../chunks/EqHb-9AZ.js";import{p as it}from"../chunks/ByYB047u.js";import{N as ot}from"../chunks/CcUbQ_Wl.js";import{P as dt}from"../chunks/BHDZZvku.js";import{I as lt}from"../chunks/D7A-gG4Z.js";import{D as vt}from"../chunks/CmbJHhgy.js";const Ge=1440*60*1e3;function pe(x){const v=typeof x=="string"?new Date(x):new Date(x);return v.setHours(0,0,0,0),v}function be(x,v){return Math.floor((pe(x).getTime()-pe(v).getTime())/Ge)}function Ye(x){const v=x.getFullYear(),f=String(x.getMonth()+1).padStart(2,"0"),R=String(x.getDate()).padStart(2,"0");return`${v}-${f}-${R}`}function qe(x,v){if(!v)return"none";const f=new Date(v);if(Number.isNaN(f.getTime()))return"none";const R=be(f,x);return R<0?"overdue":R===0?"today":R<=7?"week":"future"}function ze(x,v){if(!v)return null;const f=new Date(v);return Number.isNaN(f.getTime())?null:be(f,x)}function ct(x){if(x.length===0)return 0;let v=0;for(const f of x)v+=f.retentionStrength??0;return v/x.length}function ut(x){const v=pe(x);return v.setDate(v.getDate()-14),v.setDate(v.getDate()-v.getDay()),v}function pt(x,v){let f=0,R=0,I=0,O=0,A=0,G=0;const E=pe(x);for(const B of v){if(!B.nextReviewAt)continue;const H=new Date(B.nextReviewAt);if(Number.isNaN(H.getTime()))continue;const _=be(H,x);_<0&&f++,_<=0&&R++,_<=7&&I++,_<=30&&O++,_>=0&&(A+=(H.getTime()-E.getTime())/Ge,G++)}const b=G>0?A/G:0;return{overdue:f,dueToday:R,dueThisWeek:I,dueThisMonth:O,avgDays:b}}var xt=Oe(''),gt=Oe(''),mt=Oe(''),ft=g('
                '),_t=g(' '),ht=g(' '),bt=g('
                '),yt=g(''),wt=g(" "),kt=g(' '),$t=g('

                '),St=g('

                '),Dt=g('

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

                API unavailable.

                Could not fetch memories from /api/memories.

                '),Lt=g(`

                FSRS review schedule not yet populated.

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

                `),Wt=g('
                Overdue
                '),Pt=g('

                Nothing in this window.

                '),It=g('

                '),Ot=g('

                '),Et=g('
                '),Bt=g('
                '),Ht=g('
                ');function aa(x,v){Ue(v,!0);let f=ue(rt([])),R=ue(0),I=ue(!0),O=ue(!1),A=ue("week");const G=2e3;async function E(){const i=await Ke.memories.list({limit:String(G)});W(f,i.memories,!0),W(R,i.total,!0)}tt(async()=>{try{await E()}catch{W(O,!0),W(f,[],!0)}finally{W(I,!1)}});let b=k(()=>e(f).filter(i=>!!i.nextReviewAt)),B=k(()=>new Date),H=k(()=>e(R)>e(f).length),_=k(()=>(()=>{const i=e(A);return i==="all"?e(b):e(b).filter(l=>{const m=qe(e(B),l.nextReviewAt);if(m==="none")return!1;if(i==="today")return m==="overdue"||m==="today";if(i==="week")return m!=="future";const j=ze(e(B),l.nextReviewAt);return j!==null&&j<=30})})()),L=k(()=>pt(e(B),e(b)));async function ye(){W(I,!0);try{await Ke.consolidate(),await E(),W(O,!1)}catch{W(O,!0)}finally{W(I,!1)}}const xe=[{key:"today",label:"Due today"},{key:"week",label:"This week"},{key:"month",label:"This month"},{key:"all",label:"All upcoming"}];let ge=k(()=>[{value:"today",label:"Due today",icon:"schedule",badge:e(L).dueToday,color:"#fbbf24"},{value:"week",label:"This week",icon:"schedule",badge:e(L).dueThisWeek,color:"#818cf8"},{value:"month",label:"This month",icon:"schedule",badge:e(L).dueThisMonth,color:"#c084fc"},{value:"all",label:"All upcoming",icon:"schedule",badge:e(b).length,color:"#6ee7b7"}]),we=k(()=>{var i;return((i=xe.find(l=>l.key===e(A)))==null?void 0:i.label)??""});var ne=Ht(),ie=s(ne);dt(ie,{icon:"schedule",title:"Review Schedule",subtitle:"FSRS-6 next-review dates across your memory corpus",accent:"warning",children:(i,l)=>{vt(i,{get options(){return e(ge)},label:"Window",icon:"filter",get value(){return e(A)},set value(m){W(A,m,!0)}})},$$slots:{default:!0}});var oe=n(ie,2);{var me=i=>{var l=At(),m=s(l,!0);t(l),w(()=>p(m,e(we))),u(i,l)};N(oe,i=>{e(we)&&i(me)})}var J=n(oe,2);{var de=i=>{var l=Ft(),m=s(l);t(l),w((j,r)=>p(m,`Showing the first ${j??""} of ${r??""} memories. - Schedule reflects this slice only.`),[()=>e(f).length.toLocaleString(),()=>e(R).toLocaleString()]),u(i,l)};N(J,i=>{!e(I)&&!e(O)&&e(H)&&i(de)})}var ke=n(J,2);{var Fe=i=>{var l=jt(),m=s(l),j=n(s(m),2);X(j,20,()=>Array(42),he,(a,o)=>{var d=Nt();u(a,d)}),t(j),t(m);var r=n(m,2);X(r,20,()=>Array(5),he,(a,o)=>{var d=Ct();u(a,d)}),t(r),t(l),u(i,l)},$e=i=>{var l=Mt();u(i,l)},Ne=i=>{var l=Lt(),m=s(l),j=s(m);lt(j,{name:"schedule",size:42,strokeWidth:1.2}),t(m);var r=n(m,4),a=s(r);nt(2),t(r);var o=n(r,2);t(l),w(()=>p(a,`None of your ${e(f).length??""} memor${e(f).length===1?"y has":"ies have"} a `)),Pe("click",o,ye),u(i,l)},Ce=i=>{var l=Bt(),m=s(l),j=s(m);Rt(j,{get memories(){return e(b)}}),t(m);var r=n(m,2),a=s(r),o=n(s(a),2),d=s(o);{var $=S=>{var F=Wt(),M=n(s(F),2),U=s(M,!0);t(M),t(F),w(()=>p(U,e(L).overdue)),u(S,F)};N(d,S=>{e(L).overdue>0&&S($)})}var y=n(d,2),D=n(s(y),2),fe=s(D,!0);t(D),t(y);var V=n(y,2),Z=n(s(V),2),le=s(Z,!0);t(Z),t(V);var ve=n(V,2),ce=n(s(ve),2),h=s(ce,!0);t(ce),t(ve),t(o);var c=n(o,2),T=s(c),Y=n(s(T),2),ee=s(Y,!0);t(Y),t(T);var Q=n(T,2),te=s(Q);t(Q),t(c),t(a);var q=n(a,2),z=s(q),ae=s(z),Se=s(ae,!0);t(ae);var De=n(ae,2),je=s(De,!0);t(De),t(z);var Te=n(z,2);{var _e=S=>{var F=Pt();u(S,F)},Me=S=>{var F=Et(),M=s(F);X(M,17,()=>e(_).slice().sort((P,K)=>(P.nextReviewAt??"").localeCompare(K.nextReviewAt??"")).slice(0,50),P=>P.id,(P,K)=>{const re=k(()=>qe(e(B),e(K).nextReviewAt)),Ee=k(()=>ze(e(B),e(K).nextReviewAt)??0);var Le=It(),We=s(Le),Je=s(We,!0);t(We);var Be=n(We,2),Re=s(Be),Ve=s(Re,!0);t(Re);var He=n(Re,2),Ze=s(He);t(He),t(Be),t(Le),w(et=>{p(Je,e(K).content),Ie(Re,1,e(re)==="overdue"?"text-decay":e(re)==="today"?"text-warning":e(re)==="week"?"text-synapse-glow":"text-dream-glow"),p(Ve,e(re)==="overdue"?`${-e(Ee)}d overdue`:e(re)==="today"?"today":`in ${e(Ee)}d`),p(Ze,`· ${et??""}%`)},[()=>(e(K).retentionStrength*100).toFixed(0)]),u(P,Le)});var U=n(M,2);{var se=P=>{var K=Ot(),re=s(K);t(K),w(()=>p(re,`+${e(_).length-50} more`)),u(P,K)};N(U,P=>{e(_).length>50&&P(se)})}t(F),u(S,F)};N(Te,S=>{e(_).length===0?S(_e):S(Me,!1)})}t(q),t(r),t(l),w((S,F)=>{p(fe,e(L).dueToday),p(le,e(L).dueThisWeek),p(h,e(L).dueThisMonth),p(ee,S),p(te,`Across ${e(b).length??""} scheduled memor${e(b).length===1?"y":"ies"}`),p(Se,F),p(je,e(_).length)},[()=>e(L).avgDays.toFixed(1),()=>{var S;return(S=xe.find(F=>F.key===e(A)))==null?void 0:S.label}]),u(i,l)};N(ke,i=>{e(I)?i(Fe):e(O)?i($e,1):e(b).length===0?i(Ne,2):i(Ce,!1)})}t(ne),u(x,ne),Qe()}Xe(["click"]);export{aa as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/18.43xZFMsD.js.br b/apps/dashboard/build/_app/immutable/nodes/18.43xZFMsD.js.br deleted file mode 100644 index 0010bc8..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/18.43xZFMsD.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/18.43xZFMsD.js.gz b/apps/dashboard/build/_app/immutable/nodes/18.43xZFMsD.js.gz deleted file mode 100644 index 468dc18..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/18.43xZFMsD.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/18.Df4fIuu-.js b/apps/dashboard/build/_app/immutable/nodes/18.Df4fIuu-.js new file mode 100644 index 0000000..a80255d --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/18.Df4fIuu-.js @@ -0,0 +1 @@ +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..afdb431 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/19.BYwd4oWS.js b/apps/dashboard/build/_app/immutable/nodes/19.BYwd4oWS.js deleted file mode 100644 index 13ee522..0000000 --- a/apps/dashboard/build/_app/immutable/nodes/19.BYwd4oWS.js +++ /dev/null @@ -1,2 +0,0 @@ -import"../chunks/Bzak7iHL.js";import{o as ft}from"../chunks/TZu9D97Z.js";import{p as _t,t as $,a as n,b as bt,g as i,j as c,e as t,h as d,s as C,a9 as kt,i as p,f as Fe,r as e,n as m,m as Ie,u as F}from"../chunks/wpu9U-D0.js";import{d as yt,a as I,s as f}from"../chunks/D8mhvFt8.js";import{i as _}from"../chunks/DKve45Wd.js";import{e as fe,s as V,i as _e}from"../chunks/60_R_Vbt.js";import{a as R,r as A}from"../chunks/P1-U_Xsj.js";import{s as Y}from"../chunks/EqHb-9AZ.js";import{a as ht,s as be}from"../chunks/ByYB047u.js";import{a as O}from"../chunks/CZfHMhLI.js";import{a as wt,b as $t,i as St,w as Ct}from"../chunks/BhIgFntf.js";import{f as Dt}from"../chunks/CqMQEF-F.js";import{P as Rt}from"../chunks/BHDZZvku.js";import{I as P}from"../chunks/D7A-gG4Z.js";var At=c(' ',1),Pt=c(' Running...',1),Gt=c('
                Processed
                '),jt=c('
                Decayed
                '),zt=c('
                Embedded
                '),Mt=c('
                '),Tt=c(' Dreaming...',1),Et=c('
                '),Ft=c('
                Insights Discovered:
                ',1),It=c('
                Connections found:
                '),Ot=c('
                Memories replayed:
                '),Nt=c('
                '),Bt=c('
                '),Kt=c('
                '),Wt=c('

                Retention Distribution

                '),qt=c('
                '),Ht=c(`
                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

                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 ds(Oe,Ne){_t(Ne,!0);const b=()=>be(St,"$isConnected",Q),Be=()=>be($t,"$memoryCount",Q),L=()=>be(wt,"$avgRetention",Q),[Q,Ke]=ht(),ke=["fact","concept","pattern","decision","person","place"];let X=C(0);function We(){const s=ke[i(X)%ke.length];kt(X),Ct.injectEvent({type:"MemoryCreated",data:{id:`demo-birth-${Date.now()}`,content:`Demo memory #${i(X)} — ${s}`,node_type:s,tags:["demo","v2.3-birth-ritual"],retention:.9}})}let N=C(!1),j=C(!1),S=C(null),h=C(null),qe=C(null),G=C(null),ye=C(!0),He=C(null);ft(()=>{B()});async function B(){p(ye,!0);try{const[s,r,a]=await Promise.all([O.stats().catch(()=>null),O.health().catch(()=>null),O.retentionDistribution().catch(()=>null)]);p(qe,s,!0),p(He,r,!0),p(G,a,!0)}finally{p(ye,!1)}}async function Je(){p(N,!0),p(S,null);try{p(S,await O.consolidate(),!0),await B()}catch{}finally{p(N,!1)}}async function Ue(){p(j,!0),p(h,null);try{p(h,await O.dream(),!0),await B()}catch{}finally{p(j,!1)}}var Z=Ht(),he=t(Z);Rt(he,{icon:"settings",title:"Settings & System",subtitle:"Tune the cognitive engine, watch the system breathe, and run the rituals that keep memory alive.",accent:"synapse",children:(s,r)=>{var a=At(),x=Fe(a);let k;var u=t(x);let w;var D=d(u,2),y=t(D,!0);e(D),e(x);var l=d(x,2),v=t(l);P(v,{name:"activation",size:13}),m(2),e(l),$(()=>{k=V(x,1,"conn-pill svelte-15kgmsr",null,k,{idle:!b()}),w=V(u,1,"conn-dot w-2 h-2 rounded-full svelte-15kgmsr",null,w,{"ping-host":b(),breathe:!b()}),Y(u,`color:${b()?"var(--color-recall)":"var(--color-decay)"};background:${b()?"var(--color-recall)":"var(--color-decay)"}`),f(y,b()?"Connected":"Offline")}),I("click",l,B),n(s,a)},$$slots:{default:!0}});var ee=d(he,2),K=t(ee),we=t(K),Ve=t(we,!0);e(we),m(2),e(K),R(K,(s,r)=>{var a;return(a=A)==null?void 0:a(s,r)},()=>({delay:0}));var W=d(K,2),te=t(W),Ye=t(te);e(te),m(2),e(W),R(W,(s,r)=>{var a;return(a=A)==null?void 0:a(s,r)},()=>({delay:60}));var q=d(W,2),$e=t(q),se=t($e);let Se;var Ce=d(se,2),Le=t(Ce,!0);e(Ce),e($e),m(2),e(q),R(q,(s,r)=>{var a;return(a=A)==null?void 0:a(s,r)},()=>({delay:120}));var Qe=d(q,2);R(Qe,(s,r)=>{var a;return(a=A)==null?void 0:a(s,r)},()=>({delay:180})),e(ee);var H=d(ee,2),re=t(H),Xe=t(re);P(Xe,{name:"dreams",size:16,class:"text-dream"}),m(),e(re);var ae=d(re,2),De=t(ae),ie=d(t(De),2),Ze=t(ie);P(Ze,{name:"sparkle",size:14}),m(),e(ie),e(De),e(ae);var de=d(ae,2),Re=t(de),le=d(t(Re),2),et=t(le);P(et,{name:"memories",size:14}),m(),e(le),e(Re),e(de);var ve=d(de,2),oe=t(ve),J=d(t(oe),2),tt=t(J);{var st=s=>{var r=Pt();m(),n(s,r)},rt=s=>{var r=Ie("Consolidate");n(s,r)};_(tt,s=>{i(N)?s(st):s(rt,!1)})}e(J),e(oe);var at=d(oe,2);{var it=s=>{var r=Mt(),a=t(r),x=t(a);{var k=l=>{var v=Gt(),o=t(v),g=t(o,!0);e(o),m(2),e(v),$(()=>f(g,i(S).nodesProcessed)),n(l,v)};_(x,l=>{i(S).nodesProcessed!==void 0&&l(k)})}var u=d(x,2);{var w=l=>{var v=jt(),o=t(v),g=t(o,!0);e(o),m(2),e(v),$(()=>f(g,i(S).decayApplied)),n(l,v)};_(u,l=>{i(S).decayApplied!==void 0&&l(w)})}var D=d(u,2);{var y=l=>{var v=zt(),o=t(v),g=t(o,!0);e(o),m(2),e(v),$(()=>f(g,i(S).embeddingsGenerated)),n(l,v)};_(D,l=>{i(S).embeddingsGenerated!==void 0&&l(y)})}e(a),e(r),n(s,r)};_(at,s=>{i(S)&&s(it)})}e(ve);var Ae=d(ve,2),ne=t(Ae),z=d(t(ne),2),dt=t(z);{var lt=s=>{var r=Tt();m(),n(s,r)},vt=s=>{var r=Ie("Dream");n(s,r)};_(dt,s=>{i(j)?s(lt):s(vt,!1)})}e(z),e(ne);var ot=d(ne,2);{var nt=s=>{var r=Nt(),a=t(r);{var x=l=>{var v=Ft(),o=d(Fe(v),2);fe(o,17,()=>i(h).insights,_e,(g,M)=>{var T=Et(),xe=t(T,!0);e(T),$(E=>f(xe,E),[()=>typeof i(M)=="string"?i(M):JSON.stringify(i(M))]),n(g,T)}),n(l,v)},k=F(()=>i(h).insights&&Array.isArray(i(h).insights));_(a,l=>{i(k)&&l(x)})}var u=d(a,2);{var w=l=>{var v=It(),o=d(t(v)),g=t(o,!0);e(o),e(v),$(()=>f(g,i(h).connections_found)),n(l,v)};_(u,l=>{i(h).connections_found!==void 0&&l(w)})}var D=d(u,2);{var y=l=>{var v=Ot(),o=d(t(v)),g=t(o,!0);e(o),e(v),$(()=>f(g,i(h).memories_replayed)),n(l,v)};_(D,l=>{i(h).memories_replayed!==void 0&&l(y)})}e(r),n(s,r)};_(ot,s=>{i(h)&&s(nt)})}e(Ae),e(H),R(H,(s,r)=>{var a;return(a=A)==null?void 0:a(s,r)},()=>({delay:60}));var Pe=d(H,2);{var mt=s=>{var r=Wt(),a=t(r),x=t(a);P(x,{name:"importance",size:16,class:"text-recall"}),m(),e(a);var k=d(a,2),u=t(k);{var w=y=>{var l=Kt();fe(l,21,()=>i(G).distribution,_e,(v,o,g)=>{const M=F(()=>Math.max(...i(G).distribution.map(pe=>pe.count),1)),T=F(()=>i(o).count/i(M)*100),xe=F(()=>g<2?"#ef4444":g<4?"#f59e0b":g<7?"#6366f1":"#10b981");var E=Bt(),ue=t(E),ut=t(ue,!0);e(ue);var Ee=d(ue,2),pt=d(Ee,2);pt.textContent=`${g*10}%`,e(E),$(pe=>{f(ut,i(o).count),Y(Ee,`height: ${pe??""}%; background: ${i(xe)??""}; opacity: 0.7`)},[()=>Math.max(i(T),2)]),n(v,E)}),e(l),n(y,l)},D=F(()=>i(G).distribution&&Array.isArray(i(G).distribution));_(u,y=>{i(D)&&y(w)})}e(k),e(r),R(r,(y,l)=>{var v;return(v=A)==null?void 0:v(y,l)},()=>({delay:120})),n(s,r)};_(Pe,s=>{i(G)&&s(mt)})}var U=d(Pe,2),me=t(U),ct=t(me);P(ct,{name:"command",size:16,class:"text-synapse"}),m(),e(me);var Ge=d(me,2),je=t(Ge);fe(je,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"}],_e,(s,r)=>{var a=qt(),x=t(a),k=t(x,!0);e(x);var u=d(x,2),w=t(u,!0);e(u),e(a),$(()=>{f(k,r.key),f(w,r.desc)}),n(s,a)}),e(je),e(Ge),e(U),R(U,(s,r)=>{var a;return(a=A)==null?void 0:a(s,r)},()=>({delay:160}));var ce=d(U,2),ge=t(ce),gt=t(ge);P(gt,{name:"logo",size:16,class:"text-memory"}),m(),e(ge);var ze=d(ge,2),Me=t(ze),Te=t(Me),xt=t(Te);P(xt,{name:"logo",size:20,strokeWidth:1.8}),e(Te),m(2),e(Me),m(4),e(ze),e(ce),R(ce,(s,r)=>{var a;return(a=A)==null?void 0:a(s,r)},()=>({delay:200})),e(Z),$(s=>{f(Ve,Be()),Y(te,`color: ${L()>.7?"#10b981":L()>.4?"#f59e0b":"#ef4444"}`),f(Ye,`${s??""}%`),Se=V(se,1,"w-2.5 h-2.5 rounded-full svelte-15kgmsr",null,Se,{"ping-host":b(),breathe:!b()}),Y(se,`color:${b()?"var(--color-recall)":"var(--color-decay)"};background:${b()?"var(--color-recall)":"var(--color-decay)"}`),f(Le,b()?"Online":"Offline"),J.disabled=i(N),z.disabled=i(j),V(z,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 - ${i(j)?"glow-dream animate-pulse-glow":""}`,"svelte-15kgmsr")},[()=>(L()*100).toFixed(1)]),I("click",ie,function(...s){var r;(r=Dt)==null||r.apply(this,s)}),I("click",le,We),I("click",J,Je),I("click",z,Ue),n(Oe,Z),bt(),Ke()}yt(["click"]);export{ds as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/19.BYwd4oWS.js.br b/apps/dashboard/build/_app/immutable/nodes/19.BYwd4oWS.js.br deleted file mode 100644 index f5a023d..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/19.BYwd4oWS.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/19.BYwd4oWS.js.gz b/apps/dashboard/build/_app/immutable/nodes/19.BYwd4oWS.js.gz deleted file mode 100644 index 6a8b24b..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/19.BYwd4oWS.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/19.CMsn8k5A.js b/apps/dashboard/build/_app/immutable/nodes/19.CMsn8k5A.js new file mode 100644 index 0000000..dddca2b --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/19.CMsn8k5A.js @@ -0,0 +1 @@ +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..8e4c142 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.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..9bbed13 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/2.DEnQkIHv.js b/apps/dashboard/build/_app/immutable/nodes/2.DEnQkIHv.js deleted file mode 100644 index e732e89..0000000 --- a/apps/dashboard/build/_app/immutable/nodes/2.DEnQkIHv.js +++ /dev/null @@ -1 +0,0 @@ -import"../chunks/Bzak7iHL.js";import{c as n,f as m,a as p}from"../chunks/wpu9U-D0.js";import{s as e}from"../chunks/LDOJP_6N.js";function f(a,r){var o=n(),t=m(o);e(t,()=>r.children),p(a,o)}export{f as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/2.DEnQkIHv.js.br b/apps/dashboard/build/_app/immutable/nodes/2.DEnQkIHv.js.br deleted file mode 100644 index 2d00db9..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/2.DEnQkIHv.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/2.DEnQkIHv.js.gz b/apps/dashboard/build/_app/immutable/nodes/2.DEnQkIHv.js.gz deleted file mode 100644 index 4dca9f4..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/2.DEnQkIHv.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/20.Di2Q3Va0.js b/apps/dashboard/build/_app/immutable/nodes/20.Di2Q3Va0.js deleted file mode 100644 index 145180a..0000000 --- a/apps/dashboard/build/_app/immutable/nodes/20.Di2Q3Va0.js +++ /dev/null @@ -1 +0,0 @@ -import"../chunks/Bzak7iHL.js";import{o as Rt}from"../chunks/TZu9D97Z.js";import{p as Ct,a as p,b as kt,j as f,e,l as dt,i as D,c as At,f as lt,g as t,h as i,r as a,t as j,s as J,n as K,u as Q}from"../chunks/wpu9U-D0.js";import{d as Mt,s as R,a as Pt}from"../chunks/D8mhvFt8.js";import{i as V}from"../chunks/DKve45Wd.js";import{e as W,i as X}from"../chunks/60_R_Vbt.js";import{a as c,r as C}from"../chunks/P1-U_Xsj.js";import{s as B}from"../chunks/EqHb-9AZ.js";import{a as S}from"../chunks/CZfHMhLI.js";import{P as Dt}from"../chunks/BHDZZvku.js";import{A as U}from"../chunks/DcKTNC6e.js";import{s as Z}from"../chunks/DPdYG9yN.js";import{N as ut}from"../chunks/CcUbQ_Wl.js";var St=f('
                '),Et=f('
                '),Ot=f('
                '),Tt=f('
                '),Nt=f('
                '),jt=f('
                '),Bt=f('

                '),Ft=f('

                Retention Distribution

                Memory Types

                ',1),Lt=f('
                Total Memories
                Avg Retention
                Due for Review
                Embedding Coverage
                ',1),Ht=f('
                ');function aa(mt,pt){Ct(pt,!0);let _=J(null),h=J(null),b=J(null),ot=J(!0);Rt(async()=>{try{await(async n=>{var o=dt(n,3);D(_,o[0],!0),D(h,o[1],!0),D(b,o[2],!0)})(await Promise.all([S.stats(),S.health(),S.retentionDistribution()]))}catch{}finally{D(ot,!1)}});function tt(n){return{healthy:"#10b981",degraded:"#f59e0b",critical:"#ef4444",empty:"#6b7280"}[n]||"#6b7280"}async function gt(){try{await S.consolidate(),await(async n=>{var o=dt(n,3);D(_,o[0],!0),D(h,o[1],!0),D(b,o[2],!0)})(await Promise.all([S.stats(),S.health(),S.retentionDistribution()]))}catch{}}var at=Ht(),nt=e(at);Dt(nt,{icon:"stats",title:"System Stats",subtitle:"Live health and retention across your memory store",accent:"recall",children:(n,o)=>{var w=At(),$=lt(w);{var E=F=>{var g=St(),O=e(g),L=i(O,2),k=e(L,!0);a(L);var H=i(L,2),et=e(H);a(H),a(g),j((T,Y,rt,q)=>{B(g,`color: ${T??""}`),B(O,`color: ${Y??""}; background: ${rt??""}`),R(k,q),R(et,`v${t(h).version??""}`)},[()=>tt(t(h).status),()=>tt(t(h).status),()=>tt(t(h).status),()=>t(h).status.toUpperCase()]),p(F,g)};V($,F=>{t(h)&&F(E)})}p(n,w)},$$slots:{default:!0}});var xt=i(nt,2);{var ft=n=>{var o=Ot();W(o,20,()=>Array(8),X,(w,$)=>{var E=Et();p(w,E)}),a(o),p(n,o)},_t=n=>{var o=Lt(),w=lt(o),$=e(w),E=e($),F=e(E);U(F,{get value(){return t(_).totalMemories}}),a(E),K(2),a($),c($,(s,r)=>{var d;return(d=C)==null?void 0:d(s,r)},()=>({delay:0})),c($,s=>{var r;return(r=Z)==null?void 0:r(s)});var g=i($,2),O=e(g),L=e(O);U(L,{get value(){return t(_).averageRetention},scale:100,decimals:1,suffix:"%"}),a(O),K(2),a(g),c(g,(s,r)=>{var d;return(d=C)==null?void 0:d(s,r)},()=>({delay:70})),c(g,s=>{var r;return(r=Z)==null?void 0:r(s)});var k=i(g,2),H=e(k),et=e(H);U(et,{get value(){return t(_).dueForReview}}),a(H),K(2),a(k),c(k,(s,r)=>{var d;return(d=C)==null?void 0:d(s,r)},()=>({delay:140})),c(k,s=>{var r;return(r=Z)==null?void 0:r(s)});var T=i(k,2),Y=e(T),rt=e(Y);U(rt,{get value(){return t(_).embeddingCoverage},decimals:0,suffix:"%"}),a(Y),K(2),a(T),c(T,(s,r)=>{var d;return(d=C)==null?void 0:d(s,r)},()=>({delay:210})),c(T,s=>{var r;return(r=Z)==null?void 0:r(s)}),a(w);var q=i(w,2);{var ht=s=>{var r=Ft(),d=lt(r),vt=i(e(d),2);W(vt,21,()=>t(b).distribution,X,(u,l,x)=>{const A=Q(()=>Math.max(...t(b).distribution.map(I=>I.count),1)),N=Q(()=>t(l).count/t(A)*100),y=Q(()=>x<3?"#ef4444":x<5?"#f59e0b":x<7?"#10b981":"#6366f1");var v=Tt(),m=e(v),M=e(m,!0);a(m);var P=i(m,2),z=i(P,2),it=e(z,!0);a(z),a(v),j(()=>{R(M,t(l).count),B(P,`height: ${t(N)??""}%; background: ${t(y)??""}; opacity: 0.7; min-height: 2px`),R(it,t(l).range)}),p(u,v)}),a(vt),a(d),c(d,u=>{var l;return(l=C)==null?void 0:l(u)});var G=i(d,2),ct=i(e(G),2);W(ct,21,()=>Object.entries(t(b).byType),X,(u,l)=>{var x=Q(()=>dt(t(l),2));let A=()=>t(x)[0],N=()=>t(x)[1];var y=Nt(),v=e(y),m=i(v,2),M=e(m,!0);a(m);var P=i(m,2),z=e(P);U(z,{get value(){return N()}}),a(P),a(y),j(()=>{B(v,`background: ${(ut[A()]||"#8B95A5")??""}; box-shadow: 0 0 8px ${(ut[A()]||"#8B95A5")??""}80`),R(M,A())}),p(u,y)}),a(ct),a(G),c(G,u=>{var l;return(l=C)==null?void 0:l(u)});var yt=i(G,2);{var wt=u=>{var l=Bt(),x=e(l),A=i(e(x));a(x);var N=i(x,2);W(N,21,()=>t(b).endangered.slice(0,20),X,(y,v)=>{var m=jt(),M=e(m),P=e(M);a(M);var z=i(M,2),it=e(z);a(z);var I=i(z,2),$t=e(I,!0);a(I),a(m),j(zt=>{R(P,`${zt??""}%`),B(it,`width: ${t(v).retentionStrength*100}%`),R($t,t(v).content)},[()=>(t(v).retentionStrength*100).toFixed(0)]),p(y,m)}),a(N),a(l),c(l,y=>{var v;return(v=C)==null?void 0:v(y)}),j(()=>R(A,` Endangered Memories (${t(b).endangered.length??""})`)),p(u,l)};V(yt,u=>{t(b).endangered.length>0&&u(wt)})}p(s,r)};V(q,s=>{t(b)&&s(ht)})}var st=i(q,2),bt=e(st);a(st),c(st,s=>{var r;return(r=C)==null?void 0:r(s)}),j(()=>B(O,`color: ${t(_).averageRetention>.7?"var(--color-recall)":t(_).averageRetention>.4?"var(--color-warning)":"var(--color-decay)"}`)),Pt("click",bt,gt),p(n,o)};V(xt,n=>{t(ot)?n(ft):t(_)&&t(h)&&n(_t,1)})}a(at),p(mt,at),kt()}Mt(["click"]);export{aa as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/20.Di2Q3Va0.js.br b/apps/dashboard/build/_app/immutable/nodes/20.Di2Q3Va0.js.br deleted file mode 100644 index fdc3112..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/20.Di2Q3Va0.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/20.Di2Q3Va0.js.gz b/apps/dashboard/build/_app/immutable/nodes/20.Di2Q3Va0.js.gz deleted file mode 100644 index 683ae7f..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/20.Di2Q3Va0.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/21.BAlasPHS.js b/apps/dashboard/build/_app/immutable/nodes/21.BAlasPHS.js deleted file mode 100644 index 6cb15a2..0000000 --- a/apps/dashboard/build/_app/immutable/nodes/21.BAlasPHS.js +++ /dev/null @@ -1,3 +0,0 @@ -import"../chunks/Bzak7iHL.js";import{o as ce}from"../chunks/TZu9D97Z.js";import{p as pe,s as x,d as ue,a as d,b as _e,j as v,e as t,i as u,g as e,n as O,r as a,h as c,u as fe,t as h}from"../chunks/wpu9U-D0.js";import{d as xe,s as y,a as be}from"../chunks/D8mhvFt8.js";import{i as P}from"../chunks/DKve45Wd.js";import{e as w,i as B,s as ge}from"../chunks/60_R_Vbt.js";import{a as he,r as ye}from"../chunks/P1-U_Xsj.js";import{s as q}from"../chunks/EqHb-9AZ.js";import{a as we}from"../chunks/CZfHMhLI.js";import{N as I}from"../chunks/CcUbQ_Wl.js";import{P as ke}from"../chunks/BHDZZvku.js";import{A as G}from"../chunks/DcKTNC6e.js";import{D as $e}from"../chunks/CmbJHhgy.js";import{I as Ae}from"../chunks/D7A-gG4Z.js";var Le=v('
                memories
                '),Te=v('
                '),De=v('
                '),je=v(`

                No memories in this window yet — widen the range or come back once Vestige has - been remembering a while.

                `),Ce=v('
                '),Ne=v(' '),Oe=v('
                '),Pe=v('
                '),Be=v('
                '),Ie=v('
                '),Me=v('
                ');function Ue(J,K){pe(K,!0);let f=x(ue([])),k=x(!0),M=x(14),b=x(null);ce(()=>S());async function S(){u(k,!0);try{const s=await we.timeline(e(M),500);u(f,s.timeline,!0)}catch{u(f,[],!0)}finally{u(k,!1)}}const Q=[{value:"7",label:"Last 7 days"},{value:"14",label:"Last 14 days"},{value:"30",label:"Last 30 days"},{value:"90",label:"Last 90 days"},{value:"365",label:"Last year"}];let E=x("14");function U(s){u(M,parseInt(s,10),!0),S()}let X=fe(()=>e(f).reduce((s,r)=>s+r.count,0));var $=Me(),R=t($);ke(R,{icon:"timeline",title:"Timeline",subtitle:"Watch your memories accumulate, day by day",accent:"synapse",children:(s,r)=>{var l=Le(),m=t(l),i=t(m);G(i,{get value(){return e(X)}}),O(),a(m);var A=c(m,2);$e(A,{get options(){return Q},label:"Range",icon:"schedule",onChange:U,get value(){return e(E)},set value(_){u(E,_,!0)}}),a(l),d(s,l)},$$slots:{default:!0}});var Z=c(R,2);{var ee=s=>{var r=De();w(r,20,()=>Array(7),B,(l,m)=>{var i=Te();d(l,i)}),a(r),d(s,r)},ae=s=>{var r=je(),l=t(r),m=t(l);Ae(m,{name:"timeline",size:48,strokeWidth:1.2}),a(l),O(2),a(r),d(s,r)},te=s=>{var r=Ie(),l=c(t(r),2);w(l,23,()=>e(f),m=>m.date,(m,i,A)=>{var _=Be(),g=c(t(_),2),L=t(g),T=t(L),D=t(T),se=t(D,!0);a(D);var W=c(D,2),re=t(W);G(re,{get value(){return e(i).count}}),O(),a(W),a(T);var z=c(T,2),F=t(z);w(F,17,()=>e(i).memories.slice(0,10),B,(n,o)=>{var p=Ce();h(()=>q(p,`background: ${(I[e(o).nodeType]||"#8B95A5")??""}; opacity: ${.3+e(o).retentionStrength*.7}; box-shadow: 0 0 5px ${(I[e(o).nodeType]||"#8B95A5")??""}66`)),d(n,p)});var ie=c(F,2);{var oe=n=>{var o=Ne(),p=t(o);a(o),h(()=>y(p,`+${e(i).memories.length-10}`)),d(n,o)};P(ie,n=>{e(i).memories.length>10&&n(oe)})}a(z),a(L);var le=c(L,2);{var ne=n=>{var o=Pe();w(o,21,()=>e(i).memories,B,(p,j)=>{var C=Oe(),H=t(C),N=c(H,2),V=t(N),de=t(V,!0);a(V),a(N);var Y=c(N,2),ve=t(Y);a(Y),a(C),h(me=>{q(H,`background: ${(I[e(j).nodeType]||"#8B95A5")??""}`),y(de,e(j).content),y(ve,`${me??""}%`)},[()=>(e(j).retentionStrength*100).toFixed(0)]),d(p,C)}),a(o),d(n,o)};P(le,n=>{e(b)===e(i).date&&n(ne)})}a(g),a(_),he(_,(n,o)=>{var p;return(p=ye)==null?void 0:p(n,o)},()=>({delay:Math.min(e(A)*35,350),y:12})),h(()=>{ge(g,1,`lift w-full text-left p-4 glass-subtle rounded-xl hover:bg-white/[0.04] transition-all duration-200 - ${e(b)===e(i).date?"!border-synapse/40 glow-synapse":""}`),y(se,e(i).date)}),be("click",g,()=>u(b,e(b)===e(i).date?null:e(i).date,!0)),d(m,_)}),a(l),a(r),d(s,r)};P(Z,s=>{e(k)?s(ee):e(f).length===0?s(ae,1):s(te,!1)})}a($),d(J,$),_e()}xe(["click"]);export{Ue as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/21.BAlasPHS.js.br b/apps/dashboard/build/_app/immutable/nodes/21.BAlasPHS.js.br deleted file mode 100644 index 4105f13..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/21.BAlasPHS.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/21.BAlasPHS.js.gz b/apps/dashboard/build/_app/immutable/nodes/21.BAlasPHS.js.gz deleted file mode 100644 index 6a542c2..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/21.BAlasPHS.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/22.C719k-1W.js b/apps/dashboard/build/_app/immutable/nodes/22.C719k-1W.js deleted file mode 100644 index 6743212..0000000 --- a/apps/dashboard/build/_app/immutable/nodes/22.C719k-1W.js +++ /dev/null @@ -1,6 +0,0 @@ -import"../chunks/Bzak7iHL.js";import{o as it}from"../chunks/TZu9D97Z.js";import{p as ct,s as p,d as mt,t as f,g as a,a as v,b as dt,x as pt,h as s,e as r,i as c,j as h,ar as vt,n as $e,r as o,k as ut}from"../chunks/wpu9U-D0.js";import{d as ht,e as Qe,s as u,a as bt}from"../chunks/D8mhvFt8.js";import{i as We}from"../chunks/DKve45Wd.js";import{e as k,a as Ge,i as P,r as W,s as He}from"../chunks/60_R_Vbt.js";import{h as yt}from"../chunks/DzesjbbJ.js";import{s as gt}from"../chunks/EqHb-9AZ.js";import{b as V}from"../chunks/CnZzd20v.js";import{b as Re}from"../chunks/DrafHjYM.js";import{b as ft}from"../chunks/g5OnrUYZ.js";import{b as Ue}from"../chunks/Bxs5UR9-.js";var qt=h(''),_t=h('
                '),wt=h("

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

                '),xt=h('

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

                Checking the onboarding notes...

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

                June early access

                Vestige Pro

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

                Early access

                Reserve a Pro seat

                Why Pro exists

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

                Always-on answers

                The support bot handles the first wave.

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

                Onboarding bot

                May to June

                The plan is simple.

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

                Computing activation…

                '),ut=Y('

                Activation failed

                '),ft=Y(`

                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.

                `),vt=Y(`

                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.

                `),pt=Y('
                Seed

                '),mt=Y(`
                Seed Memory
                Live bursts fired:
                `);function Gt(a,m){Ae(m,!0);let v=I(""),h=I(!1),M=I(!1),S=I(null),w=I(null),B=I(ue([])),P=I(!0),Z=I(0),F=I(null),G=I(0);const T=new Map;function H(t){T.set(t.id,t)}function K(t){return{id:t.id,label:ce(t.content,t.id),nodeType:t.nodeType}}function ce(t,s){if(t&&t.trim().length>0){const o=t.trim();return o.length>60?o.slice(0,60)+"…":o}return s.slice(0,8)}function $(t){const s=T.get(t);return s?K(s):{id:t,label:t.slice(0,8),nodeType:"note"}}async function ee(){const t=e(v).trim();if(!t){p(S,null);return}p(h,!0),p(M,!0),p(S,null),p(w,null),p(B,[],!0);try{const s=await _e.search(t,1);if(!s.results||s.results.length===0)return;const o=s.results[0];H(o),p(w,K(o),!0);const d=await _e.explore(o.id,"associations",void 0,15),N=(d==null?void 0:d.results)??(d==null?void 0:d.nodes)??(d==null?void 0:d.associations)??[],A=[];for(const k of N){if(!k||typeof k!="object"||!("id"in k))continue;const D=k;H(D),A.push(K(D))}p(B,A,!0)}catch(s){p(S,s instanceof Error?s.message:String(s),!0),p(w,null),p(B,[],!0)}finally{p(h,!1)}}let V=null,Q=null,le=!1;Me(()=>{V=De.subscribe(t=>{if(!t||t.length===0)return;if(!le){Q=t[0],le=!0;return}if(!e(P)){Q=t[0];return}const s=rt(t,Q);if(Q=t[0],s.length!==0)for(const o of s){const d=$(o.source_id),N=o.target_ids.map(A=>$(A));p(Z,e(Z)+1),p(F,{source:d,neighbours:N},!0),p(G,e(G)+1)}})}),Se(()=>{V&&V()});var te=mt(),O=n(te);Ke(O,{icon:"activation",title:"Spreading Activation",subtitle:"Collins & Loftus 1975 — activation spreads from a seed memory to neighbours along semantic edges, decaying 0.93 per frame until it drops below 0.05. Search seeds a focused burst; live mode overlays every engine spread in real time.",accent:"synapse",children:(t,s)=>{var o=lt(),d=n(o);let N;var A=y(d,2),k=n(A,!0);c(A);var D=y(A,4);Ue(D,{get value(){return e(G)},class:"text-synapse-glow font-semibold"}),oe(2),c(o),L(()=>{N=Le(d,1,"ping-host inline-flex h-2 w-2 rounded-full",null,N,{breathe:e(P)}),j(k,e(P)?"Live":"Paused")}),E(t,o)},$$slots:{default:!0}});var q=y(O,2),re=y(n(q),2),W=n(re);be(W);var l=y(W,2),r=n(l);de(r,{name:"activation",size:15});var x=y(r);c(l),ye(l,t=>{var s;return(s=ze)==null?void 0:s(t)}),c(re),c(q),ye(q,(t,s)=>{var o;return(o=Re)==null?void 0:o(t,s)},()=>({delay:60}));var b=y(q,2),i=n(b),f=n(i);be(f),oe(2),c(i);var g=y(i,2),_=y(n(g)),X=n(_,!0);c(_),c(g),c(b);var U=y(b,2),C=n(U);{var R=t=>{var s=dt(),o=n(s),d=n(o),N=n(d);de(N,{name:"activation",size:32}),c(d),oe(2),c(o),c(s),E(t,s)},se=t=>{var s=ut(),o=n(s),d=y(n(o),4),N=n(d,!0);c(d),c(o),c(s),L(()=>j(N,e(S))),E(t,s)},z=t=>{var s=ft(),o=n(s),d=n(o),N=n(d);de(N,{name:"search",size:30}),c(d);var A=y(d,4),k=y(n(A)),D=n(k);c(k),oe(),c(A),c(o),c(s),L(()=>j(D,`"${e(v)??""}"`)),E(t,s)},ie=t=>{var s=vt(),o=n(s),d=n(o),N=n(d);de(N,{name:"activation",size:32}),c(d),oe(4),c(o),c(s),E(t,s)},ae=t=>{ct(t,{width:1040,height:560,get source(){return e(w)},get neighbours(){return e(B)},get liveBurstKey(){return e(Z)},get liveBurst(){return e(F)}})};fe(C,t=>{e(h)?t(R):e(S)?t(se,1):!e(w)&&e(M)?t(z,2):e(w)?t(ae,!1):t(ie,3)})}c(U);var Ee=y(U,2);{var Pe=t=>{var s=pt(),o=y(n(s),2),d=n(o,!0);c(o);var N=y(o,2),A=n(N),k=n(A,!0);c(A);var D=y(A,2),Fe=n(D);c(D),c(N),c(s),L(()=>{j(d,e(w).label),j(k,e(w).nodeType),j(Fe,`${e(B).length??""} neighbours`)}),E(t,s)};fe(Ee,t=>{e(w)&&t(Pe)})}c(te),L(()=>{l.disabled=e(h),j(x,` ${e(h)?"Activating…":"Activate"}`),j(X,e(G))}),ge("keydown",W,t=>t.key==="Enter"&&ee()),Te(W,()=>e(v),t=>p(v,t)),ge("click",l,ee),Oe(f,()=>e(P),t=>p(P,t)),E(a,te),Be()}Ce(["keydown","click"]);export{Gt as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/4.BpOpkZuP.js.br b/apps/dashboard/build/_app/immutable/nodes/4.BpOpkZuP.js.br deleted file mode 100644 index 5e59fe8..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/4.BpOpkZuP.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/4.BpOpkZuP.js.gz b/apps/dashboard/build/_app/immutable/nodes/4.BpOpkZuP.js.gz deleted file mode 100644 index eaa3ab1..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/4.BpOpkZuP.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..c37a0d2 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.CK5gRe3F.js b/apps/dashboard/build/_app/immutable/nodes/5.CK5gRe3F.js deleted file mode 100644 index a1bfb65..0000000 --- a/apps/dashboard/build/_app/immutable/nodes/5.CK5gRe3F.js +++ /dev/null @@ -1,2 +0,0 @@ -import"../chunks/Bzak7iHL.js";import{o as es}from"../chunks/TZu9D97Z.js";import{p as Ba,e as s,r as a,h as t,n as ye,t as _,a as l,b as Ha,f as Sa,j as n,g as e,s as pe,d as La,o as as,i as R,u as ue}from"../chunks/wpu9U-D0.js";import{d as Oa,s as r,a as Fe}from"../chunks/D8mhvFt8.js";import{i as k}from"../chunks/DKve45Wd.js";import{s as J,e as K,a as _a,r as ss,i as ja}from"../chunks/60_R_Vbt.js";import{a as te,r as re}from"../chunks/P1-U_Xsj.js";import{s as _e}from"../chunks/EqHb-9AZ.js";import{b as ts}from"../chunks/CnZzd20v.js";import{p as rs,s as ua,a as vs}from"../chunks/ByYB047u.js";import{P as ls}from"../chunks/BHDZZvku.js";import{I as Wa}from"../chunks/D7A-gG4Z.js";import{A as Da}from"../chunks/DcKTNC6e.js";import{g as ns}from"../chunks/dCAmqaEc.js";import{a as Xe}from"../chunks/CZfHMhLI.js";import{l as is,t as cs,i as os,d as ds}from"../chunks/BhIgFntf.js";var ps=n('
                '),us=n('
                Activation path
                '),_s=n(' '),ys=n('
                Retrieved
                '),ws=n(' '),ms=n('
                Suppressed
                '),fs=n(" ",1),hs=n('
                retrieved
                suppressed
                trust floor
                ');function qs(c,w){Ba(w,!0);let X=rs(w,"compact",3,!1);const we={low:"var(--color-recall, #10b981)",medium:"#f59e0b",high:"#f43f5e"};function Ze(){const j=w.receipt.retrieved[0];if(!j)return;const ce=w.receipt.retrieved.join(",");ns(`/graph?center=${encodeURIComponent(j)}&focus=${encodeURIComponent(ce)}`)}var ve=hs();let me,ea;var Y=s(ve),B=s(Y),q=s(B,!0);a(B);var fe=t(B,2);let le;var P=s(fe);a(fe),a(Y);var ne=t(Y,2),he=s(ne),aa=s(he),m=s(aa,!0);a(aa),ye(2),a(he);var Ie=t(he,2),Le=s(Ie),sa=s(Le,!0);a(Le),ye(2),a(Ie);var je=t(Ie,2),De=s(je),ya=s(De);a(De),ye(2),a(je),a(ne);var Ue=t(ne,2);{var wa=j=>{var ce=fs(),Re=Sa(ce);{var ra=T=>{var D=us(),V=t(s(D),2);K(V,16,()=>w.receipt.activation_path,N=>N,(N,Z)=>{var F=ps(),qe=s(F,!0);a(F),_(()=>r(qe,Z)),l(N,F)}),a(D),l(T,D)};k(Re,T=>{w.receipt.activation_path.length&&T(ra)})}var Be=t(Re,2);{var va=T=>{var D=ys(),V=t(s(D),2);K(V,20,()=>w.receipt.retrieved,N=>N,(N,Z)=>{var F=_s(),qe=s(F,!0);a(F),_(Me=>r(qe,Me),[()=>Z.slice(0,8)]),l(N,F)}),a(V),a(D),l(T,D)};k(Be,T=>{w.receipt.retrieved.length&&T(va)})}var ma=t(Be,2);{var He=T=>{var D=ms(),V=t(s(D),2);K(V,21,()=>w.receipt.suppressed,N=>N.id,(N,Z)=>{var F=ws(),qe=s(F);a(F),_((Me,la)=>{_a(F,"title",e(Z).reason),r(qe,`${Me??""} · ${la??""}`)},[()=>e(Z).id.slice(0,8),()=>e(Z).reason.replace("_"," ")]),l(N,F)}),a(V),a(D),l(T,D)};k(ma,T=>{w.receipt.suppressed.length&&T(He)})}l(j,ce)};k(Ue,j=>{X()||j(wa)})}var ie=t(Ue,2),ta=s(ie);Wa(ta,{name:"sparkle",size:14}),ye(),a(ie),a(ve),_(j=>{me=J(ve,1,"receipt svelte-1wdzvwu",null,me,{compact:X()}),ea=_e(ve,"",ea,{"--risk":we[w.receipt.decay_risk]}),r(q,w.receipt.receipt_id),le=_e(fe,"",le,{color:we[w.receipt.decay_risk]}),r(P,`decay: ${w.receipt.decay_risk??""}`),r(m,w.receipt.retrieved.length),r(sa,w.receipt.suppressed.length),r(ya,`${j??""}%`),ie.disabled=!w.receipt.retrieved.length},[()=>(w.receipt.trust_floor*100).toFixed(0)]),Fe("click",ie,Ze),l(c,ve),Ha()}Oa(["click"]);function Ye(c){switch(c){case"mcp.call":return"var(--color-synapse-glow, #818cf8)";case"memory.retrieve":return"var(--color-recall, #10b981)";case"memory.suppress":return"#a78bfa";case"memory.write":return"#38bdf8";case"contradiction.detected":return"#fb7185";case"sanhedrin.veto":return"#f43f5e";case"dream.patch":return"#c084fc";default:return"var(--color-synapse, #6366f1)"}}function Ne(c){switch(c){case"mcp.call":return"Tool Call";case"memory.retrieve":return"Retrieved";case"memory.suppress":return"Suppressed";case"memory.write":return"Wrote";case"contradiction.detected":return"Contradiction";case"sanhedrin.veto":return"Veto";case"dream.patch":return"Dream Patch";default:return c}}function Ma(c){switch(c){case"mcp.call":return"⟐";case"memory.retrieve":return"◉";case"memory.suppress":return"⊘";case"memory.write":return"✎";case"contradiction.detected":return"⚡";case"sanhedrin.veto":return"⛔";case"dream.patch":return"☾";default:return"•"}}function Ea(c){switch(c.type){case"mcp.call":return`${c.tool} · args ${c.argsHash.slice(0,8)}`;case"memory.retrieve":return`${c.ids.length} ${c.ids.length===1?"memory":"memories"} surfaced`;case"memory.suppress":return`${c.id.slice(0,8)} — ${c.reason.replace("_"," ")}`;case"memory.write":return`${c.id.slice(0,8)} — ${c.source}`;case"contradiction.detected":return c.detail;case"sanhedrin.veto":return`"${c.claim}" (conf ${(c.confidence*100).toFixed(0)}%)`;case"dream.patch":return`${c.proposalIds.length} consolidation proposal(s)`;default:return""}}function gs(c){switch(c.type){case"memory.retrieve":return c.ids;case"memory.suppress":case"memory.write":return[c.id];case"contradiction.detected":return c.ids;case"sanhedrin.veto":return c.evidenceIds;case"dream.patch":return c.proposalIds;default:return[]}}function xs(c){return!Number.isFinite(c)||c<=0?"—":new Date(c).toLocaleTimeString(void 0,{hour12:!1,hour:"2-digit",minute:"2-digit",second:"2-digit"})}function Ua(c,w){return Math.max(0,c-w)}var bs=n(' ',1),ks=n(' '),zs=n('awaiting…'),Cs=n(`

                No agent runs recorded yet. Make an MCP tool call — every call is - recorded here.

                `),Is=n(' '),Rs=n(' '),Ms=n(' '),Es=n(' '),Ss=n('
              • '),Ws=n('
                  '),As=n('
                  Loading trace…
                  '),Ps=n('
                  '),Ts=n('
                  Select a run to replay.
                  '),Ns=n(' '),Fs=n(""),Ls=n(' '),js=n(' '),Ds=n('
                  '),Us=n(' '),Bs=n('
                  vs
                  '),Hs=n(' '),Os=n('
                  '),Vs=n('

                  '),$s=n('

                  No memories touched yet.

                  '),Gs=n(' '),Js=n('
                  '),Ks=n('

                  Receipts — proof behind retrievals

                  '),Qs=n('
                • '),Xs=n('
                  Step

                  Memory pulse — touched this run

                  Event producers — this run

                  • mcp.call · memory.write · memory.retrieve · memory.suppress live
                  • contradiction.detected
                  • dream.patch
                  • sanhedrin.veto

                  Event log

                    ',1),Ys=n('
                    '),Zs=n('
                    '),et=n('
                    trace events

                    Watch the agent think. Watch memory change. Watch the receipt prove why.

                    '),at=n('
                    WebSocket
                    Live runId
                    Last event
                    Events seen
                    ');function ft(c,w){Ba(w,!0);const X=()=>ua(is,"$lastTraceEvent",me),we=()=>ua(os,"$isConnected",me),Ze=()=>ua(ds,"$liveRunId",me),ve=()=>ua(cs,"$traceEvents",me),[me,ea]=vs();let Y=pe(La([])),B=pe(null),q=pe(null),fe=pe(!1),le=pe(null),P=pe(0),ne=pe(!1),he=pe(La([]));const aa=ue(()=>e(q)?e(q).events.slice(0,e(P)+1):[]),m=ue(()=>e(q)&&e(q).events.length?e(q).events[e(P)]:null),Ie=ue(()=>{var i,d;return((d=(i=e(q))==null?void 0:i.events[0])==null?void 0:d.at)??0}),Le=ue(()=>Array.from(new Set(e(aa).flatMap(gs)))),sa=ue(()=>{var i;return((i=e(q))==null?void 0:i.events.some(d=>d.type==="sanhedrin.veto"))??!1}),je=ue(()=>{var i;return((i=e(q))==null?void 0:i.events.some(d=>d.type==="dream.patch"))??!1}),De=ue(()=>{var i;return((i=e(q))==null?void 0:i.events.some(d=>d.type==="contradiction.detected"))??!1});async function ya(){try{const i=await Xe.traces.list(100);R(Y,i.runs,!0),!e(B)&&e(Y).length&&Ue(e(Y)[0].runId)}catch(i){R(le,String(i),!0)}}async function Ue(i){R(B,i,!0),R(fe,!0),R(le,null);try{R(q,await Xe.traces.get(i),!0),R(P,Math.max(0,(e(q).events.length||1)-1),!0),R(he,(await Xe.receipts.listForRun(i,8)).receipts,!0)}catch(d){R(le,String(d),!0),R(q,null)}finally{R(fe,!1)}}function wa(){e(B)&&(window.location.href=Xe.traces.exportUrl(e(B)))}as(()=>{var I;const i=X();if(!i)return;const d=(I=i.data)==null?void 0:I.run_id;d&&d===e(B)&&Xe.traces.get(e(B)).then(M=>{R(q,M,!0),R(P,Math.max(0,M.events.length-1),!0)})}),es(ya);var ie=at(),ta=s(ie);ls(ta,{icon:"blackbox",title:"Agent Black Box",subtitle:"Watch the agent think. Watch memory change. Watch the receipt prove why.",accent:"synapse",children:(i,d)=>{var I=bs(),M=Sa(I);let E;var L=s(M);Wa(L,{name:"sparkle",size:14}),ye(),a(M);var ee=t(M,2),Ee=s(ee);Wa(Ee,{name:"feed",size:14}),ye(),a(ee),_(()=>{E=J(M,1,"mode-toggle svelte-1ayqwv0",null,E,{on:e(ne)}),ee.disabled=!e(B)}),Fe("click",M,()=>R(ne,!e(ne))),Fe("click",ee,wa),l(i,I)},$$slots:{default:!0}});var j=t(ta,2),ce=s(j),Re=t(s(ce),2);let ra;var Be=s(Re);let va;var ma=t(Be);a(Re),a(ce);var He=t(ce,2),T=t(s(He),2),D=s(T,!0);a(T),a(He);var V=t(He,2),N=t(s(V),2),Z=s(N);{var F=i=>{var d=ks();let I;var M=s(d,!0);a(d),_((E,L)=>{I=_e(d,"",I,E),r(M,L)},[()=>{var E,L;return{"--c":Ye((L=(E=X().data)==null?void 0:E.event)==null?void 0:L.type)}},()=>{var E,L;return Ne((L=(E=X().data)==null?void 0:E.event)==null?void 0:L.type)}]),l(i,d)},qe=i=>{var d=zs();l(i,d)};k(Z,i=>{X()?i(F):i(qe,!1)})}a(N),a(V);var Me=t(V,2),la=t(s(Me),2),Va=s(la);Da(Va,{get value(){return ve().length}}),a(la),a(Me),a(j),te(j,i=>{var d;return(d=re)==null?void 0:d(i)});var $a=t(j,2);{var Ga=i=>{var d=Ys(),I=s(d),M=t(s(I),2);{var E=p=>{var y=Cs();l(p,y)},L=p=>{var y=Ws();K(y,21,()=>e(Y),U=>U.runId,(U,f)=>{var ae=Ss(),$=s(ae);let oe;var xe=s($),de=s(xe),Oe=s(de,!0);a(de);var h=t(de,2),Se=s(h,!0);a(h),a(xe);var be=t(xe,2),ke=s(be),We=s(ke);a(ke);var ia=t(ke,2);{var qa=x=>{var g=Is(),Q=s(g);a(g),_(()=>r(Q,`↑${e(f).retrievedCount??""}`)),l(x,g)};k(ia,x=>{e(f).retrievedCount&&x(qa)})}var ca=t(ia,2);{var Ae=x=>{var g=Rs(),Q=s(g);a(g),_(()=>r(Q,`⊘${e(f).suppressedCount??""}`)),l(x,g)};k(ca,x=>{e(f).suppressedCount&&x(Ae)})}var Ve=t(ca,2);{var Pe=x=>{var g=Ms(),Q=s(g);a(g),_(()=>r(Q,`✎${e(f).writeCount??""}`)),l(x,g)};k(Ve,x=>{e(f).writeCount&&x(Pe)})}var oa=t(Ve,2);{var da=x=>{var g=Es(),Q=s(g);a(g),_(()=>r(Q,`⛔${e(f).vetoCount??""}`)),l(x,g)};k(oa,x=>{e(f).vetoCount&&x(da)})}a(be),a($),a(ae),_(x=>{oe=J($,1,"run-row svelte-1ayqwv0",null,oe,{active:e(f).runId===e(B)}),r(Oe,x),r(Se,e(f).firstTool??"—"),r(We,`${e(f).eventCount??""} ev`)},[()=>e(f).runId.replace("run_","").slice(0,10)]),Fe("click",$,()=>Ue(e(f).runId)),l(U,ae)}),a(y),l(p,y)};k(M,p=>{e(Y).length===0?p(E):p(L,!1)})}a(I),te(I,p=>{var y;return(y=re)==null?void 0:y(p)});var ee=t(I,2),Ee=s(ee);{var fa=p=>{var y=As();l(p,y)},na=p=>{var y=Ps(),U=s(y,!0);a(y),_(()=>r(U,e(le))),l(p,y)},ha=p=>{var y=Ts();l(p,y)},ge=p=>{var y=Xs(),U=Sa(y),f=s(U),ae=s(f),$=t(s(ae)),oe=s($,!0);a($);var xe=t($);a(ae);var de=t(ae,2);{var Oe=o=>{var v=Ns(),b=s(v);a(v),_(u=>r(b,`+${u??""}ms`),[()=>Ua(e(m).at,e(Ie))]),l(o,v)};k(de,o=>{e(m)&&o(Oe)})}a(f);var h=t(f,2);ss(h);var Se=t(h,2);K(Se,21,()=>e(q).events,ja,(o,v,b)=>{var u=Fs();let z,se;_((G,ze,Te)=>{z=J(u,1,"tick svelte-1ayqwv0",null,z,{past:b<=e(P)}),_a(u,"title",G),_a(u,"aria-label",ze),se=_e(u,"",se,Te)},[()=>Ne(e(v).type),()=>`Step ${b+1}: ${Ne(e(v).type)}`,()=>({"--c":Ye(e(v).type)})]),Fe("click",u,()=>R(P,b,!0)),l(o,u)}),a(Se),a(U),te(U,o=>{var v;return(v=re)==null?void 0:v(o)});var be=t(U,2);{var ke=o=>{var v=Vs();let b;var u=s(v),z=s(u),se=s(z,!0);a(z);var G=t(z,2),ze=s(G,!0);a(G);var Te=t(G,2),$e=s(Te,!0);a(Te),a(u);var Ge=t(u,2),Je=s(Ge,!0);a(Ge);var ba=t(Ge,2);{var pa=S=>{var C=Ds();K(C,20,()=>e(m).ids,W=>W,(W,H)=>{var O=js();let A;var Ce=s(O),Ke=s(Ce,!0);a(Ce);var Ca=t(Ce,2);{var Ia=Qe=>{var Ra=Ls(),Ya=s(Ra);a(Ra),_(Za=>r(Ya,`${Za??""}%`),[()=>(e(m).activation[H]*100).toFixed(0)]),l(Qe,Ra)};k(Ca,Qe=>{e(m).activation[H]!=null&&Qe(Ia)})}a(O),_(Qe=>{A=_e(O,"",A,{"--a":e(m).activation[H]??0}),r(Ke,Qe)},[()=>H.slice(0,8)]),l(W,O)}),a(C),l(S,C)},ka=S=>{var C=Bs(),W=s(C),H=s(W);a(W);var O=t(W,4);K(O,16,()=>e(m).ids.filter(A=>A!==e(m).winnerId),A=>A,(A,Ce)=>{var Ke=Us(),Ca=s(Ke,!0);a(Ke),_(Ia=>r(Ca,Ia),[()=>Ce.slice(0,8)]),l(A,Ke)}),a(C),_(A=>r(H,`kept ${A??""}`),[()=>{var A;return(A=e(m).winnerId)==null?void 0:A.slice(0,8)}]),l(S,C)},za=S=>{var C=Os();K(C,20,()=>e(m).evidenceIds,W=>W,(W,H)=>{var O=Hs(),A=s(O,!0);a(O),_(Ce=>r(A,Ce),[()=>H.slice(0,8)]),l(W,O)}),a(C),l(S,C)};k(ba,S=>{e(m).type==="memory.retrieve"?S(pa):e(m).type==="contradiction.detected"?S(ka,1):e(m).type==="sanhedrin.veto"&&S(za,2)})}a(v),te(v,S=>{var C;return(C=re)==null?void 0:C(S)}),_((S,C,W,H,O)=>{b=_e(v,"",b,S),r(se,C),r(ze,W),r($e,H),r(Je,O)},[()=>({"--c":Ye(e(m).type)}),()=>Ma(e(m).type),()=>Ne(e(m).type),()=>xs(e(m).at),()=>Ea(e(m))]),l(o,v)};k(be,o=>{e(m)&&o(ke)})}var We=t(be,2),ia=t(s(We),2);{var qa=o=>{var v=$s();l(o,v)},ca=o=>{var v=Js();K(v,20,()=>e(Le),b=>b,(b,u)=>{var z=Gs(),se=s(z,!0);a(z),_(G=>r(se,G),[()=>u.slice(0,8)]),l(b,z)}),a(v),l(o,v)};k(ia,o=>{e(Le).length===0?o(qa):o(ca,!1)})}a(We),te(We,o=>{var v;return(v=re)==null?void 0:v(o)});var Ae=t(We,2),Ve=t(s(Ae),2),Pe=t(s(Ve),2);let oa;var da=t(s(Pe),2),x=s(da,!0);a(da),a(Pe);var g=t(Pe,2);let Q;var Aa=t(s(g),2),Ka=s(Aa,!0);a(Aa),a(g);var ga=t(g,2);let Pa;var Ta=t(s(ga),2),Qa=s(Ta,!0);a(Ta),a(ga),a(Ve),a(Ae),te(Ae,o=>{var v;return(v=re)==null?void 0:v(o)});var Na=t(Ae,2);{var Xa=o=>{var v=Ks(),b=t(s(v),2);K(b,21,()=>e(he).slice(0,2),u=>u.receipt_id,(u,z)=>{qs(u,{get receipt(){return e(z)}})}),a(b),a(v),te(v,u=>{var z;return(z=re)==null?void 0:z(u)}),l(o,v)};k(Na,o=>{e(he).length&&o(Xa)})}var xa=t(Na,2),Fa=t(s(xa),2);K(Fa,21,()=>e(q).events,ja,(o,v,b)=>{var u=Qs();let z,se;var G=s(u),ze=s(G),Te=s(ze,!0);a(ze);var $e=t(ze,2),Ge=s($e,!0);a($e);var Je=t($e,2),ba=s(Je,!0);a(Je);var pa=t(Je,2),ka=s(pa);a(pa),a(G),a(u),_((za,S,C,W,H)=>{z=J(u,1,"log-row svelte-1ayqwv0",null,z,{active:b===e(P),dim:b>e(P)}),se=_e(u,"",se,za),r(Te,S),r(Ge,C),r(ba,W),r(ka,`+${H??""}ms`)},[()=>({"--c":Ye(e(v).type)}),()=>Ma(e(v).type),()=>Ne(e(v).type),()=>Ea(e(v)),()=>Ua(e(v).at,e(Ie))]),Fe("click",G,()=>R(P,b,!0)),l(o,u)}),a(Fa),a(xa),te(xa,o=>{var v;return(v=re)==null?void 0:v(o)}),_(o=>{r(oe,e(P)+1),r(xe,` / ${e(q).events.length??""}`),_a(h,"max",o),oa=J(Pe,1,"producer svelte-1ayqwv0",null,oa,{ok:e(De)}),r(x,e(De)?"fired this run":"no contradiction in this run"),Q=J(g,1,"producer caveat svelte-1ayqwv0",null,Q,{ok:e(je)}),r(Ka,e(je)?"fired this run":"No dream run in this trace"),Pa=J(ga,1,"producer caveat svelte-1ayqwv0",null,Pa,{ok:e(sa)}),r(Qa,e(sa)?"fired this run":"No veto producer connected (optional Sanhedrin hook, off by default)")},[()=>Math.max(0,e(q).events.length-1)]),ts(h,()=>e(P),o=>R(P,o)),l(p,y)};k(Ee,p=>{e(fe)?p(fa):e(le)?p(na,1):e(q)?p(ge,!1):p(ha,2)})}a(ee),a(d),l(i,d)},Ja=i=>{var d=et(),I=s(d),M=s(I);let E;var L=t(M,2),ee=s(L,!0);a(L),a(I);var Ee=t(I,2);{var fa=ge=>{const p=ue(()=>{var h;return(h=X().data)==null?void 0:h.event});var y=Zs();let U;var f=s(y),ae=s(f,!0);a(f);var $=t(f,2),oe=s($),xe=s(oe,!0);a(oe);var de=t(oe,2),Oe=s(de,!0);a(de),a($),a(y),_((h,Se,be,ke)=>{U=_e(y,"",U,h),r(ae,Se),r(xe,be),r(Oe,ke)},[()=>{var h;return{"--c":Ye((h=e(p))==null?void 0:h.type)}},()=>{var h;return Ma((h=e(p))==null?void 0:h.type)},()=>{var h;return Ne((h=e(p))==null?void 0:h.type)},()=>Ea(e(p))]),l(ge,y)};k(Ee,ge=>{X()&&ge(fa)})}var na=t(Ee,2),ha=s(na);Da(ha,{get value(){return ve().length}}),ye(2),a(na),ye(2),a(d),te(d,ge=>{var p;return(p=re)==null?void 0:p(ge)}),_(()=>{E=J(M,1,"dot big svelte-1ayqwv0",null,E,{live:we()}),r(ee,Ze()??"awaiting run…")}),l(i,d)};k($a,i=>{e(ne)?i(Ja,!1):i(Ga)})}a(ie),_(()=>{ra=J(Re,1,"spine-value svelte-1ayqwv0",null,ra,{live:we()}),va=J(Be,1,"dot svelte-1ayqwv0",null,va,{live:we()}),r(ma,` ${we()?"Connected":"Offline"}`),r(D,Ze()??"—")}),l(c,ie),Ha(),ea()}Oa(["click"]);export{ft as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/5.CK5gRe3F.js.br b/apps/dashboard/build/_app/immutable/nodes/5.CK5gRe3F.js.br deleted file mode 100644 index ade9603..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/5.CK5gRe3F.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/5.CK5gRe3F.js.gz b/apps/dashboard/build/_app/immutable/nodes/5.CK5gRe3F.js.gz deleted file mode 100644 index e46c0a3..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/5.CK5gRe3F.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/6.B_eyyG0t.js b/apps/dashboard/build/_app/immutable/nodes/6.B_eyyG0t.js new file mode 100644 index 0000000..b7a1186 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/6.B_eyyG0t.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/RBGf_S-E.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.B_eyyG0t.js.br b/apps/dashboard/build/_app/immutable/nodes/6.B_eyyG0t.js.br new file mode 100644 index 0000000..8e16cda Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/6.B_eyyG0t.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/6.B_eyyG0t.js.gz b/apps/dashboard/build/_app/immutable/nodes/6.B_eyyG0t.js.gz new file mode 100644 index 0000000..cd4dfa5 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/6.B_eyyG0t.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/6.DZyLUVX2.js b/apps/dashboard/build/_app/immutable/nodes/6.DZyLUVX2.js deleted file mode 100644 index d551f42..0000000 --- a/apps/dashboard/build/_app/immutable/nodes/6.DZyLUVX2.js +++ /dev/null @@ -1,2 +0,0 @@ -import"../chunks/Bzak7iHL.js";import{p as st,e as s,h as i,f as Ke,t as F,g as e,i as E,a as w,u as M,r as a,n as ee,b as it,s as te,j as S,au as Ye}from"../chunks/wpu9U-D0.js";import{d as ot,a as W,e as ye,s as k}from"../chunks/D8mhvFt8.js";import{i as B}from"../chunks/DKve45Wd.js";import{e as ue,a as n,s as St,i as Qe}from"../chunks/60_R_Vbt.js";import{a as le,r as me}from"../chunks/P1-U_Xsj.js";import{s as q}from"../chunks/EqHb-9AZ.js";import{p as $e}from"../chunks/ByYB047u.js";import{P as Rt}from"../chunks/BHDZZvku.js";import{D as Je}from"../chunks/CmbJHhgy.js";import{I as Ze}from"../chunks/D7A-gG4Z.js";import{A as ce}from"../chunks/DcKTNC6e.js";const nt=.7,lt=.5,Tt="#ef4444",At="#f59e0b",Et="#fde047";function Be(m){return m>nt?Tt:m>lt?At:Et}function mt(m){return m>nt?"strong":m>lt?"moderate":"mild"}const et="#8b5cf6",Dt={fact:"#3b82f6",concept:"#8b5cf6",event:"#f59e0b",person:"#10b981",place:"#06b6d4",note:"#6b7280",pattern:"#ec4899",decision:"#ef4444"};function tt(m){return m?Dt[m]??et:et}const rt=5,Ot=9;function Ct(m){if(!Number.isFinite(m))return rt;const v=m<0?0:m>1?1:m;return rt+v*Ot}const Ft=.12;function at(m,v){return v==null||v===m?1:Ft}function he(m,v=60){return m==null||typeof m!="string"||v<=0?"":m.length<=v?m:m.slice(0,v-1)+"…"}function Nt(m){if(!m||m.length===0)return 0;const v=new Set;for(const u of m)u.memory_a_id&&v.add(u.memory_a_id),u.memory_b_id&&v.add(u.memory_b_id);return v.size}function Gt(m){if(!m||m.length===0)return 0;let v=0;for(const u of m)v+=Math.abs((u.trust_a??0)-(u.trust_b??0));return v/m.length}var Pt=Ye('',1),Lt=Ye(' '),$t=Ye('',1),Bt=S('
                    '),Yt=S('
                    '),zt=S('
                    '),Vt=S('
                    topic:
                    '),Ht=S('
                    SEVERITYstrong (>0.7)moderate (0.5-0.7)mild (0.3-0.5)
                    ');function Ut(m,v){st(v,!0);let u=$e(v,"focusedPairIndex",3,null),D=$e(v,"width",3,800),O=$e(v,"height",3,600);const R=M(()=>{const d=D()/2,r=O()/2,g=Math.min(D(),O())*.38;return{cx:d,cy:r,R:g}}),fe=M(()=>{const d=[],r=[],g=v.contradictions.length||1;return v.contradictions.forEach((l,p)=>{const j=p/g*Math.PI*2-Math.PI/2,y=.18+l.similarity*.22,f=j-y,_=j+y,G=e(R).R+Math.sin(p*2.3)*18,L=e(R).R+Math.cos(p*1.7)*18,b={x:e(R).cx+Math.cos(f)*G,y:e(R).cy+Math.sin(f)*G,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:p,side:"a"},h={x:e(R).cx+Math.cos(_)*L,y:e(R).cy+Math.sin(_)*L,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:p,side:"b"};d.push(b,h);const $=(b.x+h.x)/2,I=(b.y+h.y)/2,A=.55-l.similarity*.25,z=$+(e(R).cx-$)*A,V=I+(e(R).cy-I)*A,ve=1+Math.min(l.trust_a,l.trust_b)*4;r.push({pairIndex:p,path:`M ${b.x.toFixed(1)} ${b.y.toFixed(1)} Q ${z.toFixed(1)} ${V.toFixed(1)} ${h.x.toFixed(1)} ${h.y.toFixed(1)}`,color:Be(l.similarity),thickness:ve,severity:mt(l.similarity),topic:l.topic,similarity:l.similarity,dateDiff:l.date_diff_days,aPoint:b,bPoint:h,midX:z,midY:V})}),{nodes:d,arcs:r}});let T=te(null),P=te(null),Y=te(0),N=te(0);function we(d){const r=d.currentTarget.getBoundingClientRect();E(Y,d.clientX-r.left),E(N,d.clientY-r.top)}function re(d){v.onSelectPair&&v.onSelectPair(u()===d?null:d)}function je(){var d;(d=v.onSelectPair)==null||d.call(v,null)}var ae=Ht(),C=s(ae),de=i(s(C)),H=i(de),se=i(H),ie=i(se);ue(ie,17,()=>e(fe).arcs,d=>d.pairIndex,(d,r)=>{const g=M(()=>at(e(r).pairIndex,u())),l=M(()=>u()===e(r).pairIndex);var p=Pt(),j=Ke(p),y=i(j),f=i(y);F(_=>{n(j,"d",e(r).path),n(j,"stroke",e(r).color),n(j,"stroke-width",e(r).thickness*3),n(j,"stroke-opacity",.08*e(g)),n(y,"d",e(r).path),n(y,"stroke",e(r).color),n(y,"stroke-width",e(r).thickness*(e(l)?1.6:1)),n(y,"stroke-opacity",(e(l)?1:.72)*e(g)),n(y,"aria-label",`contradiction ${e(r).pairIndex+1}: ${e(r).topic??""}`),n(f,"d",e(r).path),n(f,"stroke",e(r).color),n(f,"stroke-width",_),n(f,"stroke-opacity",.85*e(g)),q(f,`animation-duration: ${4+e(r).pairIndex%5}s`)},[()=>Math.max(1,e(r).thickness*.6)]),W("click",y,_=>{_.stopPropagation(),re(e(r).pairIndex)}),ye("mouseenter",y,()=>E(P,e(r),!0)),ye("mouseleave",y,()=>E(P,null)),W("keydown",y,_=>{_.key==="Enter"&&re(e(r).pairIndex)}),w(d,p)});var oe=i(ie);ue(oe,19,()=>e(fe).nodes,(d,r)=>d.memoryId+"-"+d.side+"-"+r,(d,r)=>{const g=M(()=>at(e(r).pairIndex,u())),l=M(()=>u()===e(r).pairIndex),p=M(()=>Ct(e(r).trust)),j=M(()=>tt(e(r).type));var y=$t(),f=Ke(y),_=i(f),G=i(_);{var L=b=>{var h=Lt(),$=s(h,!0);a(h),F(I=>{n(h,"x",e(r).x),n(h,"y",e(r).y-e(p)-8),k($,I)},[()=>he(e(r).preview,40)]),w(b,h)};B(G,b=>{e(l)&&b(L)})}F(b=>{n(f,"cx",e(r).x),n(f,"cy",e(r).y),n(f,"r",e(p)*2.2),n(f,"fill",e(j)),n(f,"opacity",.12*e(g)),n(_,"cx",e(r).x),n(_,"cy",e(r).y),n(_,"r",e(p)),n(_,"fill",e(j)),n(_,"opacity",e(g)),n(_,"stroke-opacity",e(l)?.85:.25),n(_,"stroke-width",e(l)?2:1),n(_,"aria-label",`memory ${b??""}`)},[()=>he(e(r).preview,40)]),ye("mouseenter",_,()=>E(T,e(r),!0)),ye("mouseleave",_,()=>E(T,null)),W("click",_,b=>{b.stopPropagation(),re(e(r).pairIndex)}),W("keydown",_,b=>{b.key==="Enter"&&re(e(r).pairIndex)}),w(d,y)}),ee(),a(C);var _e=i(C,2);{var Me=d=>{var r=zt(),g=s(r),l=s(g),p=i(l,2),j=s(p,!0);a(p);var y=i(p,2),f=s(y);a(y),a(g);var _=i(g,2),G=s(_,!0);a(_);var L=i(_,2);{var b=I=>{var A=Bt(),z=s(A);a(A),F(()=>k(z,`created ${e(T).created??""}`)),w(I,A)};B(L,I=>{e(T).created&&I(b)})}var h=i(L,2);{var $=I=>{var A=Yt(),z=s(A,!0);a(A),F(V=>k(z,V),[()=>e(T).tags.slice(0,4).join(" · ")]),w(I,A)};B(h,I=>{e(T).tags&&e(T).tags.length>0&&I($)})}a(r),F((I,A,z,V)=>{q(r,`left: ${I??""}px; top: ${A??""}px;`),q(l,`background: ${z??""}`),k(j,e(T).type??"memory"),k(f,`trust ${V??""}%`),k(G,e(T).preview)},[()=>Math.max(0,Math.min(e(Y)+12,D()-240)),()=>Math.max(0,Math.min(e(N)-8,O()-120)),()=>tt(e(T).type),()=>(e(T).trust*100).toFixed(0)]),w(d,r)},be=d=>{var r=Vt(),g=s(r),l=s(g),p=i(l,2),j=s(p);a(p),a(g);var y=i(g,2),f=i(s(y)),_=s(f,!0);a(f),a(y);var G=i(y,2),L=s(G);a(G),a(r),F((b,h,$)=>{q(r,`left: ${b??""}px; top: ${h??""}px;`),q(l,`background: ${e(P).color??""}`),k(j,`${e(P).severity??""} conflict`),k(_,e(P).topic),k(L,`similarity ${$??""}% · ${e(P).dateDiff??""}d apart`)},[()=>Math.max(0,Math.min(e(Y)+12,D()-240)),()=>Math.max(0,Math.min(e(N)-8,O()-120)),()=>(e(P).similarity*100).toFixed(0)]),w(d,r)};B(_e,d=>{e(T)?d(Me):e(P)&&d(be,1)})}a(ae),F(()=>{q(ae,`aspect-ratio: ${D()??""} / ${O()??""};`),n(C,"width",D()),n(C,"height",O()),n(C,"viewBox",`0 0 ${D()??""} ${O()??""}`),n(de,"width",D()),n(de,"height",O()),n(H,"cx",e(R).cx),n(H,"cy",e(R).cy),n(H,"r",e(R).R),n(se,"cx",e(R).cx),n(se,"cy",e(R).cy)}),W("mousemove",C,we),ye("mouseleave",C,()=>{E(T,null),E(P,null)}),W("click",C,je),w(m,ae),it()}ot(["mousemove","click","keydown"]);var Wt=S(' in view'),qt=S(''),Xt=S('

                    No contradictions match this filter.

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

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

                    Nothing's been dreamt yet.

                    Run a consolidation cycle and watch your episodic memories replay, - cross-reference, and crystallize into lasting semantic knowledge.

                    `),Dt=x(''),Nt=x('
                    '),It=x('
                    Replayed
                    Connections Found
                    Connections Persisted
                    Insights
                    Duration
                    '),Et=x('
                    ',1),Tt=x('
                    ');function Ut(o,v){Se(v,!0);let P=he(null),c=he(1),p=he(!1),q=he(null),Z=_(()=>e(P)!==null),$=_(()=>{const f=e(P);return f?[...f.insights].sort((g,y)=>(y.noveltyScore??0)-(g.noveltyScore??0)):[]});async function ee(){if(!e(p)){le(p,!0),le(q,null);try{const f=await We.dream();le(P,f,!0),le(c,1)}catch(f){le(q,f instanceof Error?f.message:"Dream failed",!0)}finally{le(p,!1)}}}function I(f){le(c,He(f),!0)}function te(f){const g=Number(f.currentTarget.value);I(g)}var G=Tt();Be("1fv2vo0",f=>{ze(()=>{Ge.title="Dream Cinema · Vestige"})});var L=r(G);Je(L,{icon:"dreams",title:"Dream Cinema",subtitle:"Scrub through Vestige's 5-stage consolidation cycle. Replay, cross-reference, strengthen, prune, transfer. Watch episodic become semantic.",accent:"dream",children:(f,g)=>{var y=Mt(),R=r(y);{var k=h=>{var M=kt();m(h,M)},H=h=>{var M=wt();m(h,M)};F(R,h=>{e(p)?h(k):e(Z)&&h(H,1)})}var S=l(R,2);let E;var T=r(S);{var X=h=>{var M=qt();U(2),m(h,M)},C=h=>{var M=St(),A=ge(M),w=r(A);ke(w,{name:"sparkle",size:16}),a(A),U(2),m(h,M)};F(T,h=>{e(p)?h(X):h(C,!1)})}a(S),ye(S,h=>{var M;return(M=Te)==null?void 0:M(h)}),a(y),N(()=>{S.disabled=e(p),E=we(S,1,"dream-button svelte-1fv2vo0",null,E,{"is-dreaming":e(p)})}),fe("click",S,ee),m(f,y)},$$slots:{default:!0}});var J=l(L,2);{var ne=f=>{var g=Rt(),y=r(g);ke(y,{name:"contradictions",size:16});var R=l(y,2),k=r(R,!0);a(R),a(g),ye(g,H=>{var S;return(S=Ee)==null?void 0:S(H)}),N(()=>b(k,e(q))),m(f,g)};F(J,f=>{e(q)&&f(ne)})}var se=l(J,2);{var ve=f=>{var g=Ct(),y=r(g),R=r(y);ke(R,{name:"dreams",size:52,draw:!0}),a(y);var k=l(y,6),H=r(k),S=r(H);ke(S,{name:"sparkle",size:16}),a(H),U(2),a(k),ye(k,E=>{var T;return(T=Te)==null?void 0:T(E)}),a(g),ye(g,E=>{var T;return(T=Ee)==null?void 0:T(E)}),fe("click",k,ee),m(f,g)},ie=f=>{var g=Et(),y=ge(g),R=r(y),k=r(R),H=r(k);a(k);var S=l(k,2),E=r(S),T=l(E,2);a(S),a(R);var X=l(R,2),C=r(X);Ye(C);var h=l(C,2);xe(h,22,()=>je,t=>t,(t,s,i)=>{var n=Dt();let u;var d=l(r(n),2),D=r(d);a(d),a(n),N(()=>{u=we(n,1,"tick svelte-1fv2vo0",null,u,{active:e(c)===e(i)+1,passed:e(c)>e(i)+1}),n.disabled=e(p),b(D,`${e(i)+1}. ${s??""}`)}),fe("click",n,()=>I(e(i)+1)),m(t,n)}),a(h),a(X),a(y);var M=l(y,2),A=r(M);pt(A,{get stage(){return e(c)},get dreamResult(){return e(P)}});var w=l(A,2),O=r(w),ae=l(r(O),2),oe=r(ae);a(ae),a(O);var ce=l(O,2),de=r(ce);{var Y=t=>{var s=Nt(),i=r(s);{var n=d=>{var D=Ie("Dreaming...");m(d,D)},u=d=>{var D=Ie("No insights generated this cycle.");m(d,D)};F(i,d=>{e(p)?d(n):d(u,!1)})}a(s),m(t,s)},V=t=>{var s=Fe(),i=ge(s);xe(i,19,()=>e($),(n,u)=>{var d;return u+"-"+(((d=n.insight)==null?void 0:d.slice(0,32))??"")},(n,u,d)=>{yt(n,{get insight(){return e(u)},get index(){return e(d)}})}),m(t,s)};F(de,t=>{e($).length===0?t(Y):t(V,!1)})}a(ce),a(w),a(M);var B=l(M,2);{var me=t=>{var s=It(),i=r(s),n=r(i),u=r(n,!0);a(n),U(2),a(i);var d=l(i,2),D=r(d),K=r(D,!0);a(D),U(2),a(d);var j=l(d,2),Q=r(j),ue=r(Q,!0);a(Q),U(2),a(j);var pe=l(j,2),be=r(pe),z=r(be,!0);a(be),U(2),a(pe);var Re=l(pe,2),Ce=r(Re),Oe=r(Ce,!0);a(Ce),U(2),a(Re),a(s),N(_e=>{var De,Ne;b(u,e(P).memoriesReplayed??0),b(K,((De=e(P).stats)==null?void 0:De.newConnectionsFound)??0),b(ue,e(P).connectionsPersisted??0),b(z,((Ne=e(P).stats)==null?void 0:Ne.insightsGenerated)??0),b(Oe,_e)},[()=>{var _e;return $e((_e=e(P).stats)==null?void 0:_e.durationMs)}]),m(t,s)};F(B,t=>{e(P)&&t(me)})}N(()=>{b(H,`Stage ${e(c)??""} · ${je[e(c)-1]??""}`),E.disabled=e(c)<=1||e(p),T.disabled=e(c)>=5||e(p),Ve(C,e(c)),C.disabled=e(p),b(oe,`${e($).length??""} total · by novelty`)}),fe("click",E,()=>I(e(c)-1)),fe("click",T,()=>I(e(c)+1)),fe("input",C,te),m(f,g)};F(se,f=>{!e(Z)&&!e(p)?f(ve):f(ie,!1)})}a(G),m(o,G),Me()}Ae(["click","input"]);export{Ut as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/7.yWYTsQ1Q.js.br b/apps/dashboard/build/_app/immutable/nodes/7.yWYTsQ1Q.js.br deleted file mode 100644 index e98fae1..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/7.yWYTsQ1Q.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/7.yWYTsQ1Q.js.gz b/apps/dashboard/build/_app/immutable/nodes/7.yWYTsQ1Q.js.gz deleted file mode 100644 index 328c4e4..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/7.yWYTsQ1Q.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/8.CDAVQcae.js b/apps/dashboard/build/_app/immutable/nodes/8.CDAVQcae.js new file mode 100644 index 0000000..8f98651 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/8.CDAVQcae.js @@ -0,0 +1,6 @@ +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/CvjSAYrz.js";import{d as Be,a as q,s as o}from"../chunks/FzvEaXMa.js";import{a as c,f as m,c as De}from"../chunks/BsvCUYx-.js";import{i as k}from"../chunks/ciN1mm2W.js";import{e as ie,i as ne}from"../chunks/DTnG8poT.js";import{r as ye}from"../chunks/CNfQDikv.js";import{s as oe}from"../chunks/DPl3NjBv.js";import{s as Ke}from"../chunks/Bhad70Ss.js";import{b as de}from"../chunks/CVpUe0w3.js";import{a as X}from"../chunks/DNjM5a-l.js";var Re=m(''),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 + ${e($)===i?"glass !border-synapse/30 text-synapse-glow":"glass-subtle text-dim hover:bg-white/[0.03]"}`),o(h,le[i].icon),o(p,w),o(g,le[i].desc)},[()=>i.charAt(0).toUpperCase()+i.slice(1)]),q("click",d,()=>Se(i)),c(s,d)}),t(ee);var te=a(ee,2),ce=a(r(te),2),J=r(ce);ye(J);var Fe=a(J,2);t(ce),t(te);var xe=a(te,2);{var Te=s=>{var i=Ue(),d=a(r(i),2),_=r(d,!0);t(d);var h=a(d,2),f=r(h),p=r(f,!0);t(f);var n=a(f,2),g=r(n);t(n),t(h),t(i),y((w,z)=>{o(_,w),o(p,e(F).nodeType),o(g,`${z??""}% retention`)},[()=>e(F).content.slice(0,200),()=>(e(F).retentionStrength*100).toFixed(0)]),c(s,i)};k(xe,s=>{e(F)&&s(Te)})}var me=a(xe,2);{var $e=s=>{var i=Ge(),d=ge(i),_=r(d),h=a(r(_)),f=r(h);t(h),t(_);var p=a(_,2),n=r(p);ye(n);var g=a(n,2);t(p),t(d);var w=a(d,2);{var z=u=>{var E=Ve(),x=a(r(E),2),K=r(x,!0);t(x);var M=a(x,2),S=r(M),A=r(S,!0);t(S);var T=a(S,2),P=r(T);t(T),t(M),t(E),y((Q,j)=>{o(K,Q),o(A,e(C).nodeType),o(P,`${j??""}% retention`)},[()=>e(C).content.slice(0,200),()=>(e(C).retentionStrength*100).toFixed(0)]),c(u,E)};k(w,u=>{e(C)&&u(z)})}y(()=>o(f,`(for ${e($)??""})`)),q("keydown",n,u=>u.key==="Enter"&&pe()),de(n,()=>e(G),u=>b(G,u)),q("click",g,pe),c(s,i)};k(me,s=>{(e($)==="chains"||e($)==="bridges")&&s($e)})}var ue=a(me,2);{var Ee=s=>{var i=De(),d=ge(i);{var _=p=>{var n=He(),g=a(r(n),2),w=r(g);t(g),t(n),y(()=>o(w,`Exploring ${e($)??""}...`)),c(p,n)},h=p=>{var n=et(),g=r(n),w=r(g),z=r(w);t(w),t(g);var u=a(g,2);ie(u,21,()=>e(B),ne,(E,x,K)=>{var M=Ze(),S=r(M);S.textContent=K+1;var A=a(S,2),T=r(A),P=r(T,!0);t(T);var Q=a(T,2),j=r(Q);{var L=l=>{var v=Je(),N=r(v,!0);t(v),y(()=>o(N,e(x).nodeType)),c(l,v)};k(j,l=>{e(x).nodeType&&l(L)})}var R=a(j,2);{var ae=l=>{var v=Le(),N=r(v);t(v),y(U=>o(N,`Score: ${U??""}`),[()=>Number(e(x).score).toFixed(3)]),c(l,v)};k(R,l=>{e(x).score&&l(ae)})}var W=a(R,2);{var Ie=l=>{var v=We(),N=r(v);t(v),y(U=>o(N,`Similarity: ${U??""}`),[()=>Number(e(x).similarity).toFixed(3)]),c(l,v)};k(W,l=>{e(x).similarity&&l(Ie)})}var be=a(W,2);{var je=l=>{var v=Xe(),N=r(v);t(v),y(U=>o(N,`${U??""}% retention`),[()=>(Number(e(x).retention)*100).toFixed(0)]),c(l,v)};k(be,l=>{e(x).retention&&l(je)})}var Ce=a(be,2);{var Oe=l=>{var v=Ye(),N=r(v,!0);t(v),y(()=>o(N,e(x).connectionType)),c(l,v)};k(Ce,l=>{e(x).connectionType&&l(Oe)})}t(Q),t(A),t(M),y(()=>o(P,e(x).content)),c(E,M)}),t(u),t(n),y(()=>o(z,`${e(B).length??""} Connections Found`)),c(p,n)},f=p=>{var n=tt();c(p,n)};k(d,p=>{e(O)?p(_):e(B).length>0?p(h,1):p(f,!1)})}c(s,i)};k(ue,s=>{e(F)&&s(Ee)})}var _e=a(ue,2),re=a(r(_e),4);Qe(re);var fe=a(re,2),Me=a(fe,2);{var Ne=s=>{const i=se(()=>e(D).channels),d=se(()=>Number(e(D).composite||e(D).compositeScore||0));var _=st(),h=r(_),f=r(h),p=r(f,!0);t(f);var n=a(f,2),g=r(n,!0);t(n),t(h);var w=a(h,2);{var z=u=>{var E=at();ie(E,21,()=>Object.entries(e(i)),ne,(x,K)=>{var M=se(()=>qe(e(K),2));let S=()=>e(M)[0],A=()=>e(M)[1];var T=rt(),P=r(T),Q=r(P,!0);t(P);var j=a(P,2),L=r(j);t(j);var R=a(j,2),ae=r(R,!0);t(R),t(T),y(W=>{o(Q,S()),oe(L,1,`h-full rounded-full transition-all duration-500 + ${S()==="novelty"?"bg-synapse":S()==="arousal"?"bg-dream":S()==="reward"?"bg-recall":"bg-amber-400"}`),Ke(L,`width: ${A()*100}%`),o(ae,W)},[()=>A().toFixed(2)]),c(x,T)}),t(E),c(u,E)};k(w,u=>{e(i)&&u(z)})}t(_),y(u=>{o(p,u),oe(n,1,`px-2 py-1 rounded-lg text-xs ${e(d)>.6?"bg-recall/20 text-recall border border-recall/30":"bg-white/[0.04] text-dim border border-subtle/20"}`),o(g,e(d)>.6?"SAVE":"SKIP")},[()=>e(d).toFixed(2)]),c(s,_)};k(Me,s=>{e(D)&&s(Ne)})}t(_e),t(Z),q("keydown",J,s=>s.key==="Enter"&&ve()),de(J,()=>e(V),s=>b(V,s)),q("click",Fe,ve),de(re,()=>e(H),s=>b(H,s)),q("click",fe,ke),c(he,Z),Pe()}Be(["click","keydown"]);export{ft as component}; 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..76e47a3 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.D5dP0-E2.js b/apps/dashboard/build/_app/immutable/nodes/8.D5dP0-E2.js deleted file mode 100644 index 1d60119..0000000 --- a/apps/dashboard/build/_app/immutable/nodes/8.D5dP0-E2.js +++ /dev/null @@ -1,3 +0,0 @@ -import"../chunks/Bzak7iHL.js";import{o as Re,a as Fe}from"../chunks/TZu9D97Z.js";import{p as Ae,c as Ne,f as De,g as e,a as u,b as Me,u as O,e as s,r,h as o,t as M,i as A,j as m,s as V,d as ye,n as xe}from"../chunks/wpu9U-D0.js";import{d as Ce,s as f,a as X}from"../chunks/D8mhvFt8.js";import{i as Y}from"../chunks/DKve45Wd.js";import{e as ce,i as Se,s as he,a as le,r as Pe}from"../chunks/60_R_Vbt.js";import{a as _e,r as Ze}from"../chunks/P1-U_Xsj.js";import{b as Ee}from"../chunks/CnZzd20v.js";import{s as de}from"../chunks/EqHb-9AZ.js";import{N as Ie}from"../chunks/CcUbQ_Wl.js";import{P as Be}from"../chunks/BHDZZvku.js";import{I as je}from"../chunks/D7A-gG4Z.js";import{A as we}from"../chunks/DcKTNC6e.js";import{s as He}from"../chunks/DPdYG9yN.js";function Le(n){return n>=.92?"near-identical":n>=.8?"strong":"weak"}function be(n){const a=Le(n);return a==="near-identical"?"var(--color-decay)":a==="strong"?"var(--color-warning)":"#fde047"}function Oe(n){const a=Le(n);return a==="near-identical"?"Near-identical":a==="strong"?"Strong match":"Weak match"}function ze(n){return n>.7?"#10b981":n>.4?"#f59e0b":"#ef4444"}function Ue(n){if(!n||n.length===0)return null;let a=n[0],d=Number.isFinite(a.retention)?a.retention:-1/0;for(let v=1;vd&&(a=w,d=k)}return a}function We(n,a){return n.filter(d=>d.similarity>=a)}function ke(n){return n.map(a=>a.id).slice().sort().join("|")}function Ke(n,a=80){if(!n)return"";const d=n.trim().replace(/\s+/g," ");return d.length<=a?d:d.slice(0,a)+"…"}function Te(n){if(!n||typeof n!="string")return"";const a=new Date(n);return Number.isNaN(a.getTime())?"":a.toLocaleDateString(void 0,{year:"numeric",month:"short",day:"numeric"})}function Ge(n,a=4){return Array.isArray(n)?n.slice(0,a):[]}var Ve=m('WINNER'),Xe=m(' '),Ye=m('
                    '),qe=m('

                    '),Je=m('
                    ');function Qe(n,a){Ae(a,!0);let d=V(!1);const v=O(()=>Ue(a.memories)),w=O(()=>e(v)?a.memories.filter(y=>y.id!==e(v).id).map(y=>y.id):[]);function k(){a.onMerge&&e(v)&&a.onMerge(e(v).id,e(w))}var C=Ne(),q=De(C);{var ue=y=>{var J=Je(),z=s(J),Q=s(z),_=s(Q),P=s(_),ie=s(P);r(P);var U=o(P,2),ve=s(U,!0);r(U);var W=o(U,2),se=s(W);r(W),r(_);var Z=o(_,2),$=s(Z);r(Z),r(Q);var S=o(Q,2),ne=s(S);r(S),r(z);var ee=o(z,2);ce(ee,21,()=>a.memories,T=>T.id,(T,c)=>{var L=qe(),I=s(L),B=o(I,2),j=s(B),t=s(j),i=s(t,!0);r(t);var l=o(t,2);{var p=h=>{var D=Ve();u(h,D)};Y(l,h=>{e(c).id===e(v).id&&h(p)})}var b=o(l,2);ce(b,17,()=>Ge(e(c).tags,4),Se,(h,D)=>{var H=Xe(),fe=s(H,!0);r(H),M(()=>f(fe,e(D))),u(h,H)}),r(j);var g=o(j,2),x=s(g,!0);r(g);var ae=o(g,2);{var re=h=>{var D=Ye(),H=s(D,!0);r(D),M(fe=>f(H,fe),[()=>Te(e(c).createdAt)]),u(h,D)},oe=O(()=>Te(e(c).createdAt));Y(ae,h=>{e(oe)&&h(re)})}r(B);var R=o(B,2),G=s(R),ge=s(G);r(G);var F=o(G,2),N=s(F);r(F),r(R),r(L),M((h,D,H)=>{he(L,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(v).id?"ring-1 ring-recall/30":""}`),de(I,`background: ${(Ie[e(c).nodeType]||"#8B95A5")??""}`),le(I,"title",e(c).nodeType),f(i,e(c).nodeType),he(g,1,`text-sm text-text leading-relaxed ${e(d)?"whitespace-pre-wrap":""}`),f(x,h),de(ge,`width: ${e(c).retention*100}%; background: ${D??""}`),f(N,`${H??""}%`)},[()=>e(d)?e(c).content:Ke(e(c).content),()=>ze(e(c).retention),()=>(e(c).retention*100).toFixed(0)]),u(T,L)}),r(ee);var K=o(ee,2),te=s(K),E=o(te,2),me=s(E,!0);r(E);var pe=o(E,2);r(K),r(J),M((T,c,L,I,B,j,t,i)=>{de(P,`color: ${T??""}`),f(ie,`${c??""}%`),f(ve,L),f(se,`· ${a.memories.length??""} memories`),le(Z,"aria-valuenow",I),de($,`width: ${B??""}%; background: ${j??""}; box-shadow: 0 0 12px ${t??""}66`),he(S,1,`flex-shrink-0 rounded-full border px-3 py-1 text-xs font-medium ${a.suggestedAction==="merge"?"border-recall/40 bg-recall/10 text-recall":"border-dream-glow/40 bg-dream/10 text-dream-glow"}`),f(ne,`Suggested: ${a.suggestedAction==="merge"?"Merge":"Review"}`),le(te,"title",`Merge all into highest-retention memory (${i??""}%)`),le(E,"aria-expanded",e(d)),f(me,e(d)?"Collapse":"Review")},[()=>be(a.similarity),()=>(a.similarity*100).toFixed(1),()=>Oe(a.similarity),()=>Math.round(a.similarity*100),()=>(a.similarity*100).toFixed(1),()=>be(a.similarity),()=>be(a.similarity),()=>(e(v).retention*100).toFixed(0)]),X("click",te,k),X("click",E,()=>A(d,!e(d))),X("click",pe,function(...T){var c;(c=a.onDismiss)==null||c.apply(this,T)}),u(y,J)};Y(q,y=>{a.memories.length>0&&e(v)&&y(ue)})}u(n,C),Me()}Ce(["click"]);var $e=m(' Live',1),et=m(' Detecting…',1),tt=m(' Error',1),at=m(' ',1),rt=m(`
                    Couldn't detect duplicates
                    `),it=m('
                    '),st=m('
                    '),nt=m('
                    No duplicates found — your memory is clean.
                    '),ot=m('
                    '),lt=m('
                    '),dt=m('
                    '),ct=m('
                    ');function At(n,a){Ae(a,!0);let d=V(.8),v=V(ye([])),w=V(ye(new Set)),k=V(!0),C=V(null),q;async function ue(t){return await new Promise(l=>setTimeout(l,450)),{clusters:We([{similarity:.96,suggestedAction:"merge",memories:[{id:"m-001",content:"BUG FIX: Harmony parser dropped `final` channel tokens when tool call followed. Root cause: 5-layer fallback missed the final channel marker when channel switched mid-stream. Solution: added final-channel detector before tool-call pop. Files: src/parser/harmony.rs",nodeType:"fact",tags:["bug-fix","benchmark-suite","parser"],retention:.91,createdAt:"2026-04-12T14:22:00Z"},{id:"m-002",content:"Fixed Harmony parser final-channel bug — 5-layer fallback was missing the final channel marker when a tool call followed. Added detector before tool pop.",nodeType:"fact",tags:["bug-fix","benchmark-suite"],retention:.64,createdAt:"2026-04-13T09:15:00Z"},{id:"m-003",content:"Harmony parser: final channel dropped on tool-call. Patched the fallback stack.",nodeType:"note",tags:["parser"],retention:.38,createdAt:"2026-04-14T11:02:00Z"}]},{similarity:.88,suggestedAction:"merge",memories:[{id:"m-004",content:"DECISION: Use vLLM prefix caching at 0.35 gpu_memory_utilization for benchmark suite submissions. Alternatives considered: sglang (slower cold start), TensorRT-LLM (deployment friction).",nodeType:"decision",tags:["vllm","benchmark-suite","inference"],retention:.84,createdAt:"2026-04-05T18:44:00Z"},{id:"m-005",content:"Chose vLLM with prefix caching (0.35 mem util) over sglang and TensorRT-LLM for benchmark suite inference.",nodeType:"decision",tags:["vllm","benchmark-suite"],retention:.72,createdAt:"2026-04-06T10:30:00Z"}]},{similarity:.83,suggestedAction:"review",memories:[{id:"m-006",content:"Release process prefers one change per benchmark submission — stacking changes destroyed signal in a prior run.",nodeType:"pattern",tags:["methodology","benchmark-suite"],retention:.88,createdAt:"2026-04-04T22:10:00Z"},{id:"m-007",content:"One-variable-at-a-time rule: never stack multiple changes per submission. Paper 2603.27844 proves +/-2 points is noise.",nodeType:"pattern",tags:["kaggle","methodology"],retention:.67,createdAt:"2026-04-08T16:20:00Z"},{id:"m-008",content:"Lesson: stacking many changes in one benchmark run hid the causal signal. Always isolate variables.",nodeType:"note",tags:["methodology"],retention:.42,createdAt:"2026-04-15T08:55:00Z"}]},{similarity:.78,suggestedAction:"review",memories:[{id:"m-009",content:"Dimensional Illusion performance: 7-minute flow poi set, LED config Parthenos overcook preset, tempo 128 BPM.",nodeType:"event",tags:["dimensional-illusion","poi","performance"],retention:.76,createdAt:"2026-03-28T19:45:00Z"},{id:"m-010",content:"Dimensional Illusion set: 7 min, Parthenos LED overcook, 128 BPM.",nodeType:"event",tags:["dimensional-illusion","poi"],retention:.51,createdAt:"2026-04-02T12:12:00Z"}]},{similarity:.76,suggestedAction:"review",memories:[{id:"m-011",content:"Vestige v2.0.7 shipped active forgetting via Anderson 2025 top-down inhibition + Davis Rac1 cascade. Suppress compounds, reversible 24h.",nodeType:"fact",tags:["vestige","release","active-forgetting"],retention:.93,createdAt:"2026-04-17T03:22:00Z"},{id:"m-012",content:"Active Forgetting feature: compounds on each suppress, 24h reversible labile window, violet implosion animation in graph view.",nodeType:"concept",tags:["vestige","active-forgetting"],retention:.81,createdAt:"2026-04-18T09:07:00Z"}]}],t)}}async function y(){A(k,!0),A(C,null);try{const t=await ue(e(d));A(v,t.clusters,!0);const i=new Set(e(v).map(p=>ke(p.memories))),l=new Set;for(const p of e(w))i.has(p)&&l.add(p);A(w,l,!0)}catch(t){A(C,t instanceof Error?t.message:"Failed to detect duplicates",!0),A(v,[],!0)}finally{A(k,!1)}}function J(){clearTimeout(q),q=setTimeout(y,250)}function z(t){const i=new Set(e(w));i.add(t),A(w,i,!0)}function Q(t,i,l){console.log("Merge cluster",t,{winnerId:i,loserIds:l}),z(t)}const _=O(()=>e(v).map(t=>({c:t,key:ke(t.memories)})).filter(({key:t})=>!e(w).has(t))),P=O(()=>e(_).reduce((t,{c:i})=>t+i.memories.length,0)),ie=50,U=O(()=>e(_).length>ie),ve=O(()=>e(U)?e(_).slice(0,ie):e(_));Re(()=>y()),Fe(()=>clearTimeout(q));var W=ct(),se=s(W);Be(se,{icon:"duplicates",title:"Memory Hygiene — Duplicate Detection",subtitle:"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.",accent:"synapse",children:(t,i)=>{var l=$e();xe(2),u(t,l)},$$slots:{default:!0}});var Z=o(se,2),$=s(Z),S=o(s($),2);Pe(S);var ne=o(S,2),ee=s(ne);r(ne),r($);var K=o($,2),te=s(K);{var E=t=>{var i=et();xe(2),u(t,i)},me=t=>{var i=tt();xe(2),u(t,i)},pe=t=>{var i=at(),l=o(De(i),2),p=s(l);we(p,{get value(){return e(_).length}});var b=o(p),g=o(b);we(g,{get value(){return e(P)}});var x=o(g);r(l),M(()=>{f(b,` ${e(_).length===1?"cluster":"clusters"}, `),f(x,` potential duplicate${e(P)===1?"":"s"}`)}),u(t,i)};Y(te,t=>{e(k)?t(E):e(C)?t(me,1):t(pe,!1)})}r(K);var T=o(K,2);r(Z);var c=o(Z,2);{var L=t=>{var i=rt(),l=o(s(i),2),p=s(l,!0);r(l);var b=o(l,2);r(i),M(()=>f(p,e(C))),X("click",b,y),u(t,i)},I=t=>{var i=st();ce(i,20,()=>Array(3),Se,(l,p)=>{var b=it();u(l,b)}),r(i),u(t,i)},B=t=>{var i=nt(),l=s(i),p=s(l);je(p,{name:"sparkle",size:26,draw:!0}),r(l);var b=o(l,4),g=s(b);r(b),r(i),M(x=>f(g,`Nothing clusters above ${x??""}% similarity. Lower the threshold to - surface looser matches.`),[()=>(e(d)*100).toFixed(0)]),u(t,i)},j=t=>{var i=dt(),l=s(i);{var p=g=>{var x=ot(),ae=s(x);r(x),M(()=>f(ae,`Showing first 50 of ${e(_).length??""} clusters. Raise the - threshold to narrow results.`)),u(g,x)};Y(l,g=>{e(U)&&g(p)})}var b=o(l,2);ce(b,19,()=>e(ve),({c:g,key:x})=>x,(g,x,ae)=>{let re=()=>e(x).c,oe=()=>e(x).key;var R=lt(),G=s(R),ge=s(G);Qe(ge,{get similarity(){return re().similarity},get memories(){return re().memories},get suggestedAction(){return re().suggestedAction},onDismiss:()=>z(oe()),onMerge:(F,N)=>Q(oe(),F,N)}),r(G),r(R),_e(R,(F,N)=>{var h;return(h=Ze)==null?void 0:h(F,N)},()=>({delay:Math.min(e(ae)*40,400),y:14})),_e(R,F=>{var N;return(N=He)==null?void 0:N(F)}),u(g,R)}),r(i),u(t,i)};Y(c,t=>{e(C)?t(L):e(k)?t(I,1):e(_).length===0?t(B,2):t(j,!1)})}r(W),M(t=>{f(ee,`${t??""}%`),T.disabled=e(k)},[()=>(e(d)*100).toFixed(0)]),X("input",S,J),Ee(S,()=>e(d),t=>A(d,t)),X("click",T,y),u(n,W),Me()}Ce(["input","click"]);export{At as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/8.D5dP0-E2.js.br b/apps/dashboard/build/_app/immutable/nodes/8.D5dP0-E2.js.br deleted file mode 100644 index 32163d9..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/8.D5dP0-E2.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/8.D5dP0-E2.js.gz b/apps/dashboard/build/_app/immutable/nodes/8.D5dP0-E2.js.gz deleted file mode 100644 index deb4cf4..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/8.D5dP0-E2.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/9.DVbfK-u1.js b/apps/dashboard/build/_app/immutable/nodes/9.DVbfK-u1.js new file mode 100644 index 0000000..d989ca5 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/9.DVbfK-u1.js @@ -0,0 +1,8 @@ +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(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..54d3c61 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/immutable/nodes/9.Vz-x3Q_x.js b/apps/dashboard/build/_app/immutable/nodes/9.Vz-x3Q_x.js deleted file mode 100644 index b927b0e..0000000 --- a/apps/dashboard/build/_app/immutable/nodes/9.Vz-x3Q_x.js +++ /dev/null @@ -1,6 +0,0 @@ -import"../chunks/Bzak7iHL.js";import{p as Ue,s as j,d as Ve,g as e,a as v,b as Ge,e as a,h as r,i as h,j as _,r as t,n as ke,k as Je,t as k,f as Se,c as Le,u as de,l as We,m as Te}from"../chunks/wpu9U-D0.js";import{d as Xe,a as B,s as p}from"../chunks/D8mhvFt8.js";import{i as $}from"../chunks/DKve45Wd.js";import{e as W,i as X,r as Fe,s as Y}from"../chunks/60_R_Vbt.js";import{a as $e,r as Ye}from"../chunks/P1-U_Xsj.js";import{s as le}from"../chunks/EqHb-9AZ.js";import{b as ve}from"../chunks/CnZzd20v.js";import{a as Z}from"../chunks/CZfHMhLI.js";import{P as Ze}from"../chunks/BHDZZvku.js";import{I as ee}from"../chunks/D7A-gG4Z.js";import{A as Ne}from"../chunks/DcKTNC6e.js";import{s as et}from"../chunks/DPdYG9yN.js";var tt=_(''),at=_('
                    Source

                    '),rt=_('
                    Target

                    '),st=_(`
                    Target Memory
                    `,1),it=_('
                    '),nt=_('
                    '),ot=_(' '),dt=_(' '),lt=_(' '),vt=_(' '),ct=_(' '),pt=_('

                    '),xt=_('

                    Connections Found

                    '),mt=_('

                    No connections surfaced yet

                    '),ut=_('
                    '),_t=_('
                    '),ft=_('
                    '),bt=_(`
                    Source Memory

                    Importance Scorer

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

                    `);function Et(ze,Ie){Ue(Ie,!0);let U=j(""),V=j(""),A=j(null),C=j(null),D=j(Ve([])),N=j("associations"),O=j(!1),G=j(""),H=j(null);const ce={associations:{icon:"activation",desc:"Spreading activation — find related memories via graph traversal"},chains:{icon:"reasoning",desc:"Build reasoning path from source to target memory"},bridges:{icon:"explore",desc:"Find connecting memories between two concepts"}};async function pe(){if(e(U).trim()){h(O,!0);try{const s=await Z.search(e(U),1);s.results.length>0&&(h(A,s.results[0],!0),await te())}catch{}finally{h(O,!1)}}}async function xe(){if(e(V).trim()){h(O,!0);try{const s=await Z.search(e(V),1);s.results.length>0&&(h(C,s.results[0],!0),e(A)&&await te())}catch{}finally{h(O,!1)}}}async function te(){if(e(A)){h(O,!0);try{const s=(e(N)==="chains"||e(N)==="bridges")&&e(C)?e(C).id:void 0,n=await Z.explore(e(A).id,e(N),s);h(D,n.results||n.nodes||n.chain||n.bridges||[],!0)}catch{h(D,[],!0)}finally{h(O,!1)}}}async function Me(){e(G).trim()&&h(H,await Z.importance(e(G)),!0)}function Ee(s){h(N,s,!0),e(A)&&te()}var ae=bt(),me=a(ae);Ze(me,{icon:"explore",title:"Explore Connections",subtitle:"Traverse the memory graph — spreading activation, reasoning chains, and conceptual bridges.",accent:"synapse"});var re=r(me,2);W(re,20,()=>["associations","chains","bridges"],X,(s,n)=>{var x=tt(),f=a(x),S=a(f);ee(S,{get name(){return ce[n].icon},size:22}),t(f);var y=r(f,2),c=a(y,!0);t(y);var o=r(y,2),m=a(o,!0);t(o),t(x),k(b=>{Y(x,1,`lift flex flex-col items-center gap-1 p-3 rounded-xl text-sm transition - ${e(N)===n?"glass !border-synapse/30 text-synapse-glow":"glass-subtle text-dim hover:bg-white/[0.03]"}`),Y(f,1,e(N)===n?"breathe":""),p(c,b),p(m,ce[n].desc)},[()=>n.charAt(0).toUpperCase()+n.slice(1)]),B("click",x,()=>Ee(n)),v(s,x)}),t(re);var se=r(re,2),ue=r(a(se),2),J=a(ue);Fe(J);var Ae=r(J,2);t(ue),t(se);var _e=r(se,2);{var Pe=s=>{var n=at(),x=r(a(n),2),f=a(x,!0);t(x);var S=r(x,2),y=a(S),c=a(y,!0);t(y);var o=r(y,2),m=a(o);t(o),t(S),t(n),k((b,w)=>{p(f,b),p(c,e(A).nodeType),p(m,`${w??""}% retention`)},[()=>e(A).content.slice(0,200),()=>(e(A).retentionStrength*100).toFixed(0)]),v(s,n)};$(_e,s=>{e(A)&&s(Pe)})}var fe=r(_e,2);{var je=s=>{var n=st(),x=Se(n),f=a(x),S=r(a(f)),y=a(S);t(S),t(f);var c=r(f,2),o=a(c);Fe(o);var m=r(o,2);t(c),t(x);var b=r(x,2);{var w=u=>{var z=rt(),i=r(a(z),2),T=a(i,!0);t(i);var g=r(i,2),F=a(g),E=a(F,!0);t(F);var I=r(F,2),P=a(I);t(I),t(g),t(z),k((Q,q)=>{p(T,Q),p(E,e(C).nodeType),p(P,`${q??""}% retention`)},[()=>e(C).content.slice(0,200),()=>(e(C).retentionStrength*100).toFixed(0)]),v(u,z)};$(b,u=>{e(C)&&u(w)})}k(()=>p(y,`(for ${e(N)??""})`)),B("keydown",o,u=>u.key==="Enter"&&xe()),ve(o,()=>e(V),u=>h(V,u)),B("click",m,xe),v(s,n)};$(fe,s=>{(e(N)==="chains"||e(N)==="bridges")&&s(je)})}var be=r(fe,2);{var Ce=s=>{var n=Le(),x=Se(n);{var f=c=>{var o=nt(),m=a(o),b=a(m);ee(b,{name:"activation",size:18,class:"breathe text-synapse-glow"});var w=r(b,2),u=a(w);t(w),t(m);var z=r(m,2);W(z,20,()=>Array(4),X,(i,T,g)=>{var F=it(),E=r(a(F),2),I=a(E);le(I,`width: ${88-g*9}%`);var P=r(I,2);le(P,`width: ${52-g*6}%`),t(E),t(F),v(i,F)}),t(z),t(o),k(()=>p(u,`Exploring ${e(N)??""}…`)),v(c,o)},S=c=>{var o=xt(),m=a(o),b=a(m),w=a(b);Ne(w,{get value(){return e(D).length},class:"text-aurora font-bold"}),ke(2),t(b),t(m);var u=r(m,2);W(u,21,()=>e(D),X,(z,i,T)=>{var g=pt(),F=a(g),E=a(F);E.textContent=T+1;var I=r(E,2),P=a(I),Q=a(P,!0);t(P);var q=r(P,2),K=a(q);{var oe=l=>{var d=ot(),M=a(d,!0);t(d),k(()=>p(M,e(i).nodeType)),v(l,d)};$(K,l=>{e(i).nodeType&&l(oe)})}var L=r(K,2);{var Be=l=>{var d=dt(),M=a(d);t(d),k(R=>p(M,`Score: ${R??""}`),[()=>Number(e(i).score).toFixed(3)]),v(l,d)};$(L,l=>{e(i).score&&l(Be)})}var ye=r(L,2);{var De=l=>{var d=lt(),M=a(d);t(d),k(R=>p(M,`Similarity: ${R??""}`),[()=>Number(e(i).similarity).toFixed(3)]),v(l,d)};$(ye,l=>{e(i).similarity&&l(De)})}var we=r(ye,2);{var He=l=>{var d=vt(),M=a(d);t(d),k(R=>p(M,`${R??""}% retention`),[()=>(Number(e(i).retention)*100).toFixed(0)]),v(l,d)};$(we,l=>{e(i).retention&&l(He)})}var Ke=r(we,2);{var Re=l=>{var d=ct(),M=a(d,!0);t(d),k(()=>p(M,e(i).connectionType)),v(l,d)};$(Ke,l=>{e(i).connectionType&&l(Re)})}t(q),t(I),t(F),t(g),$e(g,(l,d)=>{var M;return(M=Ye)==null?void 0:M(l,d)},()=>({delay:Math.min(T*35,350),y:12})),$e(g,l=>{var d;return(d=et)==null?void 0:d(l)}),k(()=>p(Q,e(i).content)),v(z,g)}),t(u),t(o),v(c,o)},y=c=>{var o=mt(),m=a(o);ee(m,{name:"explore",size:40,class:"breathe text-synapse-glow mx-auto mb-4 opacity-80"});var b=r(m,4),w=a(b);{var u=i=>{var T=Te("This memory hasn't formed strong links here. Try a broader source query — the graph rewards more general seeds.");v(i,T)},z=i=>{var T=Te();k(()=>p(T,`No ${e(N)??""} found between these two memories. Pick a different source or target and the path may light up.`)),v(i,T)};$(w,i=>{e(N)==="associations"?i(u):i(z,!1)})}t(b),t(o),v(c,o)};$(x,c=>{e(O)?c(f):e(D).length>0?c(S,1):c(y,!1)})}v(s,n)};$(be,s=>{e(A)&&s(Ce)})}var ge=r(be,2),ie=a(ge),Oe=a(ie);ee(Oe,{name:"importance",size:20,class:"text-recall"}),ke(),t(ie);var ne=r(ie,4);Je(ne);var he=r(ne,2),Qe=r(he,2);{var qe=s=>{const n=de(()=>e(H).channels),x=de(()=>Number(e(H).composite||e(H).compositeScore||0));var f=ft(),S=a(f),y=a(S);Ne(y,{get value(){return e(x)},decimals:2,class:"text-3xl text-aurora font-bold"});var c=r(y,2),o=a(c,!0);t(c),t(S);var m=r(S,2);{var b=w=>{var u=_t();W(u,21,()=>Object.entries(e(n)),X,(z,i)=>{var T=de(()=>We(e(i),2));let g=()=>e(T)[0],F=()=>e(T)[1];var E=ut(),I=a(E),P=a(I,!0);t(I);var Q=r(I,2),q=a(Q);t(Q);var K=r(Q,2),oe=a(K,!0);t(K),t(E),k(L=>{p(P,g()),Y(q,1,`h-full rounded-full transition-all duration-500 - ${g()==="novelty"?"bg-synapse":g()==="arousal"?"bg-dream":g()==="reward"?"bg-recall":"bg-amber-400"}`),le(q,`width: ${F()*100}%`),p(oe,L)},[()=>F().toFixed(2)]),v(z,E)}),t(u),v(w,u)};$(m,w=>{e(n)&&w(b)})}t(f),k(()=>{Y(c,1,`px-2 py-1 rounded-lg text-xs ${e(x)>.6?"bg-recall/20 text-recall border border-recall/30":"bg-white/[0.04] text-dim border border-subtle/20"}`),p(o,e(x)>.6?"SAVE":"SKIP")}),v(s,f)};$(Qe,s=>{e(H)&&s(qe)})}t(ge),t(ae),B("keydown",J,s=>s.key==="Enter"&&pe()),ve(J,()=>e(U),s=>h(U,s)),B("click",Ae,pe),ve(ne,()=>e(G),s=>h(G,s)),B("click",he,Me),v(ze,ae),Ge()}Xe(["click","keydown"]);export{Et as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/9.Vz-x3Q_x.js.br b/apps/dashboard/build/_app/immutable/nodes/9.Vz-x3Q_x.js.br deleted file mode 100644 index 6c8cc49..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/9.Vz-x3Q_x.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/9.Vz-x3Q_x.js.gz b/apps/dashboard/build/_app/immutable/nodes/9.Vz-x3Q_x.js.gz deleted file mode 100644 index b4a338b..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/9.Vz-x3Q_x.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/version.json b/apps/dashboard/build/_app/version.json index fdf4b42..a92bcb7 100644 --- a/apps/dashboard/build/_app/version.json +++ b/apps/dashboard/build/_app/version.json @@ -1 +1 @@ -{"version":"2.2.1"} \ No newline at end of file +{"version":"1777313640654"} \ 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 bf883c7..df334a5 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 b3c3a1a..e27a6be 100644 Binary files a/apps/dashboard/build/_app/version.json.gz and b/apps/dashboard/build/_app/version.json.gz differ diff --git a/apps/dashboard/build/favicon.svg.gz b/apps/dashboard/build/favicon.svg.gz index 61b0904..06ecd9b 100644 Binary files a/apps/dashboard/build/favicon.svg.gz and b/apps/dashboard/build/favicon.svg.gz differ diff --git a/apps/dashboard/build/index.html b/apps/dashboard/build/index.html index bc8e358..abd1a74 100644 --- a/apps/dashboard/build/index.html +++ b/apps/dashboard/build/index.html @@ -11,19 +11,21 @@ - - - - - - - - + + + + + + + + + - - - - + + + + + Vestige @@ -31,7 +33,7 @@
                    - -{prefix}{formatted}{suffix} diff --git a/apps/dashboard/src/lib/components/Dropdown.svelte b/apps/dashboard/src/lib/components/Dropdown.svelte deleted file mode 100644 index 9c12e69..0000000 --- a/apps/dashboard/src/lib/components/Dropdown.svelte +++ /dev/null @@ -1,338 +0,0 @@ - - - - -
                    - {#if label} - {label} - {/if} - - - {#if open} -
                    - {#each options as opt, i (opt.value)} - - {/each} -
                    - {/if} -
                    - - diff --git a/apps/dashboard/src/lib/components/Graph3D.svelte b/apps/dashboard/src/lib/components/Graph3D.svelte index d09dcc9..8c0f769 100644 --- a/apps/dashboard/src/lib/components/Graph3D.svelte +++ b/apps/dashboard/src/lib/components/Graph3D.svelte @@ -50,20 +50,6 @@ let ctx: SceneContext; let animationId: number; - // Accessibility: honour the OS "reduce motion" setting. The dominant - // continuous motion in the graph is the camera auto-rotate; disabling it - // removes the vestibular-trigger while keeping the graph fully usable - // (manual orbit, hover, selection, live events all still work). Tracked - // reactively so a mid-session OS toggle is respected. - let prefersReducedMotion = - typeof window !== 'undefined' && - window.matchMedia?.('(prefers-reduced-motion: reduce)').matches; - let reducedMotionMq: MediaQueryList | null = null; - function onReducedMotionChange(e: MediaQueryListEvent) { - prefersReducedMotion = e.matches; - if (ctx?.controls) ctx.controls.autoRotate = !prefersReducedMotion; - } - // Modules let nodeManager: NodeManager; let edgeManager: EdgeManager; @@ -88,15 +74,6 @@ onMount(() => { ctx = createScene(container); - // Respect reduced-motion: the scene defaults to auto-rotate on; turn it - // off up front for users who asked for less motion, and listen for live - // OS-setting changes. - if (prefersReducedMotion) ctx.controls.autoRotate = false; - if (typeof window !== 'undefined' && window.matchMedia) { - reducedMotionMq = window.matchMedia('(prefers-reduced-motion: reduce)'); - reducedMotionMq.addEventListener?.('change', onReducedMotionChange); - } - // Nebula background const nebula = createNebulaBackground(ctx.scene); nebulaMaterial = nebula.material; @@ -136,7 +113,6 @@ onDestroy(() => { cancelAnimationFrame(animationId); window.removeEventListener('resize', onResize); - reducedMotionMq?.removeEventListener?.('change', onReducedMotionChange); container?.removeEventListener('pointermove', onPointerMove); container?.removeEventListener('click', onClick); effects?.dispose(); diff --git a/apps/dashboard/src/lib/components/Icon.svelte b/apps/dashboard/src/lib/components/Icon.svelte deleted file mode 100644 index 1acf84f..0000000 --- a/apps/dashboard/src/lib/components/Icon.svelte +++ /dev/null @@ -1,149 +0,0 @@ - - - - - - - diff --git a/apps/dashboard/src/lib/components/InsightToast.svelte b/apps/dashboard/src/lib/components/InsightToast.svelte index 8a0a991..f941911 100644 --- a/apps/dashboard/src/lib/components/InsightToast.svelte +++ b/apps/dashboard/src/lib/components/InsightToast.svelte @@ -20,7 +20,6 @@ MemoryUnsuppressed: '◉', Rac1CascadeSwept: '✺', MemoryDeleted: '✕', - HookVerdictRecorded: '⚑', }; function iconFor(type: VestigeEventType): string { @@ -91,7 +90,7 @@ right: 0.75rem; left: 0.75rem; bottom: auto; - top: 5.25rem; + top: 0.75rem; max-width: none; width: auto; align-items: stretch; diff --git a/apps/dashboard/src/lib/components/MemoryCinema.svelte b/apps/dashboard/src/lib/components/MemoryCinema.svelte deleted file mode 100644 index b78c827..0000000 --- a/apps/dashboard/src/lib/components/MemoryCinema.svelte +++ /dev/null @@ -1,831 +0,0 @@ - - - - -{#if open} - - -{/if} - - diff --git a/apps/dashboard/src/lib/components/ObservatoryCanvas.svelte b/apps/dashboard/src/lib/components/ObservatoryCanvas.svelte deleted file mode 100644 index 9eb8754..0000000 --- a/apps/dashboard/src/lib/components/ObservatoryCanvas.svelte +++ /dev/null @@ -1,131 +0,0 @@ - - - - - -{#if status.state === 'unsupported' || status.state === 'error'} - - -{/if} - - diff --git a/apps/dashboard/src/lib/components/PageHeader.svelte b/apps/dashboard/src/lib/components/PageHeader.svelte deleted file mode 100644 index 00ad638..0000000 --- a/apps/dashboard/src/lib/components/PageHeader.svelte +++ /dev/null @@ -1,69 +0,0 @@ - - -
                    -
                    -
                    - -
                    -
                    -

                    {title}

                    - {#if subtitle} -

                    {subtitle}

                    - {/if} -
                    -
                    - {#if children} -
                    - {@render children()} -
                    - {/if} -
                    - - diff --git a/apps/dashboard/src/lib/components/ReceiptCard.svelte b/apps/dashboard/src/lib/components/ReceiptCard.svelte deleted file mode 100644 index 71ea574..0000000 --- a/apps/dashboard/src/lib/components/ReceiptCard.svelte +++ /dev/null @@ -1,218 +0,0 @@ - - -
                    -
                    - {receipt.receipt_id} - - decay: {receipt.decay_risk} - -
                    - -
                    -
                    - {receipt.retrieved.length} - retrieved -
                    -
                    - {receipt.suppressed.length} - suppressed -
                    -
                    - {(receipt.trust_floor * 100).toFixed(0)}% - trust floor -
                    -
                    - - {#if !compact} - {#if receipt.activation_path.length} -
                    - Activation path - {#each receipt.activation_path as path (path)} -
                    {path}
                    - {/each} -
                    - {/if} - - {#if receipt.retrieved.length} -
                    - Retrieved -
                    - {#each receipt.retrieved as id (id)} - {id.slice(0, 8)} - {/each} -
                    -
                    - {/if} - - {#if receipt.suppressed.length} -
                    - Suppressed -
                    - {#each receipt.suppressed as s (s.id)} - - {s.id.slice(0, 8)} · {s.reason.replace('_', ' ')} - - {/each} -
                    -
                    - {/if} - {/if} - - -
                    - - diff --git a/apps/dashboard/src/lib/components/VerdictBar.svelte b/apps/dashboard/src/lib/components/VerdictBar.svelte deleted file mode 100644 index 2131642..0000000 --- a/apps/dashboard/src/lib/components/VerdictBar.svelte +++ /dev/null @@ -1,327 +0,0 @@ - - -{#if visible} -
                    - - - {#if expanded && receipt} -
                    -
                    -
                    -
                    Claim
                    -

                    {displayClaim?.text ?? receipt.draftPreview}

                    -
                    -
                    -
                    Verdict
                    -

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

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

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

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

                    Appeal recorded.

                    - {:else} -

                    No appealable veto in this receipt.

                    - {/if} -
                    -
                    - {/if} -
                    -{/if} - - diff --git a/apps/dashboard/src/lib/components/__tests__/PatternTransferHeatmap.test.ts b/apps/dashboard/src/lib/components/__tests__/PatternTransferHeatmap.test.ts index 8f7fa32..c7b9ccf 100644 --- a/apps/dashboard/src/lib/components/__tests__/PatternTransferHeatmap.test.ts +++ b/apps/dashboard/src/lib/components/__tests__/PatternTransferHeatmap.test.ts @@ -25,20 +25,20 @@ import { // patterns/+page.svelte, but small enough to reason about by hand. // --------------------------------------------------------------------------- -const PROJECTS = ['vestige', 'api-gateway', 'desktop-app'] as const; +const PROJECTS = ['vestige', 'nullgaze', 'injeranet'] as const; const PATTERNS: TransferPatternLike[] = [ { name: 'Result', category: 'ErrorHandling', origin_project: 'vestige', - transferred_to: ['api-gateway', 'desktop-app'], + transferred_to: ['nullgaze', 'injeranet'], transfer_count: 2, }, { name: 'Axum middleware', category: 'ErrorHandling', - origin_project: 'api-gateway', + origin_project: 'nullgaze', transferred_to: ['vestige'], transfer_count: 1, }, @@ -46,7 +46,7 @@ const PATTERNS: TransferPatternLike[] = [ name: 'proptest', category: 'Testing', origin_project: 'vestige', - transferred_to: ['api-gateway'], + transferred_to: ['nullgaze'], transfer_count: 1, }, { @@ -172,15 +172,15 @@ describe('buildTransferMatrix', () => { it('aggregates transfer counts directionally', () => { const m = buildTransferMatrix(PROJECTS, PATTERNS); - // vestige → api-gateway: Result + proptest = 2 - expect(m.vestige['api-gateway'].count).toBe(2); - // vestige → desktop-app: Result only = 1 - expect(m.vestige['desktop-app'].count).toBe(1); - // api-gateway → vestige: Axum middleware = 1 - expect(m['api-gateway'].vestige.count).toBe(1); - // desktop-app → anywhere: zero (no origin in desktop-app in fixtures) - expect(m['desktop-app'].vestige.count).toBe(0); - expect(m['desktop-app']['api-gateway'].count).toBe(0); + // 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)', () => { @@ -189,7 +189,7 @@ describe('buildTransferMatrix', () => { // bug that aggregates both directions into the same cell would pass // the "count" test above but fail this symmetry check. const m = buildTransferMatrix(PROJECTS, PATTERNS); - expect(m.vestige['api-gateway'].count).not.toBe(m['api-gateway'].vestige.count); + expect(m.vestige.nullgaze.count).not.toBe(m.nullgaze.vestige.count); }); it('records self-transfer on the diagonal', () => { @@ -203,13 +203,13 @@ describe('buildTransferMatrix', () => { name: `pattern-${i}`, category: 'ErrorHandling', origin_project: 'vestige', - transferred_to: ['api-gateway'], + transferred_to: ['nullgaze'], transfer_count: 1, })); - const m = buildTransferMatrix(['vestige', 'api-gateway'], manyPatterns); - expect(m.vestige['api-gateway'].count).toBe(5); - expect(m.vestige['api-gateway'].topNames).toHaveLength(3); - expect(m.vestige['api-gateway'].topNames).toEqual(['pattern-0', 'pattern-1', 'pattern-2']); + 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', () => { @@ -233,12 +233,12 @@ describe('buildTransferMatrix', () => { name: 'StrayDest', category: 'Security', origin_project: 'vestige', - transferred_to: ['ghost-project', 'api-gateway'], + transferred_to: ['ghost-project', 'nullgaze'], transfer_count: 2, }; const m = buildTransferMatrix(PROJECTS, [strayDest]); // The known destination counts; the ghost doesn't. - expect(m.vestige['api-gateway'].count).toBe(1); + expect(m.vestige.nullgaze.count).toBe(1); expect((m.vestige as Record)['ghost-project']).toBeUndefined(); }); @@ -248,19 +248,19 @@ describe('buildTransferMatrix', () => { name: 'a', category: 'Testing', origin_project: 'vestige', - transferred_to: ['api-gateway'], + transferred_to: ['nullgaze'], transfer_count: 1, }, { name: 'b', category: 'Testing', origin_project: 'vestige', - transferred_to: ['api-gateway'], + transferred_to: ['nullgaze'], transfer_count: 1, }, ]; - const m = buildTransferMatrix(['vestige', 'api-gateway'], pats, 1); - expect(m.vestige['api-gateway'].topNames).toEqual(['a']); + const m = buildTransferMatrix(['vestige', 'nullgaze'], pats, 1); + expect(m.vestige.nullgaze.topNames).toEqual(['a']); }); }); @@ -276,7 +276,7 @@ describe('matrixMaxCount', () => { it('returns the hottest cell count across all pairs', () => { const m = buildTransferMatrix(PROJECTS, PATTERNS); - // vestige→api-gateway has 2; everything else is ≤1 + // vestige→nullgaze has 2; everything else is ≤1 expect(matrixMaxCount(PROJECTS, m)).toBe(2); }); @@ -297,12 +297,12 @@ describe('flattenNonZero', () => { const m = buildTransferMatrix(PROJECTS, PATTERNS); const rows = flattenNonZero(PROJECTS, m); // Distinct non-zero cells in fixtures: - // vestige→api-gateway = 2 - // vestige→desktop-app = 1 + // vestige→nullgaze = 2 + // vestige→injeranet = 1 // vestige→vestige = 1 - // api-gateway→vestige = 1 + // nullgaze→vestige = 1 expect(rows).toHaveLength(4); - expect(rows[0]).toMatchObject({ from: 'vestige', to: 'api-gateway', count: 2 }); + 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); }); diff --git a/apps/dashboard/src/lib/components/blackbox-helpers.ts b/apps/dashboard/src/lib/components/blackbox-helpers.ts deleted file mode 100644 index 908d880..0000000 --- a/apps/dashboard/src/lib/components/blackbox-helpers.ts +++ /dev/null @@ -1,134 +0,0 @@ -// ═══════════════════════════════════════════════════════════════════════════ -// AGENT BLACK BOX — presentation helpers -// ─────────────────────────────────────────────────────────────────────────── -// Pure functions that turn a raw `TraceEvent` into the label, color, glyph, -// and one-line summary the Black Box timeline renders. Kept out of the -// component so they are unit-testable and reused by the Proof Mode header. -// ═══════════════════════════════════════════════════════════════════════════ -import type { TraceEvent } from '$lib/stores/api'; - -export type TraceKind = TraceEvent['type']; - -/** The accent color for each trace-event kind (CSS color value). */ -export function eventColor(kind: TraceKind): string { - switch (kind) { - case 'mcp.call': - return 'var(--color-synapse-glow, #818cf8)'; - case 'memory.retrieve': - return 'var(--color-recall, #10b981)'; - case 'memory.suppress': - return '#a78bfa'; // violet — the forgetting hue - case 'memory.write': - return '#38bdf8'; // sky — a new write - case 'contradiction.detected': - return '#fb7185'; // rose — tension - case 'sanhedrin.veto': - return '#f43f5e'; // red — a block - case 'dream.patch': - return '#c084fc'; // purple — dream - default: - return 'var(--color-synapse, #6366f1)'; - } -} - -/** A short human label for each kind. */ -export function eventLabel(kind: TraceKind): string { - switch (kind) { - case 'mcp.call': - return 'Tool Call'; - case 'memory.retrieve': - return 'Retrieved'; - case 'memory.suppress': - return 'Suppressed'; - case 'memory.write': - return 'Wrote'; - case 'contradiction.detected': - return 'Contradiction'; - case 'sanhedrin.veto': - return 'Veto'; - case 'dream.patch': - return 'Dream Patch'; - default: - return kind; - } -} - -/** A single glyph (emoji-free SVG path is overkill here; a compact symbol). */ -export function eventGlyph(kind: TraceKind): string { - switch (kind) { - case 'mcp.call': - return '⟐'; - case 'memory.retrieve': - return '◉'; - case 'memory.suppress': - return '⊘'; - case 'memory.write': - return '✎'; - case 'contradiction.detected': - return '⚡'; - case 'sanhedrin.veto': - return '⛔'; - case 'dream.patch': - return '☾'; - default: - return '•'; - } -} - -/** A one-line summary of what an event did, for the timeline row. */ -export function eventSummary(ev: TraceEvent): string { - switch (ev.type) { - case 'mcp.call': - return `${ev.tool} · args ${ev.argsHash.slice(0, 8)}`; - case 'memory.retrieve': - return `${ev.ids.length} ${ev.ids.length === 1 ? 'memory' : 'memories'} surfaced`; - case 'memory.suppress': - return `${ev.id.slice(0, 8)} — ${ev.reason.replace('_', ' ')}`; - case 'memory.write': - return `${ev.id.slice(0, 8)} — ${ev.source}`; - case 'contradiction.detected': - return ev.detail; - case 'sanhedrin.veto': - return `"${ev.claim}" (conf ${(ev.confidence * 100).toFixed(0)}%)`; - case 'dream.patch': - return `${ev.proposalIds.length} consolidation proposal(s)`; - default: - return ''; - } -} - -/** The memory ids an event touched (for graph-pulse replay). */ -export function eventMemoryIds(ev: TraceEvent): string[] { - switch (ev.type) { - case 'memory.retrieve': - return ev.ids; - case 'memory.suppress': - case 'memory.write': - return [ev.id]; - case 'contradiction.detected': - return ev.ids; - case 'sanhedrin.veto': - return ev.evidenceIds; - case 'dream.patch': - return ev.proposalIds; - default: - return []; - } -} - -/** Format a millisecond timestamp as a clock time. */ -export function formatAt(at: number): string { - if (!Number.isFinite(at) || at <= 0) return '—'; - const d = new Date(at); - return d.toLocaleTimeString(undefined, { - hour12: false, - hour: '2-digit', - minute: '2-digit', - second: '2-digit' - }); -} - -/** Elapsed milliseconds of an event relative to the run's first event. */ -export function relativeMs(at: number, startAt: number): number { - return Math.max(0, at - startAt); -} diff --git a/apps/dashboard/src/lib/graph/cinema/__tests__/auteur.test.ts b/apps/dashboard/src/lib/graph/cinema/__tests__/auteur.test.ts deleted file mode 100644 index d8fa1aa..0000000 --- a/apps/dashboard/src/lib/graph/cinema/__tests__/auteur.test.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest'; -import { planShotsDeterministic, resolveShots, SHOT_DEFAULTS, type DirectorPlan } from '../auteur'; -import { planCinemaPath } from '../pathfinder'; -import { computeSignals } from '../topology'; -import { makeNode, makeEdge, resetNodeCounter } from '../../__tests__/helpers'; - -describe('auteur — carry-forward shot resolution', () => { - beforeEach(() => resetNodeCounter()); - - function smallPath() { - const a = makeNode({ id: 'a' }); - const b = makeNode({ id: 'b' }); - const c = makeNode({ id: 'c' }); - const edges = [makeEdge('a', 'b', { weight: 0.8 }), makeEdge('b', 'c', { weight: 0.6 })]; - return { path: planCinemaPath([a, b, c], edges, 'a'), nodes: [a, b, c], edges }; - } - - it('fills EVERY axis from a one-field shot, defaulting to today constants', () => { - const { path } = smallPath(); - const plan: DirectorPlan = { - source: 'backend-llm', - logline: 'x', - arc: 'flat', - shots: [{ nodeId: 'a', move: 'orbit', why: 'test' }], - }; - const resolved = resolveShots(plan, path); - expect(resolved).toHaveLength(path.beats.length); - // The specified field is honored… - expect(resolved[0].move).toBe('orbit'); - // …and every other axis is a real default, never undefined. - expect(resolved[0].standoff).toBe(SHOT_DEFAULTS.standoff); - expect(resolved[0].flightSeconds).toBe(SHOT_DEFAULTS.flightSeconds); - expect(resolved[0].angle).toBe('eye'); - for (const s of resolved) { - for (const k of Object.keys(SHOT_DEFAULTS) as (keyof typeof SHOT_DEFAULTS)[]) { - expect(s[k]).toBeDefined(); - } - expect(typeof s.why).toBe('string'); - expect(s.why.length).toBeGreaterThan(0); - } - }); - - it('carries non-cut axes forward to subsequent beats', () => { - const { path } = smallPath(); - const plan: DirectorPlan = { - source: 'backend-llm', - logline: 'x', - arc: 'flat', - // Only the FIRST beat sets standoff; later beats should inherit it. - shots: [{ nodeId: path.beats[0].nodeId, standoff: 41, why: 'set' }], - }; - const resolved = resolveShots(plan, path); - expect(resolved[0].standoff).toBe(41); - expect(resolved[resolved.length - 1].standoff).toBe(41); // carried forward - }); - - it('cut never carries forward — defaults to fly each beat', () => { - const { path } = smallPath(); - const plan: DirectorPlan = { - source: 'backend-llm', - logline: 'x', - arc: 'flat', - shots: [{ nodeId: path.beats[0].nodeId, cut: 'hard_cut', why: 'cut' }], - }; - const resolved = resolveShots(plan, path); - expect(resolved[0].cut).toBe('hard_cut'); - if (resolved.length > 1) expect(resolved[1].cut).toBe('fly'); - }); - - it('back-fills garbage / out-of-range LLM fields from defaults', () => { - const { path } = smallPath(); - const plan = { - source: 'backend-llm', - logline: 'x', - arc: 'flat', - shots: [ - { - nodeId: path.beats[0].nodeId, - move: 'teleport', // invalid enum - standoff: 9999, // out of range - dwellSeconds: -5, // out of range - why: '', - }, - ], - } as unknown as DirectorPlan; - const resolved = resolveShots(plan, path); - expect(resolved[0].move).toBe(SHOT_DEFAULTS.move); // invalid → default - expect(resolved[0].standoff).toBeLessThanOrEqual(90); // clamped - expect(resolved[0].dwellSeconds).toBeGreaterThanOrEqual(0.6); // clamped - expect(resolved[0].why.length).toBeGreaterThan(0); // empty why → fallback - }); - - it('a null plan still yields one default shot per beat', () => { - const { path } = smallPath(); - const resolved = resolveShots(null, path); - expect(resolved).toHaveLength(path.beats.length); - expect(resolved[0].move).toBe(SHOT_DEFAULTS.move); - }); -}); - -describe('auteur — deterministic director', () => { - beforeEach(() => resetNodeCounter()); - - it('produces a valid plan: one grounded shot per beat, every why non-empty, every nodeId real', () => { - const nodes = Array.from({ length: 8 }, (_, i) => makeNode({ id: `n${i}` })); - const edges = [ - makeEdge('n0', 'n1', { weight: 0.9 }), - makeEdge('n1', 'n2', { weight: 0.2, type: 'contradiction' }), - makeEdge('n0', 'n3', { weight: 0.5 }), - makeEdge('n3', 'n4', { weight: 0.7 }), - ]; - const path = planCinemaPath(nodes, edges, 'n0'); - const signals = computeSignals(nodes, edges); - const plan = planShotsDeterministic(path, signals); - const realIds = new Set(nodes.map((n) => n.id)); - expect(plan.shots).toHaveLength(path.beats.length); - for (const s of plan.shots) { - expect(realIds.has(s.nodeId)).toBe(true); - expect(s.why && s.why.length).toBeGreaterThan(0); - } - expect(plan.source).toBe('deterministic'); - expect(plan.logline.length).toBeGreaterThan(0); - }); - - it('directs a contradiction beat as a Dutch hard-cut crimson collision', () => { - const a = makeNode({ id: 'a' }); - const normal = makeNode({ id: 'normal' }); - const conflict = makeNode({ id: 'conflict' }); - const edges = [ - makeEdge('a', 'normal', { weight: 0.95 }), - makeEdge('a', 'conflict', { weight: 0.2, type: 'contradiction' }), - ]; - const path = planCinemaPath([a, normal, conflict], edges, 'a'); - const signals = computeSignals([a, normal, conflict], edges); - const plan = planShotsDeterministic(path, signals); - const contradictionShot = plan.shots.find((_, i) => path.beats[i].kind === 'contradiction'); - expect(contradictionShot).toBeDefined(); - expect(contradictionShot!.stormMode).toBe('contradiction'); - expect(contradictionShot!.cut).toBe('hard_cut'); - expect(contradictionShot!.dutch).toBeGreaterThan(0); - expect(contradictionShot!.scoreCue).toBe('minor_drop'); - }); - - it('ends on a crane pull-back with a major resolve', () => { - const nodes = Array.from({ length: 5 }, (_, i) => makeNode({ id: `m${i}` })); - const edges = nodes.slice(1).map((n, i) => makeEdge(`m${i}`, n.id, { weight: 0.6 })); - const path = planCinemaPath(nodes, edges, 'm0'); - const signals = computeSignals(nodes, edges); - const plan = planShotsDeterministic(path, signals); - const last = plan.shots[plan.shots.length - 1]; - expect(last.move).toBe('crane'); - expect(last.scoreCue).toBe('major_resolve'); - }); - - it('is deterministic — same inputs yield the same plan', () => { - const nodes = Array.from({ length: 6 }, (_, i) => makeNode({ id: `d${i}` })); - const edges = [makeEdge('d0', 'd1', { weight: 0.8 }), makeEdge('d1', 'd2', { weight: 0.5 })]; - const path = planCinemaPath(nodes, edges, 'd0'); - const sig = computeSignals(nodes, edges); - const p1 = planShotsDeterministic(path, sig); - const p2 = planShotsDeterministic(path, sig); - expect(p1.shots.map((s) => s.move)).toEqual(p2.shots.map((s) => s.move)); - expect(p1.logline).toBe(p2.logline); - }); -}); - -describe('topology — graph signals', () => { - beforeEach(() => resetNodeCounter()); - - it('computes betweenness, clusters, and peak keystone on a real shape', () => { - // Two clusters bridged by 'hub' → hub has the highest betweenness. - const hub = makeNode({ id: 'hub' }); - const l1 = makeNode({ id: 'l1' }); - const l2 = makeNode({ id: 'l2' }); - const r1 = makeNode({ id: 'r1' }); - const r2 = makeNode({ id: 'r2' }); - const edges = [ - makeEdge('l1', 'l2'), - makeEdge('l2', 'hub'), - makeEdge('hub', 'r1'), - makeEdge('r1', 'r2'), - ]; - const sig = computeSignals([hub, l1, l2, r1, r2], edges); - expect(sig.peakBetweennessId).toBe('hub'); - expect(sig.nodes.get('hub')!.betweenness).toBeGreaterThan(sig.nodes.get('l1')!.betweenness); - expect(sig.clusterCount).toBe(1); // all connected through hub - // All signals are finite and in range. - for (const s of sig.nodes.values()) { - expect(s.betweenness).toBeGreaterThanOrEqual(0); - expect(s.betweenness).toBeLessThanOrEqual(1); - expect(Number.isFinite(s.recencyRank)).toBe(true); - } - }); - - it('flags contradiction edges and computes surprise in range', () => { - const a = makeNode({ id: 'a' }); - const b = makeNode({ id: 'b' }); - const edges = [makeEdge('a', 'b', { weight: 0.1, type: 'contradiction' })]; - const sig = computeSignals([a, b], edges); - expect(sig.edges[0].isContradiction).toBe(true); - expect(sig.edges[0].surprise).toBeGreaterThanOrEqual(0); - expect(sig.edges[0].surprise).toBeLessThanOrEqual(1); - }); -}); diff --git a/apps/dashboard/src/lib/graph/cinema/__tests__/pathfinder.test.ts b/apps/dashboard/src/lib/graph/cinema/__tests__/pathfinder.test.ts deleted file mode 100644 index 40aeef6..0000000 --- a/apps/dashboard/src/lib/graph/cinema/__tests__/pathfinder.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest'; -import { planCinemaPath } from '../pathfinder'; -import { makeNode, makeEdge, resetNodeCounter } from '../../__tests__/helpers'; - -describe('planCinemaPath', () => { - beforeEach(() => resetNodeCounter()); - - it('returns an empty path for no nodes', () => { - const path = planCinemaPath([], [], 'missing'); - expect(path.beats).toEqual([]); - expect(path.flowEdges).toEqual([]); - }); - - it('starts at the requested center when it exists', () => { - const a = makeNode({ id: 'a' }); - const b = makeNode({ id: 'b' }); - const path = planCinemaPath([a, b], [makeEdge('a', 'b')], 'a'); - expect(path.beats[0].nodeId).toBe('a'); - expect(path.beats[0].kind).toBe('origin'); - expect(path.beats[0].viaEdge).toBeNull(); - }); - - it('falls back to the most-connected node when center is missing', () => { - const hub = makeNode({ id: 'hub' }); - const x = makeNode({ id: 'x' }); - const y = makeNode({ id: 'y' }); - const path = planCinemaPath( - [x, hub, y], - [makeEdge('hub', 'x'), makeEdge('hub', 'y')], - 'does-not-exist' - ); - expect(path.beats[0].nodeId).toBe('hub'); - }); - - it('visits the strongest-weighted connection first', () => { - const a = makeNode({ id: 'a' }); - const weak = makeNode({ id: 'weak' }); - const strong = makeNode({ id: 'strong' }); - const path = planCinemaPath( - [a, weak, strong], - [makeEdge('a', 'weak', { weight: 0.1 }), makeEdge('a', 'strong', { weight: 0.9 })], - 'a' - ); - expect(path.beats[1].nodeId).toBe('strong'); - expect(path.beats[1].kind).toBe('connection'); - }); - - it('detours through a contradiction edge when reachable', () => { - const a = makeNode({ id: 'a' }); - const normal = makeNode({ id: 'normal' }); - const conflict = makeNode({ id: 'conflict' }); - const path = planCinemaPath( - [a, normal, conflict], - [ - makeEdge('a', 'normal', { weight: 0.95, type: 'semantic' }), - makeEdge('a', 'conflict', { weight: 0.2, type: 'contradiction' }), - ], - 'a' - ); - const kinds = path.beats.map((b) => b.kind); - expect(kinds).toContain('contradiction'); - // The contradiction beat carries max intensity. - const c = path.beats.find((b) => b.kind === 'contradiction'); - expect(c?.intensity).toBe(1); - }); - - it('never exceeds maxBeats and never repeats a node', () => { - const nodes = Array.from({ length: 20 }, (_, i) => makeNode({ id: `n${i}` })); - const edges = nodes.slice(1).map((n) => makeEdge('n0', n.id, { weight: Math.random() })); - const path = planCinemaPath(nodes, edges, 'n0', 5); - expect(path.beats.length).toBeLessThanOrEqual(5); - const ids = path.beats.map((b) => b.nodeId); - expect(new Set(ids).size).toBe(ids.length); - }); - - it('is deterministic — same inputs yield the same path', () => { - const nodes = [makeNode({ id: 'a' }), makeNode({ id: 'b' }), makeNode({ id: 'c' })]; - const edges = [makeEdge('a', 'b', { weight: 0.8 }), makeEdge('b', 'c', { weight: 0.6 })]; - const p1 = planCinemaPath(nodes, edges, 'a'); - const p2 = planCinemaPath(nodes, edges, 'a'); - expect(p1.beats.map((b) => b.nodeId)).toEqual(p2.beats.map((b) => b.nodeId)); - }); - - it('records flowEdges for each traversed connection', () => { - const a = makeNode({ id: 'a' }); - const b = makeNode({ id: 'b' }); - const path = planCinemaPath([a, b], [makeEdge('a', 'b', { weight: 0.7 })], 'a'); - expect(path.flowEdges.length).toBeGreaterThanOrEqual(1); - expect(path.flowEdges[0].source === 'a' || path.flowEdges[0].target === 'a').toBe(true); - }); -}); diff --git a/apps/dashboard/src/lib/graph/cinema/auteur.ts b/apps/dashboard/src/lib/graph/cinema/auteur.ts deleted file mode 100644 index e2334bf..0000000 --- a/apps/dashboard/src/lib/graph/cinema/auteur.ts +++ /dev/null @@ -1,223 +0,0 @@ -// The Auteur — the director's brain + the typed shot-plan contract. -// -// The LLM (Tier 1) or the deterministic rule table (Tier 2) produces a -// DirectorPlan: a sequence of cinematographic Shots, one per CinemaBeat, each -// grounded in a real node and justified by a real graph metric. The camera -// runtime (director.ts) executes it. Carry-forward semantics mean a sparse or -// half-hallucinated plan ALWAYS resolves to a coherent film — the same -// robustness pattern as narrator.resolveNarration. - -import type { CinemaPath, CinemaBeat } from './pathfinder'; -import type { GraphSignals } from './topology'; - -// ── Camera grammar (string unions keep LLM output validatable) ─────────────── -export type Move = 'push_in' | 'pull_back' | 'orbit' | 'crane' | 'whip_pan' | 'rack_focus' | 'hold'; -export type Angle = 'eye' | 'low' | 'high'; // low = look up (power); high = look down (decay) -export type Cut = 'fly' | 'hard_cut' | 'match_cut'; -export type StormMode = 'anchor' | 'connection' | 'contradiction' | 'surprise'; -export type CaptionTone = 'curious' | 'tense' | 'resolved' | 'awe' | 'neutral'; -export type ScoreCue = 'motif' | 'minor_drop' | 'major_resolve' | 'silence'; -export type Act = 'I' | 'II' | 'III'; -export type EmotionalArc = 'man_in_hole' | 'rags_to_riches' | 'icarus' | 'cinderella' | 'oedipus' | 'flat'; -export type DirectorSource = 'backend-llm' | 'on-device' | 'deterministic'; - -/** A directed shot. Only axes that CHANGE need be set — the rest carry forward - * from the previous resolved shot (ultimate default = today's camera constants). */ -export interface Shot { - nodeId: string; // MUST cite a real node (alignment key + grounding constraint) - move?: Move; - angle?: Angle; - dutch?: number; // camera roll, radians, 0..~0.5 - standoff?: number; // world units - flightSeconds?: number; - dwellSeconds?: number; - halflife?: number; // spring smoothing; 0 = jump-cut - cut?: Cut; - stormMode?: StormMode; - intensity?: number; // 0..1 → scales the ignition spike - tension?: number; // 0..1 master scalar - act?: Act; - tone?: CaptionTone; - scoreCue?: ScoreCue; - why: string; // REQUIRED: cites the real metric driving this shot - viaEdgeKey?: string; // `${source}->${target}` for two-node framing -} - -export interface DirectorPlan { - source: DirectorSource; - logline: string; - arc: EmotionalArc; - shots: Shot[]; -} - -/** Every axis filled after carry-forward — what the director reads each beat. */ -export type ResolvedShot = Required> & { viaEdgeKey?: string }; - -// Ultimate defaults — today's hardcoded camera constants, so a plan-less or -// fully-sparse run is byte-identical to the pre-Auteur camera. -export const SHOT_DEFAULTS: Omit = { - move: 'hold', - angle: 'eye', - dutch: 0, - standoff: 26, - flightSeconds: 2.4, - dwellSeconds: 3.2, - halflife: 0.35, - cut: 'fly', - stormMode: 'connection', - intensity: 0.7, - tension: 0.3, - act: 'I', - tone: 'neutral', - scoreCue: 'motif', -}; - -const MOVES: ReadonlySet = new Set(['push_in', 'pull_back', 'orbit', 'crane', 'whip_pan', 'rack_focus', 'hold']); -const ANGLES: ReadonlySet = new Set(['eye', 'low', 'high']); -const CUTS: ReadonlySet = new Set(['fly', 'hard_cut', 'match_cut']); -const STORM_MODES: ReadonlySet = new Set(['anchor', 'connection', 'contradiction', 'surprise']); -const TONES: ReadonlySet = new Set(['curious', 'tense', 'resolved', 'awe', 'neutral']); -const SCORE_CUES: ReadonlySet = new Set(['motif', 'minor_drop', 'major_resolve', 'silence']); -const ACTS: ReadonlySet = new Set(['I', 'II', 'III']); - -function num(v: unknown, lo: number, hi: number, fallback: number): number { - const n = typeof v === 'number' && Number.isFinite(v) ? v : NaN; - if (Number.isNaN(n)) return fallback; - return Math.max(lo, Math.min(hi, n)); -} -function pick(v: unknown, set: ReadonlySet, fallback: T): T { - return typeof v === 'string' && set.has(v as T) ? (v as T) : fallback; -} - -/** - * Resolve a DirectorPlan into one fully-specified ResolvedShot per beat. - * Aligns by nodeId; every unspecified/garbage axis is back-filled by carry-forward - * (previous shot → SHOT_DEFAULTS). A shot can NEVER be blank or invalid. - */ -export function resolveShots(plan: DirectorPlan | null, path: CinemaPath): ResolvedShot[] { - const byNode = new Map(); - for (const s of plan?.shots ?? []) { - if (s && typeof s.nodeId === 'string') byNode.set(s.nodeId, s); - } - const resolved: ResolvedShot[] = []; - let prev: ResolvedShot | null = null; - for (const beat of path.beats) { - const raw = byNode.get(beat.nodeId); - const base = prev ?? { ...SHOT_DEFAULTS, nodeId: beat.nodeId, why: '' }; - const shot: ResolvedShot = { - nodeId: beat.nodeId, - move: pick(raw?.move, MOVES, base.move), - angle: pick(raw?.angle, ANGLES, base.angle), - dutch: num(raw?.dutch, 0, 0.6, base.dutch), - standoff: num(raw?.standoff, 8, 90, base.standoff), - flightSeconds: num(raw?.flightSeconds, 0.4, 6, base.flightSeconds), - dwellSeconds: num(raw?.dwellSeconds, 0.6, 8, base.dwellSeconds), - halflife: num(raw?.halflife, 0, 1.5, base.halflife), - cut: pick(raw?.cut, CUTS, 'fly'), // cut never carries forward — default per beat - stormMode: pick(raw?.stormMode, STORM_MODES, base.stormMode), - intensity: num(raw?.intensity, 0, 1, base.intensity), - tension: num(raw?.tension, 0, 1, base.tension), - act: pick(raw?.act, ACTS, base.act), - tone: pick(raw?.tone, TONES, base.tone), - scoreCue: pick(raw?.scoreCue, SCORE_CUES, 'motif'), - why: typeof raw?.why === 'string' && raw.why.trim() ? raw.why : base.why || 'establishing shot', - viaEdgeKey: typeof raw?.viaEdgeKey === 'string' ? raw.viaEdgeKey : undefined, - }; - resolved.push(shot); - prev = shot; - } - return resolved; -} - -// ── The deterministic auteur (Tier 2) ──────────────────────────────────────── -// The graph-metric → shot-grammar rule table. This SAME table is handed to the -// LLM as its system prompt (see directorSystemPrompt), so Tier-1 output is -// directly comparable to and back-fillable against this baseline. - -function actFor(progress: number): Act { - return progress < 0.34 ? 'I' : progress < 0.72 ? 'II' : 'III'; -} - -/** - * Produce a cinematic DirectorPlan from pure graph signals — no LLM. This alone - * ships the hero film: every shot is grounded and justified by a real metric. - */ -export function planShotsDeterministic(path: CinemaPath, signals: GraphSignals): DirectorPlan { - const n = path.beats.length; - const shots: Shot[] = path.beats.map((beat, i) => { - const progress = n > 1 ? i / (n - 1) : 0; - const act = actFor(progress); - const sig = signals.nodes.get(beat.nodeId); - const isPeak = beat.nodeId === signals.peakBetweennessId; - const isFinale = i === n - 1; - const isOrigin = i === 0; - - // Default shot for a plain connection beat. - let shot: Shot = { - nodeId: beat.nodeId, - move: 'push_in', - angle: 'eye', - cut: 'fly', - stormMode: 'connection', - tone: 'curious', - scoreCue: 'motif', - act, - intensity: 0.6, - tension: 0.3, - why: 'a connected memory', - }; - - if (isOrigin) { - shot = { ...shot, move: 'push_in', tone: 'curious', tension: 0.25, stormMode: 'anchor', why: 'opening on the focal memory' }; - } - // High-betweenness keystone → reverent low-angle slow orbit. - if (isPeak || (sig && sig.betweenness > 0.6)) { - shot = { ...shot, move: 'orbit', angle: 'low', stormMode: 'anchor', intensity: 0.75, tension: 0.45, tone: 'awe', why: 'low-angle orbit — the most load-bearing memory in the graph' }; - } - // Contradiction → Dutch push-in, hard cut, crimson chaos, minor drop. - if (beat.kind === 'contradiction') { - shot = { ...shot, move: 'push_in', angle: 'eye', dutch: 0.28, cut: 'hard_cut', stormMode: 'contradiction', intensity: 1, tension: 0.95, tone: 'tense', scoreCue: 'minor_drop', viaEdgeKey: beat.viaEdge ? `${beat.viaEdge.source}->${beat.viaEdge.target}` : undefined, why: 'two memories in tension — a Dutch two-shot collision' }; - } - // Surprise edge → gold/violet convergence, rising awe. - if (beat.kind === 'surprise') { - shot = { ...shot, move: 'orbit', stormMode: 'surprise', intensity: 0.85, tension: 0.6, tone: 'awe', scoreCue: 'motif', why: 'a surprising, distant-but-plausible connection' }; - } - // Fading memory → drifting high angle. - if (sig && (sig.retention < 0.35 || sig.suppression > 0.5)) { - shot = { ...shot, angle: 'high', move: 'pull_back', tone: 'neutral', intensity: 0.4, why: 'a fading memory — high-angle drift' }; - } - // Recent → the "now" beat. - if (beat.kind === 'recent') { - shot = { ...shot, move: 'push_in', tone: 'resolved', tension: 0.4, why: 'where the memory is now' }; - } - // Finale → crane pull-back, major resolve. - if (isFinale) { - shot = { ...shot, move: 'crane', cut: 'fly', stormMode: 'anchor', tone: 'awe', tension: 0.5, scoreCue: 'major_resolve', why: 'crane pull-back over the whole cluster — resolution' }; - } - return shot; - }); - - const arc: EmotionalArc = path.beats.some((b) => b.kind === 'contradiction') ? 'man_in_hole' : 'rags_to_riches'; - const originLabel = path.beats[0]?.node.label ?? 'a memory'; - const logline = `A short film about ${originLabel} — ${n} shots through the graph${arc === 'man_in_hole' ? ', through a contradiction and out the other side' : ''}.`; - - return { source: 'deterministic', logline, arc, shots }; -} - -/** The rule table as an LLM system prompt — keeps Tier-1 output comparable to - * the Tier-2 baseline (and thus back-fillable by resolveShots). */ -export function directorSystemPrompt(): string { - return [ - 'You are a film director shooting a short documentary about an AI\'s own memory graph.', - 'Output a DirectorPlan: a logline, an emotional arc, and one shot per beat.', - 'Each shot MUST cite a real nodeId and a real "why" referencing a graph metric.', - 'Grammar → meaning:', - '- high betweenness (load-bearing memory) → low-angle slow orbit, reverent', - '- contradiction edge → Dutch angle + push_in + hard_cut + crimson storm + minor_drop score', - '- surprising distant link → gold/violet orbit→stream convergence + awe', - '- merge/supersede → match_cut at identical standoff+angle (same idea)', - '- low retention / high suppression → high-angle drift (fading)', - '- finale → crane pull_back + major_resolve', - 'Build a real emotional arc across acts I→II→III. Only specify axes that change.', - ].join('\n'); -} diff --git a/apps/dashboard/src/lib/graph/cinema/director.ts b/apps/dashboard/src/lib/graph/cinema/director.ts deleted file mode 100644 index 0891169..0000000 --- a/apps/dashboard/src/lib/graph/cinema/director.ts +++ /dev/null @@ -1,285 +0,0 @@ -// Memory Cinema — the camera director. -// -// Drives a smooth, cinematic camera flight through a planned CinemaPath. Pure -// choreography: it mutates a THREE.PerspectiveCamera + an OrbitControls-like -// target each frame and emits beat-arrival callbacks the narrator + sandbox -// hook into. It knows nothing about which renderer (WebGL/WebGPU) is on screen, -// so it works identically for the legacy graph and the WebGPU sandbox. -// -// Respects prefers-reduced-motion: when reduced, it JUMP-CUTS between beats -// (instant position, dwell, advance) instead of flying — captions still fire. - -import * as THREE from 'three'; -import type { CinemaPath, CinemaBeat } from './pathfinder'; -import type { ResolvedShot } from './auteur'; - -export interface DirectorCallbacks { - /** Fired once when the camera arrives at (or cuts to) a beat. The resolved - * shot for the beat is passed so consumers can drive storm/score/captions. */ - onBeat?: (beat: CinemaBeat, index: number, shot: ResolvedShot | null) => void; - /** Fired when the whole tour finishes. */ - onComplete?: () => void; - /** Fired every frame with overall progress 0..1 (for a scrubber/progress bar). */ - onProgress?: (t: number) => void; -} - -export interface DirectorOptions { - /** Seconds of camera flight between consecutive beats. */ - flightSeconds?: number; - /** Seconds the camera dwells on each beat before advancing. */ - dwellSeconds?: number; - /** Stand-off distance from the focused node, in world units. */ - standoff?: number; - /** Instant cuts instead of flights (prefers-reduced-motion). */ - reducedMotion?: boolean; - /** Optional per-beat director's plan (one ResolvedShot per beat, aligned by - * index). When ABSENT the camera behaves byte-identically to the pre-Auteur - * director — every value falls back to the constants above. When present, - * each shot's move/angle/dutch/standoff/flight/dwell/cut directs that beat. */ - shots?: ResolvedShot[]; - /** When true, the camera frames the WORLD ORIGIN every shot (the WebGPU storm - * is pinned there) instead of flying out to scattered node positions — so the - * subject is ALWAYS centered and can never fly off-screen. Camera variety - * comes purely from angle/standoff/orbit. Used by the WebGPU sandbox path. */ - centerOnOrigin?: boolean; -} - -type Phase = 'idle' | 'flying' | 'dwelling' | 'done'; - -const _tmpDir = new THREE.Vector3(); -const _tmpUp = new THREE.Vector3(0, 1, 0); -const _origin = new THREE.Vector3(0, 0, 0); - -export class CinemaDirector { - private camera: THREE.PerspectiveCamera; - private target: THREE.Vector3; - private positions: Map; - private path: CinemaPath; - private cb: DirectorCallbacks; - private opts: Required; - - private phase: Phase = 'idle'; - private beatIndex = 0; - private phaseElapsed = 0; - - // Flight interpolation endpoints. - private fromPos = new THREE.Vector3(); - private toPos = new THREE.Vector3(); - private fromTarget = new THREE.Vector3(); - private toTarget = new THREE.Vector3(); - - constructor( - camera: THREE.PerspectiveCamera, - target: THREE.Vector3, - positions: Map, - path: CinemaPath, - cb: DirectorCallbacks = {}, - opts: DirectorOptions = {} - ) { - this.camera = camera; - this.target = target; - this.positions = positions; - this.path = path; - this.cb = cb; - this.opts = { - flightSeconds: opts.flightSeconds ?? 2.4, - dwellSeconds: opts.dwellSeconds ?? 3.2, - standoff: opts.standoff ?? 26, - reducedMotion: opts.reducedMotion ?? false, - shots: opts.shots ?? [], - centerOnOrigin: opts.centerOnOrigin ?? false, - }; - } - - /** The resolved shot directing a beat, or null when no plan was supplied - * (→ the camera uses the constant defaults = pre-Auteur behavior). */ - private shotAt(index: number): ResolvedShot | null { - return this.opts.shots[index] ?? null; - } - - /** Per-beat flight duration: the shot's value, else the global default. A - * hard/match cut has zero flight (handled in beginFlightTo). */ - private flightSecondsAt(index: number): number { - return this.shotAt(index)?.flightSeconds ?? this.opts.flightSeconds; - } - private dwellSecondsAt(index: number): number { - return this.shotAt(index)?.dwellSeconds ?? this.opts.dwellSeconds; - } - - get totalBeats(): number { - return this.path.beats.length; - } - - get isRunning(): boolean { - return this.phase !== 'idle' && this.phase !== 'done'; - } - - /** Begin the tour from the first beat. */ - start(): void { - if (this.path.beats.length === 0) { - this.phase = 'done'; - this.cb.onComplete?.(); - return; - } - this.beatIndex = 0; - this.beginFlightTo(0); - } - - stop(): void { - this.phase = 'done'; - } - - /** The focal point a beat frames: the world ORIGIN in centered mode (storm is - * pinned there), else the node's laid-out position. */ - private focalPoint(beat: CinemaBeat): THREE.Vector3 | null { - if (this.opts.centerOnOrigin) return _origin; - return this.positions.get(beat.nodeId) ?? null; - } - - /** Compute the camera stand-off position for a beat's focal point, directed by - * its shot (move / angle / standoff). With no shot, reproduces the original - * framing exactly: standoff = opts.standoff, +0.35 up-bias (filmic tilt). */ - private framePosition(beat: CinemaBeat, index: number, out: THREE.Vector3): THREE.Vector3 { - const nodePos = this.focalPoint(beat); - if (!nodePos) { - // Node has no resolved position yet — keep current framing. - return out.copy(this.camera.position); - } - const shot = this.shotAt(index); - - _tmpDir.copy(this.camera.position).sub(nodePos); - if (_tmpDir.lengthSq() < 1e-4) _tmpDir.set(0, 0.4, 1); - _tmpDir.normalize(); - - // Vertical bias = the camera angle. Default +0.35 (slightly above, the - // original filmic tilt). low = look UP at the node (power), high = look - // DOWN (decay/fading). - let upBias = 0.35; - if (shot) { - if (shot.angle === 'low') upBias = -0.45; - else if (shot.angle === 'high') upBias = 0.7; - } - _tmpDir.addScaledVector(_tmpUp, upBias).normalize(); - - // Stand-off = how close: push_in tightens, pull_back/crane widen. - let standoff = shot?.standoff ?? this.opts.standoff; - if (shot) { - if (shot.move === 'push_in') standoff *= 0.7; - else if (shot.move === 'pull_back') standoff *= 1.5; - else if (shot.move === 'crane') standoff *= 1.8; - } - // In centered (WebGPU storm) mode the subject is pinned to the origin and - // the sandbox clamps the camera to a far band. Keep the directed standoff - // INSIDE that band so the camera never fights the clamp (which read as an - // off-center jump) — variety here comes from angle + orbit, not distance. - if (this.opts.centerOnOrigin) standoff = Math.max(31, Math.min(43, standoff)); - return out.copy(nodePos).addScaledVector(_tmpDir, standoff); - } - - private beginFlightTo(index: number): void { - const beat = this.path.beats[index]; - const nodePos = this.focalPoint(beat); - const shot = this.shotAt(index); - - this.fromPos.copy(this.camera.position); - this.fromTarget.copy(this.target); - this.framePosition(beat, index, this.toPos); - this.toTarget.copy(nodePos ?? this.target); - this.phaseElapsed = 0; - - // A directed hard/match cut snaps instantly (like reduced-motion), so the - // editorial "cut" reads as an edit, not a fly. reduced-motion forces this - // for every beat regardless of shot. - const snap = this.opts.reducedMotion || shot?.cut === 'hard_cut' || shot?.cut === 'match_cut'; - if (snap) { - this.camera.position.copy(this.toPos); - this.target.copy(this.toTarget); - this.phase = 'dwelling'; - this.cb.onBeat?.(beat, index, shot); - } else { - this.phase = 'flying'; - } - } - - /** Advance the choreography. Call once per animation frame with delta seconds. */ - update(deltaSeconds: number): void { - if (this.phase === 'idle' || this.phase === 'done') return; - // Clamp dt so a tab-switch stall doesn't teleport the camera. - const dt = Math.max(0, Math.min(deltaSeconds, 0.05)); - this.phaseElapsed += dt; - - const flightSecs = this.flightSecondsAt(this.beatIndex); - const dwellSecs = this.dwellSecondsAt(this.beatIndex); - - if (this.phase === 'flying') { - const t = Math.min(1, this.phaseElapsed / flightSecs); - const e = easeInOutCubic(t); - this.camera.position.lerpVectors(this.fromPos, this.toPos, e); - this.target.lerpVectors(this.fromTarget, this.toTarget, e); - this.applyDutch(this.beatIndex, e); - if (t >= 1) { - this.phase = 'dwelling'; - this.phaseElapsed = 0; - this.cb.onBeat?.(this.path.beats[this.beatIndex], this.beatIndex, this.shotAt(this.beatIndex)); - } - } else if (this.phase === 'dwelling') { - if (!this.opts.reducedMotion) { - const nodePos = this.focalPoint(this.path.beats[this.beatIndex]); - if (nodePos) { - this.target.lerp(nodePos, 0.02); // gentle settle keeps the shot alive - // An orbit shot slowly revolves the camera around the node - // during the dwell — the signature "reverent" move for keystones. - if (this.shotAt(this.beatIndex)?.move === 'orbit') { - this.orbitAround(nodePos, dt * 0.35); - } - } - } - if (this.phaseElapsed >= dwellSecs) { - const nextIndex = this.beatIndex + 1; - if (nextIndex >= this.path.beats.length) { - this.phase = 'done'; - this.cb.onProgress?.(1); - this.cb.onComplete?.(); - return; - } - this.beatIndex = nextIndex; - this.beginFlightTo(nextIndex); - } - } - - // Overall progress across the whole tour (beat + intra-beat fraction). - // Guard against an empty path (per = 0) so progress can never be NaN. - const per = this.path.beats.length > 0 ? 1 / this.path.beats.length : 0; - const intra = - this.phase === 'flying' - ? Math.min(1, this.phaseElapsed / flightSecs) * 0.5 - : 0.5 + Math.min(1, this.phaseElapsed / dwellSecs) * 0.5; - this.cb.onProgress?.(Math.min(1, this.beatIndex * per + intra * per)); - } - - /** Revolve the camera around a node by `angle` radians (orbit shots). */ - private orbitAround(center: THREE.Vector3, angle: number): void { - _tmpDir.copy(this.camera.position).sub(center); - const cos = Math.cos(angle); - const sin = Math.sin(angle); - const x = _tmpDir.x * cos - _tmpDir.z * sin; - const z = _tmpDir.x * sin + _tmpDir.z * cos; - _tmpDir.x = x; - _tmpDir.z = z; - this.camera.position.copy(center).add(_tmpDir); - } - - /** Roll the camera (Dutch angle) toward the shot's target roll over the - * flight, easing back to upright for non-Dutch shots. */ - private applyDutch(index: number, t: number): void { - const targetRoll = this.shotAt(index)?.dutch ?? 0; - const roll = targetRoll * t; - // camera.up = rotate world-up around the camera's forward axis by `roll`. - _tmpDir.set(0, 0, -1).applyQuaternion(this.camera.quaternion); // forward - this.camera.up.set(0, 1, 0).applyAxisAngle(_tmpDir, roll); - } -} - -function easeInOutCubic(t: number): number { - return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; -} diff --git a/apps/dashboard/src/lib/graph/cinema/narrator.ts b/apps/dashboard/src/lib/graph/cinema/narrator.ts deleted file mode 100644 index 7e3d429..0000000 --- a/apps/dashboard/src/lib/graph/cinema/narrator.ts +++ /dev/null @@ -1,148 +0,0 @@ -// Memory Cinema — narration tiers 1 & 2. -// -// Tier 1 (premium): a backend LLM endpoint (/api/narrative) authors rich prose -// from the planned path. Used only when the backend advertises it. -// Tier 2 (smart local default): deterministic, structured captions generated -// purely from the real node/edge data — no network, no LLM, instant. This is -// what the static HN demo and any backend-without-LLM setup uses. -// -// Tier 3 (the BFS camera engine in director.ts) always runs underneath; the -// narrator only decides what TEXT accompanies each beat. If everything here -// fails, captions fall back to Tier 2, which cannot fail. - -import type { CinemaBeat, CinemaPath } from './pathfinder'; - -export interface BeatNarration { - nodeId: string; - /** The caption shown + optionally spoken for this beat. */ - text: string; - /** Short label for the beat kind, shown as a chip. */ - chip: string; -} - -export type NarrationSource = 'backend-llm' | 'local-captions'; - -export interface CinemaNarration { - source: NarrationSource; - beats: BeatNarration[]; -} - -// `satisfies` makes the compiler error if a new CinemaBeat['kind'] is added -// without a chip here — closes the silent "undefined chip → blank UI" gap. -const KIND_CHIP = { - origin: 'Origin', - connection: 'Connection', - contradiction: 'Tension', - recent: 'Now', - bridge: 'Jump', - surprise: 'Surprise', -} satisfies Record; - -function snippet(content: string, max = 90): string { - const s = (content ?? '').replace(/\s+/g, ' ').trim(); - if (s.length <= max) return s; - return s.slice(0, max - 1).trimEnd() + '…'; -} - -function typeLabel(nodeType: string): string { - const t = (nodeType ?? 'memory').toLowerCase(); - return t.charAt(0).toUpperCase() + t.slice(1); -} - -/** - * Tier 2 — deterministic structured captions from real data only. - * Never throws; always returns a caption per beat. - */ -export function localCaptions(path: CinemaPath): CinemaNarration { - const beats: BeatNarration[] = path.beats.map((beat, i) => { - const n = beat.node; - const what = snippet(n.label || `(${typeLabel(n.type)} memory)`); - let text: string; - switch (beat.kind) { - case 'origin': - text = `We begin at a ${typeLabel(n.type).toLowerCase()} the graph is centered on — "${what}".`; - break; - case 'contradiction': { - const via = beat.viaEdge?.type ? beat.viaEdge.type.replace(/_/g, ' ') : 'a conflict'; - text = `This is held in tension with the last memory through ${via}: "${what}".`; - break; - } - case 'recent': - text = `And where the mind is now — a recent memory: "${what}".`; - break; - case 'bridge': - text = `Crossing to a separate cluster — "${what}".`; - break; - default: { - const w = beat.viaEdge?.weight ?? 0; - const strength = w > 0.66 ? 'strongly' : w > 0.33 ? 'closely' : 'loosely'; - text = `${strength} connected from there: a ${typeLabel(n.type).toLowerCase()} — "${what}".`; - } - } - // Tags add texture when present. - if (n.tags && n.tags.length > 0 && i > 0) { - text += ` [${n.tags.slice(0, 3).join(', ')}]`; - } - return { nodeId: beat.nodeId, text, chip: KIND_CHIP[beat.kind] }; - }); - return { source: 'local-captions', beats }; -} - -/** - * Resolve the best available narration for a path. - * - * @param fetchBackend optional async fn that returns backend-LLM narration - * beats (Tier 1). If it's absent, rejects, times out, or returns a mismatched - * shape, we silently fall back to Tier 2 local captions. The caller passes - * this only when the backend has advertised /api/narrative support. - */ -export async function resolveNarration( - path: CinemaPath, - fetchBackend?: () => Promise -): Promise { - const fallback = localCaptions(path); - if (!fetchBackend) return fallback; - - let timer: ReturnType | undefined; - try { - const backend = await Promise.race([ - fetchBackend(), - new Promise((resolve) => { - timer = setTimeout(() => resolve(null), 6000); - }), - ]); - - // Keep only well-formed backend beats (guards against null/empty/garbage - // entries that would otherwise produce blank captions mid-tour). - const valid = Array.isArray(backend) - ? backend.filter( - (b): b is BeatNarration => - !!b && typeof b.nodeId === 'string' && typeof b.text === 'string' && b.text.trim().length > 0 - ) - : []; - if (valid.length === 0) return fallback; - - // Align backend beats to the real path by nodeId; fill any gap from the - // bounds-safe local caption so every beat always has text (never blank). - const byNode = new Map(valid.map((b) => [b.nodeId, b])); - const beats: BeatNarration[] = path.beats.map((beat, i) => { - const hit = byNode.get(beat.nodeId); - if (hit) { - const chip = typeof hit.chip === 'string' && hit.chip.trim() ? hit.chip : KIND_CHIP[beat.kind]; - return { nodeId: beat.nodeId, text: hit.text, chip }; - } - return ( - fallback.beats[i] ?? { - nodeId: beat.nodeId, - text: beat.node.label || '(unlabeled memory)', - chip: KIND_CHIP[beat.kind], - } - ); - }); - return { source: 'backend-llm', beats }; - } catch { - return fallback; - } finally { - if (timer) clearTimeout(timer); - } -} diff --git a/apps/dashboard/src/lib/graph/cinema/pathfinder.ts b/apps/dashboard/src/lib/graph/cinema/pathfinder.ts deleted file mode 100644 index 4e8a457..0000000 --- a/apps/dashboard/src/lib/graph/cinema/pathfinder.ts +++ /dev/null @@ -1,171 +0,0 @@ -// Memory Cinema — Tier 3: the bulletproof pathfinder. -// -// Plans a cinematic tour through the REAL memory graph using nothing but the -// nodes + edges the backend already returns. This is the deterministic engine -// that ALWAYS drives the camera, regardless of which narration tier (backend -// LLM / local captions / none) is active. No WebGPU, no network, no LLM — if -// everything else fails, this still produces a coherent, watchable flythrough. -// -// The path is intentionally a STORY, not a raw BFS dump: -// 1. start at the center (the memory the graph is focused on) -// 2. visit its strongest-weighted connections (what it's most tied to) -// 3. detour to a contradiction edge if one exists (tension = interesting) -// 4. end on a recently-created node (where the mind is now) -// Falling back to plain weighted BFS when those signals are absent. - -import type { GraphNode, GraphEdge } from '$types'; - -export interface CinemaBeat { - /** Node this beat centers the camera on. */ - nodeId: string; - /** The node payload, for the narrator + visuals. */ - node: GraphNode; - /** Edge traversed to arrive here (null for the opening beat). */ - viaEdge: GraphEdge | null; - /** Why this beat exists — drives the deterministic caption + visual emphasis. */ - kind: 'origin' | 'connection' | 'contradiction' | 'recent' | 'bridge' | 'surprise'; - /** 0..1 emphasis used by the sandbox to spike emissive/bloom on arrival. */ - intensity: number; -} - -export interface CinemaPath { - beats: CinemaBeat[]; - /** The node the requested centerId resolved to (which the tour actually - * starts from). May differ from the requested centerId when it was missing, - * in which case `pivoted` is true — callers can surface this if they care. */ - centerId: string; - /** True when the requested centerId did not exist and we picked a start node. */ - pivoted: boolean; - /** Edges that should visibly "flow" during the tour, in beat order. */ - flowEdges: GraphEdge[]; -} - -export interface Adjacency { - [nodeId: string]: { edge: GraphEdge; otherId: string }[]; -} - -export function buildAdjacency(edges: GraphEdge[]): Adjacency { - const adj: Adjacency = {}; - for (const edge of edges) { - (adj[edge.source] ??= []).push({ edge, otherId: edge.target }); - (adj[edge.target] ??= []).push({ edge, otherId: edge.source }); - } - // Strongest connections first so the tour visits the most meaningful ties. - for (const id of Object.keys(adj)) { - adj[id].sort((a, b) => (b.edge.weight ?? 0) - (a.edge.weight ?? 0)); - } - return adj; -} - -export function isContradictionEdge(edge: GraphEdge): boolean { - const t = (edge.type ?? '').toLowerCase(); - return t.includes('contradict') || t.includes('conflict') || t.includes('supersede'); -} - -export function recencyOf(node: GraphNode): number { - // Larger = more recent. Tolerates missing/invalid timestamps. - const t = Date.parse(node.updatedAt || node.createdAt || ''); - return Number.isFinite(t) ? t : 0; -} - -/** - * Plan a cinematic path over the real graph. - * - * @param maxBeats hard cap on tour length (keeps the flythrough watchable). - * Deterministic: same inputs always yield the same path (no randomness), so the - * recorded launch GIF is reproducible. - */ -export function planCinemaPath( - nodes: GraphNode[], - edges: GraphEdge[], - centerId: string, - maxBeats = 7 -): CinemaPath { - const byId = new Map(nodes.map((n) => [n.id, n])); - const empty: CinemaPath = { beats: [], centerId, pivoted: false, flowEdges: [] }; - if (nodes.length === 0) return empty; - - // Resolve a real starting node: prefer centerId, else the explicit center - // flag, else the most-connected node, else the first node. Track whether we - // had to pivot off the requested centerId so callers can surface it. - const adj = buildAdjacency(edges); - const requestedExists = byId.has(centerId); - let startId = requestedExists ? centerId : ''; - if (!startId) startId = nodes.find((n) => n.isCenter)?.id ?? ''; - if (!startId) { - startId = nodes - .map((n) => ({ id: n.id, deg: adj[n.id]?.length ?? 0 })) - .sort((a, b) => b.deg - a.deg)[0].id; - } - const start = byId.get(startId); - if (!start) return empty; - const pivoted = !requestedExists; - - const visited = new Set([startId]); - const beats: CinemaBeat[] = [ - { nodeId: startId, node: start, viaEdge: null, kind: 'origin', intensity: 1 }, - ]; - const flowEdges: GraphEdge[] = []; - - // Greedy weighted walk: from the current frontier, step to the strongest - // unvisited neighbour, with a one-time detour to a contradiction if reachable. - let current = startId; - let contradictionUsed = false; - - while (beats.length < maxBeats) { - const neighbours = adj[current] ?? []; - - // Prefer an unused contradiction edge once — tension makes a better story. - let next: { edge: GraphEdge; otherId: string } | undefined; - if (!contradictionUsed) { - next = neighbours.find((n) => !visited.has(n.otherId) && isContradictionEdge(n.edge)); - if (next) contradictionUsed = true; - } - // Otherwise the strongest unvisited tie. - if (!next) next = neighbours.find((n) => !visited.has(n.otherId)); - - // Dead end: hop to the most recent unvisited node anywhere (a "bridge" - // cut) so the tour can keep going instead of stalling. - if (!next) { - const remaining = nodes - .filter((n) => !visited.has(n.id)) - .sort((a, b) => recencyOf(b) - recencyOf(a)); - if (remaining.length === 0) break; - const node = remaining[0]; - visited.add(node.id); - beats.push({ nodeId: node.id, node, viaEdge: null, kind: 'bridge', intensity: 0.6 }); - current = node.id; - continue; - } - - const node = byId.get(next.otherId); - if (!node) { - visited.add(next.otherId); - continue; - } - visited.add(node.id); - flowEdges.push(next.edge); - beats.push({ - nodeId: node.id, - node, - viaEdge: next.edge, - kind: isContradictionEdge(next.edge) ? 'contradiction' : 'connection', - intensity: isContradictionEdge(next.edge) ? 1 : Math.min(1, 0.55 + (next.edge.weight ?? 0) * 0.45), - }); - current = node.id; - } - - // Closing beat: end on the single most-recent node not already the finale, - // so the tour lands on "where the memory is now". Only if it adds variety. - if (beats.length < maxBeats) { - const last = beats[beats.length - 1].nodeId; - const recent = nodes - .filter((n) => n.id !== last) - .sort((a, b) => recencyOf(b) - recencyOf(a))[0]; - if (recent && !beats.some((b) => b.nodeId === recent.id)) { - beats.push({ nodeId: recent.id, node: recent, viaEdge: null, kind: 'recent', intensity: 0.8 }); - } - } - - return { beats, centerId: startId, pivoted, flowEdges }; -} diff --git a/apps/dashboard/src/lib/graph/cinema/sandbox.ts b/apps/dashboard/src/lib/graph/cinema/sandbox.ts deleted file mode 100644 index fbc7fe1..0000000 --- a/apps/dashboard/src/lib/graph/cinema/sandbox.ts +++ /dev/null @@ -1,283 +0,0 @@ -// Memory Cinema — the isolated WebGPU sandbox. -// -// Boots a SEPARATE WebGPU canvas + scene on Cinema launch. The legacy WebGL -// graph (nebula, grain, every current user's experience) is never touched — -// zero regression by construction. Inside the sandbox: the SemanticComputeStorm -// + selective MRT emissive bloom, driven by the CinemaDirector's beats. -// -// Everything here is dynamically imported (three/webgpu, three/tsl, storm.ts) -// so the heavy WebGPU bundle stays out of the main app. If WebGPU is -// unavailable, isSupported() returns false and the caller falls back to the -// camera-only flythrough on the existing canvas (captions still play). - -import * as THREE from 'three'; -import type { SemanticRole, SemanticComputeStorm } from './storm'; - -// The storm lives at the world origin, permanently. The camera always looks here -// and is clamped to a safe distance band so the subject can never leave frame. -const ORIGIN = new THREE.Vector3(0, 0, 0); -// Keep the camera in a narrow, fairly FAR band so the contained storm always -// sits comfortably small and centered in frame (a closer camera makes the cloud -// fill — and spill past — the edges once the bloom halo is added). -const MIN_CAM_DIST = 30; -const MAX_CAM_DIST = 44; - -export function isWebGPUSupported(): boolean { - return typeof navigator !== 'undefined' && 'gpu' in navigator; -} - -interface SandboxDeps { - WebGPURenderer: new (params: object) => { - init: () => Promise; - setSize: (w: number, h: number) => void; - setPixelRatio: (r: number) => void; - renderAsync: (scene: THREE.Scene, camera: THREE.Camera) => Promise; - computeAsync: (node: unknown) => Promise; - domElement: HTMLCanvasElement; - dispose?: () => void; - }; - PostProcessing: new (renderer: unknown) => { renderAsync: () => Promise; outputNode: unknown }; - StormCtor: typeof SemanticComputeStorm; - tsl: typeof import('three/tsl'); - bloomMod: { bloom: (node: unknown, strength?: number, radius?: number, threshold?: number) => unknown }; -} - -export class CinemaSandbox { - private container: HTMLElement; - private deps!: SandboxDeps; - private renderer!: SandboxDeps['WebGPURenderer']['prototype']; - // Scene/camera are created in boot() from the three/webgpu module so every - // object handed to the WebGPU renderer comes from the SAME Three.js instance - // (avoids the "multiple instances of Three.js" incompatibility — the base - // three import is used only for the shared Vector3 math type the director - // mutates, which is identical across instances). - private scene!: THREE.Scene; - private camera!: THREE.PerspectiveCamera; - private storm!: SemanticComputeStorm; - private post: { renderAsync: () => Promise } | null = null; - private booted = false; - - /** Camera target the director drives; mirrored into camera.lookAt each frame. */ - readonly target = new THREE.Vector3(0, 0, 0); - - // FLYTHROUGH — when >0, relaxes the camera-distance clamp floor so the camera - // can plunge inside the shell, and the storm stretches sprites along the - // apparent motion vector. Camera velocity is derived per-frame from the - // position delta (one Vector3, no compute). 0 = no streak (reduced-motion). - private flythrough = 0; - private prevCamPos = new THREE.Vector3(); - private camVel = new THREE.Vector3(); - - constructor(container: HTMLElement) { - this.container = container; - } - - get cameraRef(): THREE.PerspectiveCamera { - return this.camera; - } - - /** - * Boot the WebGPU pipeline. Throws if WebGPU is unsupported or init fails — - * the caller treats a throw as "fall back to camera-only mode". - */ - async boot(): Promise { - if (this.booted) return; - if (!isWebGPUSupported()) throw new Error('WebGPU not supported'); - - // Dynamic imports keep three/webgpu out of the main bundle. - const webgpu = (await import('three/webgpu')) as unknown as { - WebGPURenderer: SandboxDeps['WebGPURenderer']; - PostProcessing: SandboxDeps['PostProcessing']; - Scene: new () => THREE.Scene; - PerspectiveCamera: new (fov: number, aspect: number, near: number, far: number) => THREE.PerspectiveCamera; - Color: new (hex: number) => THREE.Color; - }; - const tsl = (await import('three/tsl')) as typeof import('three/tsl'); - // bloom() lives in the TSL display helpers; import the node module. - const bloomMod = (await import( - 'three/examples/jsm/tsl/display/BloomNode.js' - )) as unknown as SandboxDeps['bloomMod']; - const { SemanticComputeStorm } = await import('./storm'); - - this.deps = { - WebGPURenderer: webgpu.WebGPURenderer, - PostProcessing: webgpu.PostProcessing, - StormCtor: SemanticComputeStorm, - tsl, - bloomMod, - }; - - // Fail loud if the dynamic import didn't yield the expected constructors, - // instead of a cryptic "undefined is not a constructor" later. - if (!webgpu.WebGPURenderer || !webgpu.Scene || !webgpu.PerspectiveCamera || !webgpu.Color) { - throw new Error('[cinema] three/webgpu is missing expected exports'); - } - - // Build scene + camera from the SAME (webgpu) module instance the - // renderer + storm use, so all objects are instance-compatible. - const w = Math.max(1, this.container.clientWidth); - const h = Math.max(1, this.container.clientHeight); - this.scene = new webgpu.Scene(); - this.scene.background = new webgpu.Color(0x02020a); - this.camera = new webgpu.PerspectiveCamera(60, w / h, 0.1, 2000); - this.camera.position.set(0, 18, 60); - - const renderer = new this.deps.WebGPURenderer({ antialias: true, alpha: false }); - renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); - renderer.setSize(w, h); - // CRITICAL FOOTGUN: WebGPU init is async. Must await before first render - // or the canvas silently draws nothing. - await renderer.init(); - this.container.appendChild(renderer.domElement); - this.renderer = renderer; - - // The compute storm (150k GPU particles). - this.storm = new this.deps.StormCtor(renderer, this.scene, {}); - - // Selective MRT bloom: scene pass emits an emissive MRT; bloom only the - // emissive channel so the storm blazes against the void without washing - // the whole frame to grey. Falls back to a plain pass if MRT setup - // throws on a given driver. - try { - const { pass, mrt, output, emissive } = this.deps.tsl as unknown as { - pass: (s: THREE.Scene, c: THREE.Camera) => { - setMRT: (m: unknown) => void; - getTextureNode: (name: string) => unknown; - }; - mrt: (cfg: Record) => unknown; - output: unknown; - emissive: unknown; - }; - const scenePass = pass(this.scene, this.camera); - if (typeof scenePass?.setMRT !== 'function' || typeof scenePass?.getTextureNode !== 'function') { - throw new Error('three/tsl pass() API mismatch — setMRT/getTextureNode missing'); - } - scenePass.setMRT(mrt({ output, emissive })); - const outputTex = scenePass.getTextureNode('output'); - const emissiveTex = scenePass.getTextureNode('emissive'); - // Gentler bloom (strength 0.6, threshold 0.35) so it accents the bright - // cores instead of washing the whole colored cloud to white. - const bloomed = this.deps.bloomMod.bloom(emissiveTex, 0.6, 0.65, 0.35); - const post = new this.deps.PostProcessing(renderer); - (post as unknown as { outputNode: unknown }).outputNode = ( - outputTex as { add: (n: unknown) => unknown } - ).add(bloomed); - this.post = post as unknown as { renderAsync: () => Promise }; - } catch (e) { - // MRT/bloom unavailable on this driver — render straight, no crash. - console.warn('[cinema] selective bloom unavailable, rendering without MRT:', e); - this.post = null; - } - - this.booted = true; - } - - /** Retarget the storm's MODE/ignition for a beat. The storm is permanently - * centered at the WORLD ORIGIN (see render) so it is always dead-center in - * frame — worldPos here only conveys which node, not where the storm sits. - * `act` lets the storm hold Act I dimmer (it opens too hot otherwise). */ - transitionTo( - role: SemanticRole, - _worldPos: THREE.Vector3, - act: 'I' | 'II' | 'III' = 'II', - beatIndex = 99 - ): void { - if (!this.booted) return; - this.storm.transitionTo(role, ORIGIN, act, beatIndex); - } - - /** Fire one endless-dream beat — a random crazier figure + color blast. Called - * on a timer after the scripted tour ends so the storm never sits idle. */ - dreamBeat(): void { - if (!this.booted) return; - this.storm.dreamBeat(); - } - - /** Flythrough strength 0..1. Relaxes the clamp floor so the camera can dive - * inside the shell, and drives the storm's velocity-stretch streak. Set to 0 - * for reduced-motion (no streak, normal clamp). */ - setFlythrough(s: number): void { - this.flythrough = THREE.MathUtils.clamp(s, 0, 1); - if (this.booted) this.storm.setStreak(s); - } - - /** Pass-through: set the storm streak strength directly. */ - setStreak(s: number): void { - if (this.booted) this.storm.setStreak(s); - } - - /** Pass-through: push view-space camera velocity to the storm. */ - setCameraVel(v: THREE.Vector3): void { - if (this.booted) this.storm.setCameraVel(v); - } - - /** Render one frame. The storm is pinned to the origin and the camera always - * looks at the origin, so the storm CANNOT leave the frame. The director - * varies only the camera's orbital position/angle (set via cameraRef), and we - * clamp that to a safe distance band here as a final guarantee. */ - async render(deltaSeconds: number): Promise { - if (!this.booted) return; - - // Camera velocity (world units / sec) from the position delta this frame — - // one Vector3 subtract, no compute. Captured BEFORE the clamp so it tracks - // the director's intended move (the clamp only rescues runaway distances). - this.camVel.copy(this.camera.position).sub(this.prevCamPos).divideScalar(Math.max(deltaSeconds, 1e-3)); - this.prevCamPos.copy(this.camera.position); - - // Hard guarantee: clamp the camera into a distance band from origin so a - // runaway director move can never push the subject out of view, then look - // dead at the origin where the storm lives. Flythrough relaxes the floor - // toward 6 so the camera can plunge inside the shell; the MAX clamp stands. - const minDist = THREE.MathUtils.lerp(MIN_CAM_DIST, 6, this.flythrough); - const distToOrigin = this.camera.position.length(); - if (distToOrigin < minDist || distToOrigin > MAX_CAM_DIST || !Number.isFinite(distToOrigin)) { - const d = Math.min(MAX_CAM_DIST, Math.max(minDist, distToOrigin || MAX_CAM_DIST)); - if (distToOrigin > 1e-3) this.camera.position.setLength(d); - else this.camera.position.set(0, 12, d); - } - this.camera.lookAt(ORIGIN); - - // Push view-space apparent particle velocity to the storm (negated world - // camera velocity transformed into view space → the direction sprites - // appear to streak). matrixWorldInverse is the previous frame's (the - // renderer refreshes it during renderAsync below) — a one-frame lag that is - // imperceptible for a streak direction. - const camVelView = this.camVel - .clone() - .applyMatrix3(new THREE.Matrix3().setFromMatrix4(this.camera.matrixWorldInverse)) - .negate(); - this.storm.setCameraVel(camVelView); - - // Size the containment sphere to the camera's VERTICAL FOV at the origin - // (the limiting dimension on a landscape frame). 0.82 lets the storm fill - // most of the frame; the storm's internal shell sits well inside this and - // the hard boundary snap keeps the bloom halo from spilling past the edge. - const dist = this.camera.position.length(); - const vfov = (this.camera.fov * Math.PI) / 180; - const fitRadius = Math.tan(vfov / 2) * dist * 0.82; - this.storm.setContainRadius(fitRadius); - - await this.storm.update(deltaSeconds); - if (this.post) await this.post.renderAsync(); - else await this.renderer.renderAsync(this.scene, this.camera); - } - - resize(): void { - if (!this.booted) return; - const w = Math.max(1, this.container.clientWidth); - const h = Math.max(1, this.container.clientHeight); - this.camera.aspect = w / h; - this.camera.updateProjectionMatrix(); - this.renderer.setSize(w, h); - } - - dispose(): void { - if (!this.booted) return; - this.storm?.dispose(); - this.renderer?.dispose?.(); - if (this.renderer?.domElement?.parentNode) { - this.renderer.domElement.parentNode.removeChild(this.renderer.domElement); - } - this.booted = false; - } -} diff --git a/apps/dashboard/src/lib/graph/cinema/storm.ts b/apps/dashboard/src/lib/graph/cinema/storm.ts deleted file mode 100644 index 55caa26..0000000 --- a/apps/dashboard/src/lib/graph/cinema/storm.ts +++ /dev/null @@ -1,1006 +0,0 @@ -// Memory Cinema — the Semantic Compute Storm (WebGPU / TSL GPGPU). -// -// 150k particles whose physics run ENTIRELY on the GPU via Three Shading -// Language compute nodes. The storm shifts behaviour with the narrative beat: -// - origin/anchor → stable orbital swarm around the focused node -// - connection → fluid streaming toward the target with wave motion -// - contradiction → explosive Rössler strange-attractor chaos (crimson) -// Emissive colour is routed so only the storm blazes through the selective -// MRT bloom pass against a clean void. -// -// IMPORTANT — verified against the INSTALLED three@0.172 three/tsl build: -// * use select() (NOT cond — does not exist in this build) -// * use TSL sin()/cos() (NOT Math.sin inside Fn) -// * SpriteNodeMaterial (NOT SpritePointsMaterial) -// * renderer.computeAsync() for the dispatch -// The whole module is dynamically imported only when Cinema launches, so the -// heavy three/webgpu + three/tsl bundles never load for normal dashboard use. -// -// This file is intentionally framework-agnostic and uses `any` for the WebGPU -// renderer type: three/webgpu's WebGPURenderer is a runtime-only dynamic import -// (kept out of the main bundle), so a compile-time type isn't available here. - -import * as THREE from 'three'; -// StorageBufferAttribute + SpriteNodeMaterial live in the three/webgpu entry, -// not the base three module. This file is dynamically imported only at Cinema -// launch, so pulling from three/webgpu here does NOT add WebGPU to the main -// bundle. -import { StorageBufferAttribute, SpriteNodeMaterial } from 'three/webgpu'; -import { - Fn, - storage, - instanceIndex, - vec3, - uniform, - select, - float, - sin, - cos, - length, - clamp, - min, - mix, - fract, - abs, - floor, - smoothstep, - oneMinus, - cross, - sqrt, - pow, - mx_noise_vec3, - vec2, - atan, - positionView, -} from 'three/tsl'; -// note: .max()/.div()/.sub()/.cos()/.sin()/.log()/.lessThanEqual() etc. are -// fluent methods on TSL nodes — no import needed. - -export type SemanticRole = 'anchor' | 'connection' | 'contradiction'; - -const ROLE_MODE: Record = { - anchor: 0, - connection: 1, - contradiction: 2, -}; - -export interface StormOptions { - count?: number; - /** World-space radius of the initial particle cloud. */ - spawnRadius?: number; -} - -/** - * GPU compute particle storm. Construct with a WebGPURenderer + Scene, call - * update(dt) each frame, and transitionTo(role, worldPos) on each narrative - * beat. dispose() releases all GPU resources. - */ -/** The TSL compute node Fn(...)().compute(count) produces. three@0.172 does not - * export a public type for it; it is opaque and only handed to computeAsync(). */ -type ComputeDispatch = ReturnType>['compute']>; - -export class SemanticComputeStorm { - readonly count: number; - private scene: THREE.Scene; - // WebGPURenderer — runtime-only type (dynamic import); see file header. - private renderer: { computeAsync: (node: ComputeDispatch) => Promise }; - - private bufferPos: StorageBufferAttribute | null; - private bufferVel: StorageBufferAttribute | null; - private bufferPhase: StorageBufferAttribute | null; - - // Definite-assigned in buildCompute() (called from the constructor). - private computeNode!: ComputeDispatch; - private mesh: THREE.InstancedMesh | null = null; - private material: THREE.Material | null = null; - - // Serialize GPU compute dispatches: never queue a new compute pass before the - // previous one resolves, or the WebGPU dispatch queue backs up and stalls. - private computeInFlight: Promise | null = null; - - // Uniforms driven from the camera/beat loop. uIgnition starts non-zero so - // the storm is visible on the very first frame (before any beat fires). - private uTarget = uniform(new THREE.Vector3(0, 0, 0)); - private uTime = uniform(0); - private uIgnition = uniform(0.2); - private uMode = uniform(0); - // World-space radius the storm is contained within. Particles past this get - // a spring force back so the storm NEVER flies off-screen. Sized to the - // camera framing by the sandbox via setContainRadius(). - private uContainRadius = uniform(48); - // Global hue rotation (advances over time) + how strongly the beat's mode - // tint overrides the rainbow (0 = full rainbow, 1 = full mode color). - private uHueShift = uniform(0); - private uModeTintAmt = uniform(0.25); - // Detonation cycle: spikes to 1 on each beat (explosion), decays to 0 - // (crystallize/reform). Drives the explode→pixelate→reform look. - private uBurst = uniform(0); - // ACT DIMMER — a master brightness scalar set per beat from the narrative - // act. Act I opens too hot (the cloud is still in its dense initial spawn and - // the first ignition flash stacks on top), so we hold Act I dimmer and let - // Acts II/III blaze at full. 1.0 = full brightness. Starts very low so the - // pre-first-beat / beat-0 boot frames fade in soft instead of flashing white. - private uActDim = uniform(0.12); - // WORLD STATE MACHINE — each narrative beat (1..7) is a UNIQUE visual world: - // 0 nebula mist · 1 orbital anchor · 2 strange attractor · 3 detonation void - // 4 crystal lattice · 5 fluid galaxy · 6 phyllotaxis bloom - // Beats map 1:1 to worlds (beatIndex % 7). The compute kernel builds all 7 - // home targets + forces and select()s the live one — particles are never - // swapped, only the forces acting on them, which IS the journey. - private uWorld = uniform(0); - private uPrevWorld = uniform(0); - // Crossfade prev→current world over ~1s after each beat (eased in update()). - // 1 = fully previous world, 0 = fully current. - private uBlend = uniform(0); - private readonly worldCount = 7; - // COLOR BLAST — a LONG-LIVED chroma envelope, decoupled from the fast physics - // burst so the detonation color OUTLIVES the shockwave (owner: "color too - // brief"). uBlast is the 0..1 magnitude (slow ~2.8s decay); uBlastTime counts - // seconds since the last detonation and drives the outward spectral wave. - private uBlast = uniform(0); - private uBlastTime = uniform(0); - // ENDLESS DREAM MODE — after the scripted 7-beat tour, the storm keeps - // generating crazier figures forever instead of sitting idle. uMorphSeed - // randomizes each procedural figure (worlds 7..11); uChaos ramps 0→1 over the - // dream so every figure is wilder than the last. - private uMorphSeed = uniform(0); - private uChaos = uniform(0); - // JARRING CLASH PAIR — which opposing inner/outer duotone is live (0..4). Set - // per beat so every figure is a fresh ice-vs-fire / acid-vs-blood collision. - private uClash = uniform(0); - // NEAR-PLANE FADE — particles dissolve as they pass very close to the camera - // (flythrough) so they never additive-pop. Distance band in world units. - private uFadeNear = uniform(2.0); - private uFadeBand = uniform(7.0); - // VOLUMETRIC FOG — distant particles dim toward the void with view depth (exp - // falloff) for atmospheric depth. Combined with near-fade in one depth read. - private uFogDensity = uniform(0.012); - // DEPTH OF FIELD — off-focus particles dim (read as bokeh defocus under the - // bloom). Folded into the single depthFade depth read (no sprite-scale, which - // is finicky + collides with the streak). Focus tracks the dive. - private uFocus = uniform(28.0); - private uFocusRange = uniform(20.0); // wider in-focus band → most of figure crisp - private uDofDim = uniform(0.3); // subtle off-focus fade → depth without darkening - // INFINITE DROSTE ZOOM — the spine. The cloud endlessly dives inward: the - // nested inner figure grows by λ each period to become the new outer shell, - // while a fresh inner spawns inside, looping FOREVER with no seam. λ = 1/0.52 - // (the inner scale) makes inner→outer EXACT so the snap is invisible. Pure - // fract(uTime/T) — no camera dolly (can't clip / fight the camera clamp). - private uZoomPeriod = uniform(9.0); // T: one promotion every 9s - private uLambda = uniform(1.923); // 1 / 0.52 self-similar ratio - private uZoomOn = uniform(0); // 0 = off (beats 0/1, reduced-motion), 1 = diving - // VELOCITY-STRETCH FLYTHROUGH STREAK — when the camera plunges through the - // shell, sprites elongate along the screen-space apparent motion vector (a - // motion-streak look). Pure scaleNode/rotationNode (a SEPARATE output graph - // from color/emissive) + camera-velocity uniforms → zero per-frame compute, - // no positionView read in color/emissive. Strength is gated to 0 from JS for - // reduced-motion, so this is a no-op until the director drives uStreak. - private uCamVelView = uniform(new THREE.Vector3(0, 0, 0)); // view-space apparent particle velocity - private uStreak = uniform(0); // 0..1 flythrough strength - private uMaxStretch = uniform(7.0); - // JS-side dream state (not uniforms): which figure is live + how many fired. - private dreamCount = 0; - - constructor( - renderer: { computeAsync: (node: ComputeDispatch) => Promise }, - scene: THREE.Scene, - opts: StormOptions = {} - ) { - this.renderer = renderer; - this.scene = scene; - this.count = opts.count ?? 150_000; - // Spawn particles ALREADY SPREAD across a wide spherical SHELL (not a tiny - // dense ball at the origin). The old ±8 cube packed all 150k into a tiny - // volume, so the very first frame (Beat 0, before the cloud expands to its - // rim-falloff homes) was a solid white blob — additive overlap dominates at - // high density regardless of per-particle dimming. Booting on a broad shell - // means the storm reads as a calm colored cloud from frame one. - const spawn = opts.spawnRadius ?? 34; - - const positions = new Float32Array(this.count * 3); - const velocities = new Float32Array(this.count * 3); - const phases = new Float32Array(this.count); - for (let i = 0; i < this.count; i++) { - // Uniform direction on a sphere, radius biased to the outer shell so the - // boot cloud is hollow-cored like the rim look (never a dense center). - const u1 = Math.random(); - const u2 = Math.random(); - const theta = u1 * Math.PI * 2; - const z = u2 * 2 - 1; - const r = Math.sqrt(Math.max(0, 1 - z * z)); - const rad = spawn * (0.55 + Math.random() * 0.45); // shell 0.55..1.0 - positions[i * 3] = Math.cos(theta) * r * rad; - positions[i * 3 + 1] = z * rad; - positions[i * 3 + 2] = Math.sin(theta) * r * rad; - phases[i] = Math.random() * Math.PI * 2; - } - const bufferPos = new StorageBufferAttribute(positions, 3); - const bufferVel = new StorageBufferAttribute(velocities, 3); - const bufferPhase = new StorageBufferAttribute(phases, 1); - this.bufferPos = bufferPos; - this.bufferVel = bufferVel; - this.bufferPhase = bufferPhase; - - this.buildCompute(bufferPos, bufferVel, bufferPhase); - this.buildRender(bufferPos, bufferPhase); - } - - private buildCompute( - bufferPos: StorageBufferAttribute, - bufferVel: StorageBufferAttribute, - bufferPhase: StorageBufferAttribute - ): void { - const posStore = storage(bufferPos, 'vec3', this.count); - const velStore = storage(bufferVel, 'vec3', this.count); - const phaseStore = storage(bufferPhase, 'float', this.count); - - this.computeNode = Fn(() => { - const pos = posStore.element(instanceIndex); - const vel = velStore.element(instanceIndex); - const phase = phaseStore.element(instanceIndex); - - // ── DETERMINISTIC PER-PARTICLE BASIS (phase → stable spherical coords) ── - const a1 = phase.mul(12.9898).sin().mul(43758.5453); - const a2 = phase.mul(78.233).sin().mul(12543.531); - const a3 = phase.mul(39.346).sin().mul(24634.633); - const u = fract(a1); // 0..1 - const v = fract(a2); // 0..1 - const w2 = fract(a3); // 0..1 - const theta = u.mul(6.28318); // azimuth 0..2π - const phi = v.mul(3.14159); // polar 0..π - const R = this.uContainRadius; - // Outer-shell bias (0.62..1.0) keeps the core hollow → reads as color, - // not a white-blooming dense center. (The dialed-in anti-white-out.) - const shellT = fract(phase.mul(3.7)); - const homeFrac = float(0.62).add(shellT.mul(shellT).mul(0.38)); - const fi = float(instanceIndex); // particle index as float (phyllotaxis) - - // ── CURL NOISE (divergence-free flow → worlds 0 nebula, 5 fluid) ── - // Never clumps, never stops; the signature "living smoke" motion. - const curl = Fn(([p]: [ReturnType]) => { - const e = float(0.6); - const dx = mx_noise_vec3(p.add(vec3(e, 0, 0))).sub(mx_noise_vec3(p.sub(vec3(e, 0, 0)))); - const dy = mx_noise_vec3(p.add(vec3(0, e, 0))).sub(mx_noise_vec3(p.sub(vec3(0, e, 0)))); - const dz = mx_noise_vec3(p.add(vec3(0, 0, e))).sub(mx_noise_vec3(p.sub(vec3(0, 0, e)))); - return vec3(dy.z.sub(dz.y), dz.x.sub(dx.z), dx.y.sub(dy.x)).normalize(); - }); - - // ── 7 WORLD HOME TARGETS (all centered on origin → centroid can't drift) ── - const sphereShell = vec3(sin(phi).mul(cos(theta)), cos(phi), sin(phi).mul(sin(theta))); - const wNebula = sphereShell.mul(R.mul(homeFrac)); // world 0 (and 3 base) - const wAnchor = sphereShell.mul(R.mul(float(0.5).add(shellT.mul(0.3)))); // world 1 - // world 2 attractor: home is "ahead" along the Thomas flow from current pos. - const bT = float(0.19); - const thomas = vec3( - sin(pos.y).sub(pos.x.mul(bT)), - sin(pos.z).sub(pos.y.mul(bT)), - sin(pos.x).sub(pos.z.mul(bT)) - ); - const wAttractor = pos.add(thomas.mul(R.mul(0.12))); - const wVoid = wNebula; // world 3 = sphere; the burst dominates this beat - const wCrystal = vec3( // world 4 cube lattice - u.sub(0.5).mul(2).mul(R.mul(0.8)), - v.sub(0.5).mul(2).mul(R.mul(0.8)), - w2.sub(0.5).mul(2).mul(R.mul(0.8)) - ); - const armAng = u.mul(6.28318).mul(3).add(w2.mul(0.6)); // world 5 galaxy spiral - const gr = R.mul(0.2).add(R.mul(0.8).mul(w2)); - const wGalaxy = vec3( - gr.mul(cos(armAng)), - R.mul(0.06).mul(sin(phase.mul(20))), - gr.mul(sin(armAng)) - ); - const golden = float(2.39996323); // world 6 phyllotaxis (Vogel sunflower) - const pAng = fi.mul(golden); - const pRad = sqrt(fi).mul(R.mul(0.0042)); // ~R at 150k particles - const wPhyllo = vec3(pAng.cos().mul(pRad), R.mul(0.04).mul(sin(phase.mul(9))), pAng.sin().mul(pRad)); - - // ══════════════════════════════════════════════════════════════════ - // ENDLESS DREAM FIGURES (worlds 7..11) — the generative mode that - // kicks in after the scripted 7-beat tour. These are PROCEDURAL and - // RANDOMIZED: uMorphSeed (set per auto-beat) + uChaos (ramps up over - // time → each figure crazier than the last) modulate the parameters, - // so the same world index never looks the same twice. - // ══════════════════════════════════════════════════════════════════ - const seed = this.uMorphSeed; - const chaos = this.uChaos; - // seeded per-figure scalars (deterministic hash of the seed) - const s1 = fract(seed.mul(0.731).add(0.13)); - const s2 = fract(seed.mul(1.323).add(0.51)); - const s3 = fract(seed.mul(2.117).add(0.27)); - - // ── (u,v) MANIFOLD GRID ── THE spaghetti→skin fix. The hash-scatter - // basis (u,v,w2) is white noise → reads as gas/strings. A deterministic - // tensor grid over instanceIndex makes neighbors share rows/cols, so the - // procedural forms below render as a continuous SCULPTED SKIN, not lines. - // 387² = 149769 ≈ 150k. Pure arithmetic on fi — no buffers, no indexing. - const GW = float(387); - const ug = fract(fi.div(GW)); // grid u 0..1 (across a row) - const vg = floor(fi.div(GW)).div(GW); // grid v 0..1 (down columns) - - // ── COMPLEX-MATH HELPERS (sinh/cosh/tanh are NOT in three@0.172 — expand - // via the confirmed .exp()). Used by the Calabi–Yau + Boy's surface forms. ── - type FNode = ReturnType; - type VNode = ReturnType; - const sinhT = (x: FNode) => x.exp().sub(x.mul(-1).exp()).mul(0.5); - const coshT = (x: FNode) => x.exp().add(x.mul(-1).exp()).mul(0.5); - const cMul = (a: VNode, b: VNode) => - vec2(a.x.mul(b.x).sub(a.y.mul(b.y)), a.x.mul(b.y).add(a.y.mul(b.x))); - const cExp = (a: VNode) => { - const e = a.x.exp(); - return vec2(e.mul(cos(a.y)), e.mul(sin(a.y))); - }; - const cLog = (a: VNode) => - vec2(a.x.mul(a.x).add(a.y.mul(a.y)).max(1e-12).log().mul(0.5), atan(a.y, a.x)); - const cPow = (a: VNode, p: FNode) => cExp(cMul(vec2(p, float(0)), cLog(a))); - const cCosh = (z: VNode) => vec2(coshT(z.x).mul(cos(z.y)), sinhT(z.x).mul(sin(z.y))); - const cSinh = (z: VNode) => vec2(sinhT(z.x).mul(cos(z.y)), coshT(z.x).mul(sin(z.y))); - const cInv = (a: VNode) => { - const dd = a.x.mul(a.x).add(a.y.mul(a.y)).max(1e-6); - return vec2(a.x.div(dd), a.y.mul(-1).div(dd)); - }; - - // world 7 · SUPERSHAPE (3D superformula — petals/stars/blobs, never same) - const m1 = float(2).add(floor(s1.mul(14))); // symmetry 2..15 - const sfAng = theta; - const sfR1 = pow(abs(cos(m1.mul(sfAng).div(4))), float(2).add(s2.mul(8))) - .add(pow(abs(sin(m1.mul(sfAng).div(4))), float(2).add(s3.mul(8)))) - .add(0.0001) - .pow(float(-0.5)); - const sfR2 = pow(abs(cos(m1.mul(phi).div(4))), float(3)) - .add(pow(abs(sin(m1.mul(phi).div(4))), float(3))) - .add(0.0001) - .pow(float(-0.5)); - const sfRad = R.mul(0.85).mul(clamp(sfR1.mul(sfR2).mul(0.5), 0.1, 1.4)); - const wSuper = vec3( - sin(phi).mul(cos(theta)).mul(sfRad), - cos(phi).mul(sfRad), - sin(phi).mul(sin(theta)).mul(sfRad) - ); - - // ══════ IMPOSSIBLE-GEOMETRY FORM PACK (worlds 8..11) ══════ - // Brand-new signature skins nobody ships as a living particle figure. - - // world 8 · CALABI–YAU quintic cross-section (6D string-theory manifold, - // Hanson 4D→3D projection). 25 interlocking petals; α rotates it THROUGH - // the 4th dimension so petals pass through each other. The trophy. - const nCY = float(5); - const patch = floor(fract(seed.mul(0.013).add(fi.mul(0.00667))).mul(25)); - const k1 = floor(patch.div(5)); // 0..4 - const k2 = patch.sub(k1.mul(5)); // 0..4 - const cyx = ug.mul(2).sub(1); // x ∈ [-1,1] - const cyy = vg.mul(1.5708); // y ∈ [0, π/2] - const zc = vec2(cyx, cyy); - const e1 = cExp(vec2(float(0), k1.mul(6.28318).div(nCY))); - const e2 = cExp(vec2(float(0), k2.mul(6.28318).div(nCY))); - const z1 = cMul(e1, cPow(cCosh(zc), float(0.4))); // 2/n = 0.4 - const z2 = cMul(e2, cPow(cSinh(zc), float(0.4))); - const alpha = this.uTime.mul(0.25).add(seed).add(chaos.mul(1.5)); - const wKnot = vec3( - z1.x, - z2.x, - cos(alpha).mul(z1.y).add(sin(alpha).mul(z2.y)) - ).mul(R.mul(0.55)); // ±1.6 → ~0.88R, centroid (0,0,0) - - // world 9 · BOY'S SURFACE (Bryant–Kusner minimal immersion of RP²) — a - // CLOSED non-orientable surface with one triple point, no spikes. Pure - // rational complex arithmetic over the unit disk → a perfect 2-manifold. - const br = sqrt(ug); // sqrt → uniform area on the disk - const bth = vg.mul(6.28318); - const zb = vec2(br.mul(cos(bth)), br.mul(sin(bth))); - const zb2 = cMul(zb, zb); - const zb3 = cMul(zb2, zb); - const zi2 = cInv(zb2); - const zi3 = cInv(zb3); - const denom = vec2(zb3.x.sub(zi3.x).add(2.2360679), zb3.y.sub(zi3.y)); // +√5 - const aZ = cInv(denom); - const V0 = cMul(vec2(float(0), float(1)), vec2(zb2.x.sub(zi2.x), zb2.y.sub(zi2.y))); - const V1 = vec2(zb2.x.add(zi2.x), zb2.y.add(zi2.y)); - const V2 = cMul(vec2(float(0), float(0.6667)), vec2(zb3.x.add(zi3.x), zb3.y.add(zi3.y))); - const Mx = cMul(aZ, V0).x; - const My = cMul(aZ, V1).x; - const Mz = cMul(aZ, V2).x.add(0.5); - const m2 = Mx.mul(Mx).add(My.mul(My)).add(Mz.mul(Mz)).max(1e-4); // sphere inversion - const wLissa = vec3(Mx.div(m2), My.div(m2), Mz.div(m2).sub(0.86)) // sub centroid z - .mul(R.mul(0.5)); - - // world 10 · AIZAWA attractor SHELL (capped spiral torus mapped over u,v; - // the Aizawa vector field added in the motion modifiers makes it breathe). - const az = vg.mul(2).sub(1); // -1..1 vertical - const ar = sqrt(float(1).sub(az.mul(az)).max(0)).mul(0.9).add(0.25); // radial profile - const aang = ug.mul(6.28318).add(az.mul(6).mul(chaos.add(0.4))); // spiral twist - const wHelix = vec3( - ar.mul(cos(aang)), - az.mul(1.4), // centered by construction - ar.mul(sin(aang)) - ).mul(R.mul(0.5)); - - // world 11 · GYROID↔SCHWARZ-D Bonnet morph (triply-periodic minimal - // surface — alien coral/bone lattice). The Bonnet angle θ continuously - // BENDS the gyroid into Schwarz-D. A woven solid skin, never seen living. - const period = float(2.2).add(chaos.mul(2.0)); - const gx = ug.mul(6.28318).mul(period); - const gy = vg.mul(6.28318).mul(period); - const gz = this.uTime.mul(0.3).add(seed.mul(6.28318)); - const gyroid = sin(gx).mul(cos(gy)).add(sin(gy).mul(cos(gz))).add(sin(gz).mul(cos(gx))); - const schwD = cos(gx).mul(cos(gy)).mul(cos(gz)).sub(sin(gx).mul(sin(gy)).mul(sin(gz))); - const bonnet = this.uTime.mul(0.15); - const fTPMS = cos(bonnet).mul(gyroid).add(sin(bonnet).mul(schwD)); - const tpmsBase = vec3( - sin(vg.mul(3.14159)).mul(cos(ug.mul(6.28318))), - cos(vg.mul(3.14159)), - sin(vg.mul(3.14159)).mul(sin(ug.mul(6.28318))) - ); - const wFoam = tpmsBase.mul(R.mul(0.5).add(fTPMS.mul(R.mul(0.12)))); // skin ± displacement - - // select() chain — no dynamic indexing in this TSL build. - const homeFor = (idx: ReturnType) => - select(idx.equal(0), wNebula, - select(idx.equal(1), wAnchor, - select(idx.equal(2), wAttractor, - select(idx.equal(3), wVoid, - select(idx.equal(4), wCrystal, - select(idx.equal(5), wGalaxy, - select(idx.equal(6), wPhyllo, - select(idx.equal(7), wSuper, - select(idx.equal(8), wKnot, - select(idx.equal(9), wLissa, - select(idx.equal(10), wHelix, wFoam))))))))))); - const homeCur = homeFor(float(this.uWorld)); - const homePrev = homeFor(float(this.uPrevWorld)); - // uBlend eases prev→cur (smoothstep) so the world morph is silky. - const blendE = smoothstep(float(0), float(1), oneMinus(this.uBlend)); - const outerHome = mix(homePrev, homeCur, blendE); - - // ══════════════════════════════════════════════════════════════════ - // 3D-WITHIN-3D — a NESTED INNER FIGURE at the core. - // ~33% of particles (a deterministic slice of the index) form a - // SECOND, smaller shape inside the outer shell — a different world, - // counter-rotating, at ~38% scale. This fills the formerly-blank-bright - // center with intentional structure (a figure inside a figure) and adds - // depth nobody ships with particles. The inner world is offset from the - // outer so the two layers never collapse into the same shape. - // ══════════════════════════════════════════════════════════════════ - const isInner = fract(fi.mul(0.001).add(0.5)).greaterThan(0.66); // ~34% inner - // Inner world = outer + 5, wrapped into 0..11 (a clearly different shape). - // Done with select() (no .mod()) so it's valid in this TSL build. - const innerSum = float(this.uWorld).add(5); - const innerWorldIdx = select(innerSum.greaterThan(11), innerSum.sub(12), innerSum); - const innerRaw = homeFor(innerWorldIdx); - // Counter-rotate the inner figure about Y so it spins against the shell, - // and scale it down to sit inside. cos/sin build a Y-rotation matrix. - const ia = this.uTime.mul(0.4); - const ic = cos(ia); - const is = sin(ia); - const innerRot = vec3( - innerRaw.x.mul(ic).sub(innerRaw.z.mul(is)), - innerRaw.y, - innerRaw.x.mul(is).add(innerRaw.z.mul(ic)) - ); - const innerHome = innerRot.mul(0.52); // nested core at ~52% scale (spread → less white) - - // ── INFINITE ZOOM DIVE ── two layers ride offset phases of fract(uTime/T): - // the outer grows pow(λ, phase) toward the camera then snaps back (invisible - // because inner@1/λ == outer@1); the inner (half-period offset) grows from - // its own base, promoted by λ so it becomes the next outer shell. uZoomOn - // gates it → beats 0/1 + reduced-motion stay perfectly still. - const zPhaseO = this.uTime.div(this.uZoomPeriod).fract(); - const zPhaseI = this.uTime.div(this.uZoomPeriod).add(0.5).fract(); - const zoomO = mix(float(1.0), this.uLambda.pow(zPhaseO), this.uZoomOn); - const zoomI = mix(float(1.0), this.uLambda.pow(zPhaseI), this.uZoomOn); - const outerDive = outerHome.mul(zoomO); - const innerDive = innerHome.mul(zoomI.mul(this.uLambda)); // inner promoted by λ each cycle - // Each particle is permanently outer OR inner (no flicker): pick its home. - const home = mix(outerDive, innerDive, isInner.select(float(1), float(0))); - - // ── DETONATION: per-particle staggered radial blast so it blooms as a - // shockwave, not all-at-once. uBurst spikes on each beat, decays fast. - const outDir = pos.normalize(); - const stagger = oneMinus(fract(phase.mul(7.3)).mul(0.4)); - vel.addAssign(outDir.mul(this.uBurst.mul(0.95).mul(stagger))); - - // ── REFORM SPRING toward the (blended) world home ── - vel.addAssign(home.sub(pos).mul(0.045)); - - // ── PER-WORLD MOTION MODIFIERS (added to the spring) ── - // worlds 0 & 5: curl turbulence (living mist / liquid arms) - const curlV = curl(pos.mul(0.045).add(vec3(0, this.uTime.mul(0.2), 0))); - const curlAmt = select(this.uWorld.equal(0), float(0.05), - select(this.uWorld.equal(5), float(0.06), float(0.0))); - vel.addAssign(curlV.mul(curlAmt)); - // world 1: orbital spin around Y (cross product → orbit, not collapse) - vel.addAssign(cross(vec3(0, 1, 0), pos).mul(0.0009).mul(select(this.uWorld.equal(1), float(1), float(0)))); - // world 2: integrate the Thomas attractor (chaos lattice) - vel.addAssign(thomas.mul(0.012).mul(select(this.uWorld.equal(2), float(1), float(0)))); - // world 10: integrate the AIZAWA vector field so the shell breathes/spirals - // along the real attractor flow (not a static torus). - const azx = pos.x.div(R.mul(0.5)); - const azy = pos.y.div(R.mul(0.5)); - const azz = pos.z.div(R.mul(0.5)); - const aizawa = vec3( - azz.sub(0.7).mul(azx).sub(azy.mul(3.5)), - azx.mul(3.5).add(azz.sub(0.7).mul(azy)), - float(0.6).add(azz.mul(0.95)).sub(azz.mul(azz).mul(azz).div(3)).sub(azx.mul(azx).add(azy.mul(azy))) - ); - vel.addAssign(aizawa.mul(0.008).mul(select(this.uWorld.equal(10), float(1), float(0)))); - // world 5: tangential swirl for liquid galaxy arms - vel.addAssign(cross(vec3(0, 1, 0), pos).mul(0.0016).mul(select(this.uWorld.equal(5), float(1), float(0)))); - - // Subtle living shimmer (mean-zero, no net drift). - const shimmer = home.normalize().mul(sin(this.uTime.mul(1.3).add(phase.mul(6.1))).mul(0.015)); - vel.addAssign(shimmer); - - // Hard velocity clamp — nothing can ever fly off or blow up. - const speed = length(vel); - const maxSpeed = float(1.3); - vel.assign(vel.mul(min(maxSpeed, speed).div(speed.max(0.0001)))); - - pos.addAssign(vel); - vel.mulAssign(0.9); // strong damping → crisp crystallization, no overshoot - - // ── PIXELATION: voxel snap as particles crystallize (low burst). World 4 - // (crystal lattice) pushes it hardest for the holographic shard look. - const crystalBoost = select(this.uWorld.equal(4), float(1.6), float(1.0)); - const cell = mix(float(0.55), float(6.0), clamp(this.uBurst, 0, 1)); - const quantized = floor(pos.div(cell)).add(0.5).mul(cell); - const pixelAmt = clamp(oneMinus(this.uBurst.mul(1.4)), 0, 0.9).mul(crystalBoost).min(0.9); - pos.assign(mix(pos, quantized, pixelAmt)); - - // Final hard safety net: clamp anything past the contain radius back - // onto the boundary shell — guarantees nothing is ever off-screen. - const finalDist = length(pos); - const hardR = this.uContainRadius; - const snapped = pos.normalize().mul(hardR); - pos.assign(mix(pos, snapped, finalDist.greaterThan(hardR).select(float(1), float(0)))); - })().compute(this.count); - } - - private buildRender(bufferPos: StorageBufferAttribute, bufferPhase: StorageBufferAttribute): void { - // SpriteNodeMaterial: emissive routed to bloom; additive against the void. - const mat = new SpriteNodeMaterial({ - transparent: true, - blending: THREE.AdditiveBlending, - depthWrite: false, - }) as SpriteNodeMaterial & { - positionNode: unknown; - colorNode: unknown; - emissiveNode: unknown; - }; - - // CANONICAL SPRITE CENTER: positionNode is the sprite's CENTER only. - // SpriteNodeMaterial.setupPositionView already builds the billboard quad - // from positionGeometry.xy (scaled by scaleNode, rotated by rotationNode), - // so the previous `.add(positionLocal)` double-counted the quad (harmless at - // the 0.1 size, sub-pixel). Using the bare center is required for the - // velocity-stretch streak (scaleNode/rotationNode now drive the quad shape). - const phaseStore = storage(bufferPhase, 'float', this.count); - const instancePos = storage(bufferPos, 'vec3', this.count).element(instanceIndex); - mat.positionNode = instancePos; - - // ── VELOCITY-STRETCH STREAK (scaleNode + rotationNode) ── a SEPARATE output - // graph from color/emissive: it reads only camera-velocity uniforms + - // phaseStore, NEVER positionView (a 2nd positionView read in a material - // output would stack-overflow three@0.172's node builder). Zero per-frame - // compute — the camera velocity is one uniform pushed from the sandbox. - const matStretch = mat as typeof mat & { scaleNode: unknown; rotationNode: unknown }; - { - // Screen-plane apparent particle velocity (view space x/y). speed≥ε so - // length()/atan() stay finite when the camera is still. - const velView = vec3(this.uCamVelView); - const vScreen = vec2(velView.x, velView.y); - const speed = length(vScreen).max(1e-4); - // Per-particle jitter (0.75..1.25) so streaks don't all stretch identically. - const jit = fract(phaseStore.element(instanceIndex).mul(13.17)).mul(0.5).add(0.75); - // Orient the quad's long axis along the motion vector. atan(y, x) is the - // 2-arg form (full -π..π range), NOT atan2. - matStretch.rotationNode = atan(vScreen.y, vScreen.x); - // X-stretch: 1 (no streak) → uMaxStretch, scaled by speed × strength × jitter. - const stretch = clamp(speed.mul(this.uStreak).mul(jit).mul(0.6).add(1.0), float(1.0), this.uMaxStretch); - // bloat = DOF circle-of-confusion base (DOF brightness is in depthFade; - // the sprite-scale bloat is the neutral 1.0 here). Streak elongates X only; - // bloat stays on both axes so the DOF circle is preserved. - const bloat = float(1.0); - matStretch.scaleNode = vec2(bloat.mul(stretch), bloat); - } - - // ── SHARED RAINBOW COLOR ── - // One Fn produces the pure iridescent color for a particle; we feed it to - // BOTH colorNode (the lit/additive surface color) AND emissiveNode (the - // channel the selective MRT bloom reads). The original code only set - // colorNode, so the bloom had NO color to bloom — it washed the frame to - // white. Routing the SAME rainbow to emissive makes the bloom glow in full - // spectral color, which is the whole point. - - // ── IQ COSINE PALETTE ── one scalar t → smooth, vivid, loopable color. - // color(t) = a + b·cos(2π·(c·t + d)). The workhorse for per-world palettes - // and the spectral dispersion wave. - const palette = Fn( - ([t, a, b, c, d]: [ - ReturnType, - ReturnType, - ReturnType, - ReturnType, - ReturnType, - ]) => a.add(b.mul(cos(c.mul(t).add(d).mul(6.28318)))) - ); - - // ── BLACKBODY K→RGB ── real plasma-cooling color (Tanner-Helland approx). - // Drives the detonation: blue-white core (hot) cooling to red embers as the - // blast decays. if/else collapsed to select() for this TSL build. - const blackbody = Fn(([kelvin]: [ReturnType]) => { - const k = kelvin.div(100.0); - const rHot = pow(k.sub(60.0).max(0.0001), float(-0.1332047592)).mul(329.698727446); - const r = k.lessThanEqual(66.0).select(float(255.0), rHot); - const gCool = k.max(0.0001).log().mul(99.4708025861).sub(161.1195681661); - const gHot = pow(k.sub(60.0).max(0.0001), float(-0.0755148492)).mul(288.1221695283); - const g = k.lessThanEqual(66.0).select(gCool, gHot); - const bMid = k.sub(10.0).max(0.0001).log().mul(138.5177312231).sub(305.0447927307); - const b = k.greaterThanEqual(66.0).select( - float(255.0), - k.lessThanEqual(19.0).select(float(0.0), bMid) - ); - return clamp(vec3(r, g, b).div(255.0), 0, 1); - }); - - const rainbowColor = Fn(() => { - const pos = instancePos; - const ph = phaseStore.element(instanceIndex); - const radius = length(pos.sub(vec3(this.uTarget))); - - // Recompute the inner/outer layer split (same formula as the compute kernel). - const fiC = float(instanceIndex); - const isInnerC = fract(fiC.mul(0.001).add(0.5)).greaterThan(0.66); - - // A flowing texture coordinate per particle — drives gradients WITHIN each - // layer's duotone so it shimmers, but stays inside that layer's color world. - const spatialBand = pos.x.mul(0.03).add(pos.y.mul(0.021)).add(pos.z.mul(0.027)); - const flow = fract( - ph.mul(0.41).add(radius.mul(0.06)).add(spatialBand).add(this.uTime.mul(0.10)).add(this.uHueShift) - ); - - // ══════════════════════════════════════════════════════════════════ - // JARRING DUOTONE CLASH — the share hook. - // The outer shell and the inner nested figure are painted from OPPOSING - // color universes (ice vs fire, acid vs blood, gold vs violet…). Not a - // hue shift in one rainbow — two palettes that FIGHT. uClash (set per - // beat) picks which clashing pair is live, so it's a fresh jarring combo - // every beat. Each layer is a 2-color gradient (cold→cold, hot→hot) so - // the layer reads as ONE color world, and the two worlds collide at the - // boundary. THIS is what makes someone stop scrolling and share. - // ══════════════════════════════════════════════════════════════════ - // Five hand-picked clash pairs: [outerA, outerB, innerA, innerB]. - const cl = this.uClash; // 0..4, set per beat - // outer gradient endpoints - const outA = select(cl.equal(0), vec3(0.0, 0.85, 1.0), // ICE: electric cyan - select(cl.equal(1), vec3(0.55, 1.0, 0.0), // ACID lime - select(cl.equal(2), vec3(1.0, 0.82, 0.0), // GOLD - select(cl.equal(3), vec3(0.0, 1.0, 0.6), // MINT/emerald - vec3(0.1, 0.5, 1.0))))); // ELECTRIC blue - const outB = select(cl.equal(0), vec3(0.3, 0.2, 1.0), // ICE→deep indigo - select(cl.equal(1), vec3(0.0, 0.7, 0.5), // ACID→teal - select(cl.equal(2), vec3(1.0, 0.4, 0.0), // GOLD→amber - select(cl.equal(3), vec3(0.0, 0.6, 1.0), // MINT→cyan - vec3(0.5, 0.0, 1.0))))); // ELECTRIC→violet - // inner gradient endpoints — the OPPOSING world - const inA = select(cl.equal(0), vec3(1.0, 0.25, 0.0), // FIRE: molten orange - select(cl.equal(1), vec3(1.0, 0.0, 0.55), // BLOOD: hot pink - select(cl.equal(2), vec3(0.6, 0.0, 1.0), // VIOLET - select(cl.equal(3), vec3(1.0, 0.1, 0.3), // CRIMSON - vec3(1.0, 0.7, 0.0))))); // GOLD - const inB = select(cl.equal(0), vec3(1.0, 0.0, 0.3), // FIRE→crimson - select(cl.equal(1), vec3(1.0, 0.45, 0.0), // BLOOD→orange - select(cl.equal(2), vec3(1.0, 0.0, 0.7), // VIOLET→magenta - select(cl.equal(3), vec3(1.0, 0.5, 0.0), // CRIMSON→amber - vec3(1.0, 0.2, 0.4))))); // GOLD→rose - - // Each layer = a 2-stop gradient driven by `flow` (stays in its world). - const grad = smoothstep(float(0.0), float(1.0), flow); - const outerColor = mix(outA, outB, grad); - const innerColor = mix(inA, inB, grad); - // Hard pick by layer → the clash is absolute at the boundary. - const rainbow = mix(outerColor, innerColor, isInnerC.select(float(1), float(0))); - - // Beat mode tint kept very light so it never muddies the clash. - const modeTint = select( - this.uMode.equal(2), - vec3(1.0, 0.08, 0.32), - select(this.uMode.equal(3), vec3(1.0, 0.78, 0.1), vec3(0.1, 0.9, 1.0)) - ); - return mix(rainbow, modeTint, this.uModeTintAmt.mul(0.4)); - }); - - // ── RIM GLOW ── THE look: bright glowing EDGES, dim center. - // The dense middle of each form (particles near the center axis, all - // stacking toward the camera) is what blooms to white. So we DIM the core - // and BLAZE the rim: brightness rises with a particle's radial distance - // from the form's center. Near center → ~0.12 (deep, calm), at the outer - // shell → ~1.0 (full blaze). The result is the glowing-shell / hollow-eye - // torus look — luminous silhouette, serene dark center. - const rimFactor = Fn(() => { - const pos = instancePos; - // Normalized radial position 0 (center) .. 1 (contain radius). - const rNorm = clamp(length(pos).div(this.uContainRadius.max(0.0001)), 0, 1); - // Smooth ramp: dark core, bright rim — the outer-shell glow that keeps the - // center from blooming white (preserved white-out protection). - const edge = rNorm.mul(rNorm); - // FACING-RATIO FRESNEL — the "make it solid" amplifier. Approximate each - // particle's surface normal as its outward radial direction; view ≈ +Z. - // pow(1−|n·v|, 4) blazes the turning-away SILHOUETTE and quiets the front, - // which flips "glowing fog" into a lit, sculpted SKIN. - const nrm = pos.normalize(); - const fres = pow(oneMinus(abs(nrm.z)), float(4.0)); - const outerRim = float(0.12).add(edge.mul(0.6)).add(fres.mul(0.5)); - // The NESTED inner figure lives at small radius where `edge` is ~0 → it - // would be invisible. Give inner particles their OWN brightness: a higher - // floor + the same Fresnel silhouette so the inner figure reads as its own - // glowing sculpted object floating inside the shell. - const fiC = float(instanceIndex); - const isInnerC = fract(fiC.mul(0.001).add(0.5)).greaterThan(0.66); - // Inner glow kept VERY low so the nested figure's CLASH COLOR survives as - // color, not white. Dense small-radius overlap blows to white fast and - // kills the contrast — so the inner sits dim and saturated, carried by its - // Fresnel silhouette. This is what makes the jarring inner/outer clash read. - const innerRim = float(0.07).add(fres.mul(0.3)); - const baseRim = isInnerC.select(innerRim, outerRim); - - // ── ZOOM SEAM CROSS-FADE ── each dive layer must be fully transparent - // exactly when it snaps (phase 0/1), so the infinite loop has NO visible - // pop. sin(phase·π) = 0 at the snap, 1 mid-cycle. Normalize by the sum so - // total opacity stays ≈1. uZoomOn=0 forces weight to 1 (no fade when still). - const pO = this.uTime.div(this.uZoomPeriod).fract(); - const pI = this.uTime.div(this.uZoomPeriod).add(0.5).fract(); - const wO = sin(pO.mul(3.14159)); - const wI = sin(pI.mul(3.14159)); - const wSum = wO.add(wI).max(0.0001); - const seamO = mix(float(1.0), wO.div(wSum).mul(2.0).min(1.0), this.uZoomOn); - const seamI = mix(float(1.0), wI.div(wSum).mul(2.0).min(1.0), this.uZoomOn); - const seam = isInnerC.select(seamI, seamO); - return baseRim.mul(seam); - }); - - // ── DEPTH FADE (near dissolve × volumetric fog) ── ONE Fn, ONE positionView - // read. Reading positionView from a SECOND Fn in the same material output - // triggers a cyclic type-resolution stack-overflow in three@0.172's node - // builder, so near-fade AND fog are combined here into a single depth read. - // near: smoothstep 0→1 across [near, near+band] (≈1 at far camera → no - // change until we fly inside; dissolves sprites at the camera). - // fog: exp falloff dims distant particles toward the void → 3D depth. - const depthFade = Fn(() => { - const d = positionView.z.negate(); // +forward view distance, read ONCE - const near = smoothstep(this.uFadeNear, this.uFadeNear.add(this.uFadeBand), d); - // Fog floor raised to 0.45 so the far field still READS — all four depth - // systems multiply, so each must dim gently or they stack to black. - const fog = clamp(this.uFogDensity.mul(d).negate().exp(), 0.45, 1.0); - // DOF: dim particles off the focus plane (defocus → bokeh under bloom). - // coc 0 at focus → 1 fully out of focus; brightness 1 → (1-uDofDim). - const coc = clamp(d.sub(this.uFocus).abs().div(this.uFocusRange), 0, 1); - const focusBright = oneMinus(coc.mul(this.uDofDim)); - return near.mul(fog).mul(focusBright); - }); - - // ── THE COLOR BLAST ── the signature detonation chroma. Keyed on the LONG - // uBlast envelope (~2.8s) so the color OUTLIVES the physics burst (owner's - // "color too brief" fix). Two layers: a blackbody plasma core that cools as - // the blast ages, and an outward-traveling SPECTRAL DISPERSION WAVE — rainbow - // shockwave rings expanding through the radius over uBlastTime, like a prism - // shattering. The unexpected color blast nobody else ships. - const blastColor = Fn(() => { - const pos = instancePos; - const b = clamp(this.uBlast, 0, 1); - const bt = this.uBlastTime; - const rNorm = clamp(length(pos).div(this.uContainRadius.max(0.0001)), 0, 1); - // Blackbody embers: a WARM core (capped ~5200K so it's hot-orange, NOT - // blinding blue-white — the white-out the owner saw was a 13000K plasma - // flash). Gentle gain so it tints, never dominates. - const kelvin = mix(float(1600.0), float(5200.0), b); - const gain = clamp(b.mul(1.1).add(0.4), 0, 1.3); - const fire = blackbody(kelvin).mul(gain); - // THE STAR OF THE BLAST — an outward SPECTRAL DISPERSION shockwave: - // concentric rainbow rings travel out through the radius over time (red - // lags, blue leads — real prism order). This is the color, not the fire. - const specT = fract(rNorm.mul(1.6).sub(bt.mul(1.5))); - const spectrum = palette(specT, vec3(0.55), vec3(0.55), vec3(3.0), vec3(0.0, 0.33, 0.67)); - // Spectrum DOMINATES (0.78); a touch of warm fire underneath for energy. - // The owner wants a COLOR blast, so the rainbow wins over the plasma. - return mix(fire, spectrum, float(0.78)); - }); - - // colorNode: world color × rim × act dim, then the blast overrides toward - // detonation chroma at the peak and lingers (the long uBlast tail) before - // melting back into the next world's palette. - mat.colorNode = Fn(() => { - const glow = clamp(this.uIgnition.mul(0.05).add(0.72), 0, 1.25); // lifted: 4 depth systems multiply-dim - const world = rainbowColor().mul(glow).mul(rimFactor()).mul(this.uActDim); - // Blast is CAPPED at 0.6 so the inner/outer CLASH duotone always shows - // through even during a detonation — the clash is the star, the blast is - // an accent (was fully overriding, which washed the contrast to rainbow). - const blastMix = smoothstep(float(0.0), float(0.85), clamp(this.uBlast, 0, 1)).mul(0.6); - return mix(world, blastColor().mul(rimFactor()).mul(this.uActDim), blastMix).mul(depthFade()); - })(); - - // emissiveNode: what the selective bloom reads — THE glow channel. Rim-gated - // so ONLY the outer shell blooms (calm dark center, no white blob). The blast - // gain is held below the color path (×0.85) so the bloom never clips white. - mat.emissiveNode = Fn(() => { - const emGain = clamp(this.uIgnition.mul(0.04).add(0.85), 0, 1.35); // lifted to feed the bloom through the depth dimming - const world = rainbowColor().mul(emGain).mul(rimFactor()).mul(this.uActDim); - // Same cap as colorNode so the bloom feeds on the CLASH colors, not a - // rainbow override. - const blastMix = smoothstep(float(0.0), float(0.85), clamp(this.uBlast, 0, 1)).mul(0.6); - // ×nearFade so the bloom ALSO dissolves near the camera (no near-plane flash). - return mix(world, blastColor().mul(0.85).mul(rimFactor()).mul(this.uActDim), blastMix).mul(depthFade()); - })(); - - // One instanced sprite per particle. Small quads (0.1) keep individual - // particles as crisp colored points of light rather than overlapping into - // white mush across the now-larger volume. - const geometry = new THREE.PlaneGeometry(0.1, 0.1); - const mesh = new THREE.InstancedMesh(geometry, mat as unknown as THREE.Material, this.count); - mesh.frustumCulled = false; - this.material = mat; - this.mesh = mesh; - this.scene.add(this.mesh); - } - - /** Advance the GPU physics one frame. Compute dispatches are serialized so - * a slow GPU never lets passes pile up and stall the queue. */ - async update(deltaSeconds: number): Promise { - const dt = Math.max(0, Math.min(deltaSeconds, 0.05)); - this.uTime.value += dt; - // Slowly rotate the whole rainbow so the cloud is always shimmering. - this.uHueShift.value = (this.uHueShift.value + dt * 0.06) % 1; - // World crossfade: ease uBlend 1→0 over ~1s after each beat so the cloud - // melts from the previous world's home/forces into the new one's. - this.uBlend.value = Math.max(0, this.uBlend.value - dt * 1.0); - // Ignition decays toward 0 between beats (spikes back up on transitionTo()). - this.uIgnition.value = Math.max(0, this.uIgnition.value - dt * 2.0); - // Burst decays fast so the explosion crystallizes back within ~1.2s. - this.uBurst.value = Math.max(0, this.uBurst.value - dt * 0.85); - // COLOR BLAST: slow decay (~2.8s) so the detonation chroma LASTS, and the - // wave clock counts up so the spectral shockwave travels outward over time. - this.uBlast.value = Math.max(0, this.uBlast.value - dt * 0.35); - this.uBlastTime.value += dt; - // RACK FOCUS — when diving, the focus plane tracks the descent (pulls inward - // over each zoom cycle so the new inner figure crisps up as it grows); when - // still, a gentle breathing pull keeps the DOF alive. - const zp = (this.uTime.value / this.uZoomPeriod.value) % 1; - const focusTarget = this.uZoomOn.value > 0.5 - ? 40 - zp * 16 // 40→24 over each dive cycle (keeps the grown figure in focus) - : 26 + Math.sin(this.uTime.value * 0.18) * 9; // 17..35 breathing - this.uFocus.value += (focusTarget - this.uFocus.value) * Math.min(1, dt * 3); - - // Wait for any in-flight compute to finish before queuing the next. - if (this.computeInFlight) await this.computeInFlight; - this.computeInFlight = this.renderer.computeAsync(this.computeNode).finally(() => { - this.computeInFlight = null; - }); - await this.computeInFlight; - } - - /** Fired on each narrative beat: retarget the storm + spike ignition. - * `act` blazes Acts II/III at full; `beatIndex` (0-based) holds the very first - * beats EXTRA dim — beats 0 and 1 fire while the cloud is still bunched from - * the initial reform and would otherwise wash to white. They ramp up to full - * over the opening, so the storm fades IN beautifully instead of flashing. */ - transitionTo( - role: SemanticRole, - worldPos: THREE.Vector3, - act: 'I' | 'II' | 'III' = 'II', - beatIndex = 99 - ): void { - this.uTarget.value.copy(worldPos); - const mode = ROLE_MODE[role] ?? 1; - this.uMode.value = mode; - - // WORLD ADVANCE: beats map 1:1 to the 7 worlds. Record the outgoing world - // and reset the crossfade so the cloud melts prev→new over ~1s. - this.uPrevWorld.value = this.uWorld.value; - this.uWorld.value = beatIndex % this.worldCount; - this.uBlend.value = 1; // 1 = fully previous; update() eases it to 0 - // Cycle the jarring inner/outer clash pair so each beat collides a fresh - // pair of opposing color worlds (ice↔fire, acid↔blood, …). - this.uClash.value = beatIndex % 5; - // Engage the infinite dive from Act II onward (beats 0/1 stay calm + still). - this.uZoomOn.value = beatIndex >= 2 ? 1 : 0; - - // Per-beat warm-up dim: beats 0/1 stay calm (dialed-in safety), then hands - // off to the act-based brightness. Acts II/III blaze. - const warmup = beatIndex === 0 ? 0.12 : beatIndex === 1 ? 0.2 : null; - const actDim = act === 'I' ? 0.26 : 1.0; - this.uActDim.value = warmup ?? actDim; - // Ignition flash: nearly none on beats 0/1, gentle for the rest of Act I, - // strong (but no longer white-blowing) for Acts II/III — lowered from 8.0 to - // 4.5 so the inner/outer CLASH colors survive the detonation instead of - // washing to white. - this.uIgnition.value = beatIndex <= 1 ? 0.4 : act === 'I' ? 1.6 : 4.5; - // PHYSICS BURST (fast) — contradiction + the DETONATION world (3) hit hardest. - const isDetonation = this.uWorld.value === 3; - this.uBurst.value = mode === 2 || isDetonation ? 1.0 : 0.8; - // COLOR BLAST (LONG) — fire the chroma envelope + reset its outward-wave - // clock. Beats 0/1 keep it very low so the calm opener never flashes. - this.uBlast.value = beatIndex <= 1 ? 0.25 : 1.0; - this.uBlastTime.value = 0; - // Dramatic beats (contradiction=2, surprise=3) push their mode color over - // the rainbow so they read clearly; calm beats stay mostly iridescent. - this.uModeTintAmt.value = mode >= 2 ? 0.7 : 0.22; - } - - /** - * ENDLESS DREAM BEAT — fired on a timer AFTER the scripted tour ends, so the - * storm never sits idle. Jumps to a RANDOM procedural figure (worlds 7..11), - * reseeds it (so it's never the same shape twice), ramps uChaos up so each one - * is wilder than the last, and detonates a full color blast. This is the - * "random figure generator that makes even crazier beats." - */ - dreamBeat(): void { - this.dreamCount += 1; - // Pick a random wild figure (worlds 7..11 are the procedural generators). - const world = 7 + Math.floor(Math.random() * 5); - this.uPrevWorld.value = this.uWorld.value; - this.uWorld.value = world; - this.uBlend.value = 1; - // Fresh random seed → the superformula/knot/lissajous/helix/foam params all - // change, so the same world index never looks the same twice. - this.uMorphSeed.value = Math.random() * 1000; - // Random opposing clash pair each dream figure → never the same collision. - this.uClash.value = Math.floor(Math.random() * 5); - // Chaos ramps up and saturates — figures get progressively crazier, then - // hold at max wildness. Eases in over the first ~8 dream beats. - this.uChaos.value = Math.min(1, 0.25 + this.dreamCount * 0.1); - // Dream mode dives endlessly — the infinite zoom is always on here. - this.uZoomOn.value = 1; - // Detonation + long color blast every dream beat — but ignition kept - // MODERATE (not the tour's 8.0) so the random dense figures don't wash to - // white at the blast peak. The rim-gated spectral blast carries the color. - this.uActDim.value = 0.85; - this.uIgnition.value = 3.0; - this.uBurst.value = 1.0; - this.uBlast.value = 1.0; - this.uBlastTime.value = 0; - // Vary the mode tint randomly too so the palette keeps surprising. - const modes = [1, 2, 3]; - this.uMode.value = modes[Math.floor(Math.random() * modes.length)]; - this.uModeTintAmt.value = 0.3 + Math.random() * 0.5; - } - - /** Size the containment sphere (world units) so the storm always stays in - * frame. The sandbox derives this from the camera distance + fov. */ - setContainRadius(radius: number): void { - this.uContainRadius.value = Math.max(8, radius); - } - - /** Enable/disable the infinite Droste zoom dive. Off for beats 0/1 (calm) and - * reduced-motion; on for Act II+ and dream mode. */ - setZoom(on: boolean): void { - this.uZoomOn.value = on ? 1 : 0; - } - - /** View-space apparent particle velocity (the negated, view-transformed camera - * velocity). Drives the streak orientation + length. */ - setCameraVel(v: THREE.Vector3): void { - this.uCamVelView.value.copy(v); - } - - /** Flythrough streak strength 0..1. Set to 0 for reduced-motion (no streak). */ - setStreak(s: number): void { - this.uStreak.value = s; - } - - dispose(): void { - if (this.mesh) { - this.scene.remove(this.mesh); - this.mesh.geometry?.dispose(); - this.mesh.dispose?.(); - this.mesh = null; - } - this.material?.dispose(); - this.material = null; - // StorageBufferAttribute extends BufferAttribute, which has no dispose(): - // its GPU buffer is released by the renderer when the owning geometry is - // disposed (done above). Drop our references so the ~2.1MB of backing - // Float32Arrays can be garbage-collected. - this.bufferPos = null; - this.bufferVel = null; - this.bufferPhase = null; - } -} diff --git a/apps/dashboard/src/lib/graph/cinema/topology.ts b/apps/dashboard/src/lib/graph/cinema/topology.ts deleted file mode 100644 index 5518b39..0000000 --- a/apps/dashboard/src/lib/graph/cinema/topology.ts +++ /dev/null @@ -1,225 +0,0 @@ -// The Auteur — graph signal extraction. -// -// Pure, dependency-free statistics over the REAL /api/graph data, computed once -// per Cinema launch. These signals are what gives the AI director something -// meaningful to direct: which memory is most load-bearing (betweenness), where -// tension lives (contradictions), what's surprising (distant-but-plausible -// links), what's fading (low retention / suppression). No LLM, no WebGPU, no -// network — fully headless-testable. - -import type { GraphNode, GraphEdge } from '$types'; -import { buildAdjacency, recencyOf, isContradictionEdge } from './pathfinder'; - -export interface NodeSignal { - nodeId: string; - /** Raw connection count. */ - degree: number; - /** Brandes betweenness centrality, normalized 0..1 — how load-bearing this - * memory is as a bridge between clusters. The director favors high-betweenness - * nodes for hero shots. */ - betweenness: number; - /** Connected-component id (which cluster of memory this belongs to). */ - clusterId: number; - /** 0..1, 1 = most recent. */ - recencyRank: number; - /** FSRS retention 0..1. */ - retention: number; - /** Suppression pressure 0..1 (memory actively being forgotten). */ - suppression: number; -} - -export interface EdgeSignal { - source: string; - target: string; - isContradiction: boolean; - isMergeSupersede: boolean; - /** 0..1: high when endpoints share neighbors (plausible) yet the edge weight - * is low (distant) — a surprising, non-obvious connection. */ - surprise: number; - weight: number; -} - -export interface GraphSignals { - nodes: Map; - edges: EdgeSignal[]; - clusterCount: number; - /** Node id with the single highest betweenness — the graph's keystone. */ - peakBetweennessId: string; -} - -function isMergeSupersedeEdge(edge: GraphEdge): boolean { - const t = (edge.type ?? '').toLowerCase(); - return t.includes('merge') || t.includes('supersede') || t.includes('duplicate'); -} - -/** - * Brandes' algorithm for betweenness centrality on an unweighted, undirected - * graph. O(V·E) — fine for /api/graph payloads. Returns raw (unnormalized) - * scores keyed by node id; the caller normalizes. - */ -function brandesBetweenness(nodeIds: string[], adj: Record): Map { - const cb = new Map(); - for (const v of nodeIds) cb.set(v, 0); - - for (const s of nodeIds) { - const stack: string[] = []; - const pred = new Map(); - const sigma = new Map(); - const dist = new Map(); - for (const v of nodeIds) { - pred.set(v, []); - sigma.set(v, 0); - dist.set(v, -1); - } - sigma.set(s, 1); - dist.set(s, 0); - - // BFS (unweighted shortest paths). - const queue: string[] = [s]; - let head = 0; - while (head < queue.length) { - const v = queue[head++]; - stack.push(v); - for (const { otherId: w } of adj[v] ?? []) { - if ((dist.get(w) ?? -1) < 0) { - dist.set(w, (dist.get(v) ?? 0) + 1); - queue.push(w); - } - if ((dist.get(w) ?? -1) === (dist.get(v) ?? 0) + 1) { - sigma.set(w, (sigma.get(w) ?? 0) + (sigma.get(v) ?? 0)); - pred.get(w)!.push(v); - } - } - } - - // Accumulation (back-propagate dependencies). - const delta = new Map(); - for (const v of nodeIds) delta.set(v, 0); - while (stack.length > 0) { - const w = stack.pop()!; - for (const v of pred.get(w) ?? []) { - const c = ((sigma.get(v) ?? 0) / (sigma.get(w) || 1)) * (1 + (delta.get(w) ?? 0)); - delta.set(v, (delta.get(v) ?? 0) + c); - } - if (w !== s) cb.set(w, (cb.get(w) ?? 0) + (delta.get(w) ?? 0)); - } - } - return cb; -} - -/** Union-find connected components → a cluster id per node. */ -function components(nodeIds: string[], edges: GraphEdge[]): { clusterOf: Map; count: number } { - const parent = new Map(); - for (const id of nodeIds) parent.set(id, id); - const find = (x: string): string => { - let root = x; - while (parent.get(root) !== root) root = parent.get(root)!; - // Path compression. - let cur = x; - while (parent.get(cur) !== root) { - const next = parent.get(cur)!; - parent.set(cur, root); - cur = next; - } - return root; - }; - const union = (a: string, b: string) => { - const ra = find(a); - const rb = find(b); - if (ra !== rb) parent.set(ra, rb); - }; - for (const e of edges) { - if (parent.has(e.source) && parent.has(e.target)) union(e.source, e.target); - } - const rootToCluster = new Map(); - const clusterOf = new Map(); - let next = 0; - for (const id of nodeIds) { - const r = find(id); - if (!rootToCluster.has(r)) rootToCluster.set(r, next++); - clusterOf.set(id, rootToCluster.get(r)!); - } - return { clusterOf, count: next }; -} - -/** - * Compute all director signals from the real graph. Pure; safe to call once at - * launch. Caps betweenness work on very large graphs by limiting to the - * top-degree subset (the only nodes that can carry meaningful centrality). - */ -export function computeSignals(nodes: GraphNode[], edges: GraphEdge[]): GraphSignals { - const nodeIds = nodes.map((n) => n.id); - const adj = buildAdjacency(edges); - - // Recency ranking (0..1, 1 = newest). - const byRecency = [...nodes].sort((a, b) => recencyOf(a) - recencyOf(b)); - const recencyRank = new Map(); - byRecency.forEach((n, i) => recencyRank.set(n.id, nodes.length > 1 ? i / (nodes.length - 1) : 1)); - - // Betweenness — guard pathological sizes: above the cap, compute on the - // top-degree subset (others get 0; they can't be meaningful bridges anyway). - const BETWEENNESS_CAP = 600; - let betweennessNodes = nodeIds; - if (nodeIds.length > BETWEENNESS_CAP) { - betweennessNodes = [...nodeIds] - .sort((a, b) => (adj[b]?.length ?? 0) - (adj[a]?.length ?? 0)) - .slice(0, BETWEENNESS_CAP); - } - const rawBetween = brandesBetweenness(betweennessNodes, adj); - let maxBetween = 0; - for (const v of rawBetween.values()) maxBetween = Math.max(maxBetween, v); - - const { clusterOf, count: clusterCount } = components(nodeIds, edges); - - const maxSuppression = Math.max(1, ...nodes.map((n) => n.suppression_count ?? 0)); - - const nodeSignals = new Map(); - let peakBetweennessId = nodeIds[0] ?? ''; - let peakVal = -1; - for (const n of nodes) { - const bt = maxBetween > 0 ? (rawBetween.get(n.id) ?? 0) / maxBetween : 0; - if (bt > peakVal) { - peakVal = bt; - peakBetweennessId = n.id; - } - nodeSignals.set(n.id, { - nodeId: n.id, - degree: adj[n.id]?.length ?? 0, - betweenness: bt, - clusterId: clusterOf.get(n.id) ?? 0, - recencyRank: recencyRank.get(n.id) ?? 0, - retention: clamp01(n.retention ?? 0), - suppression: clamp01((n.suppression_count ?? 0) / maxSuppression), - }); - } - - // Edge signals incl. surprise (shared-neighbor overlap × edge distance). - const neighborSets = new Map>(); - for (const id of nodeIds) neighborSets.set(id, new Set((adj[id] ?? []).map((a) => a.otherId))); - const edgeSignals: EdgeSignal[] = edges.map((e) => { - const a = neighborSets.get(e.source); - const b = neighborSets.get(e.target); - let shared = 0; - if (a && b) { - const [small, large] = a.size < b.size ? [a, b] : [b, a]; - for (const x of small) if (large.has(x)) shared++; - } - const union = (a?.size ?? 0) + (b?.size ?? 0) - shared || 1; - const overlap = shared / union; // Jaccard: structural plausibility. - const distance = 1 - clamp01(e.weight ?? 0); // low weight = semantically distant. - return { - source: e.source, - target: e.target, - isContradiction: isContradictionEdge(e), - isMergeSupersede: isMergeSupersedeEdge(e), - surprise: clamp01(overlap * distance * 2), // plausible AND distant = surprising. - weight: e.weight ?? 0, - }; - }); - - return { nodes: nodeSignals, edges: edgeSignals, clusterCount, peakBetweennessId }; -} - -function clamp01(x: number): number { - return Math.max(0, Math.min(1, Number.isFinite(x) ? x : 0)); -} diff --git a/apps/dashboard/src/lib/graph/nodes.ts b/apps/dashboard/src/lib/graph/nodes.ts index e247fae..8aaa528 100644 --- a/apps/dashboard/src/lib/graph/nodes.ts +++ b/apps/dashboard/src/lib/graph/nodes.ts @@ -421,7 +421,7 @@ export class NodeManager { /// The scene runs an UnrealBloomPass with threshold 0.2, so any bright /// canvas pixels get smeared into a halo. Previously the labels were /// near-white (#e2e8f0) text on a transparent background, which bloomed - /// into unreadable white blobs (issue filed 2026-04-19). The fix: + /// 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 diff --git a/apps/dashboard/src/lib/observatory/ObservatoryStage.svelte b/apps/dashboard/src/lib/observatory/ObservatoryStage.svelte deleted file mode 100644 index 0c1fda4..0000000 --- a/apps/dashboard/src/lib/observatory/ObservatoryStage.svelte +++ /dev/null @@ -1,411 +0,0 @@ - - - - - -
                    - - -
                    - -
                    - - - {#if showHud} -
                    - {#if chrome === 'full'} - - - {/if} - - - {#if chrome === 'full' && onexit} - - {/if} - - - {#if chrome === 'full' && showSwitcher} -
                    - {#each DEMO_MODES as mode (mode)} - - {/each} -
                    - {/if} - - - {#if loading} -
                    -
                    - LOADING MEMORY FIELD... -
                    -
                    - {/if} - - - {#if error && !loading} -
                    -
                    - {error} -
                    -
                    - {/if} - - - {#if chrome === 'full'} - - {/if} - - - {#if chrome === 'full' && demo === 'salience-rescue' && rescuePlan?.viable} - - {/if} - - - {#if chrome === 'full' && demo === 'firewall' && firewallPlan?.viable} - - {/if} - - - {#if !loading && graphData && graphData.nodeCount === 0} -
                    -
                    - NO MEMORIES IN FIELD -
                    -
                    - {/if} -
                    - {/if} -
                    - - diff --git a/apps/dashboard/src/lib/observatory/__tests__/birth-plan.test.ts b/apps/dashboard/src/lib/observatory/__tests__/birth-plan.test.ts deleted file mode 100644 index 03a9d08..0000000 --- a/apps/dashboard/src/lib/observatory/__tests__/birth-plan.test.ts +++ /dev/null @@ -1,227 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; - -// Mock WebGPU before any imports -vi.mock('$lib/observatory/engine', () => ({ - ObservatoryEngine: class {}, -})); - -// Mock document.createElement for canvas (needed by demo-clock) -const mockCanvas = { - width: 512, - height: 64, - getContext: () => null, - toDataURL: () => 'data:image/png;base64,', -}; - -if (typeof globalThis.document === 'undefined') { - (globalThis as any).document = { - createElement: (tag: string) => (tag === 'canvas' ? mockCanvas : {}), - }; -} - -import { buildBirthPlan, pickTargetIndex } from '../birth-plan'; -import type { ObservatoryGraph, ObservatoryNode, ObservatoryEdge } from '../types'; - -// --------------------------------------------------------------------------- -// Test helpers -// --------------------------------------------------------------------------- - -function makeNode( - id: string, - index: number, - opts: Partial = {} -): ObservatoryNode { - return { - id, - index, - label: `Node ${id}`, - type: 'memory', - retention: opts.retention ?? 0.5, - tags: opts.tags ?? [], - isCenter: opts.isCenter ?? false, - suppressed: opts.suppressed ?? false, - }; -} - -function makeEdge(sourceIndex: number, targetIndex: number): ObservatoryEdge { - return { sourceIndex, targetIndex, weight: 1.0, type: 'association' }; -} - -function makeGraph( - nodes: ObservatoryNode[], - edges: ObservatoryEdge[] -): ObservatoryGraph { - const indexById = new Map(); - for (const n of nodes) indexById.set(n.id, n.index); - const centerIndex = nodes.findIndex((n) => n.isCenter); - return { - nodes, - edges, - indexById, - centerIndex: centerIndex < 0 ? 0 : centerIndex, - }; -} - -// --------------------------------------------------------------------------- -// Tests -// --------------------------------------------------------------------------- - -describe('pickTargetIndex', () => { - it('picks center\'s highest-retention neighbor when edges exist', () => { - const nodes = [ - makeNode('center', 0, { isCenter: true, retention: 0.9 }), - makeNode('a', 1, { retention: 0.8 }), - makeNode('b', 2, { retention: 0.95 }), - makeNode('c', 3, { retention: 0.7 }), - ]; - const edges = [ - makeEdge(0, 1), // center <-> a (ret 0.8) - makeEdge(0, 2), // center <-> b (ret 0.95) - makeEdge(1, 3), // a <-> c (not incident to center) - ]; - const graph = makeGraph(nodes, edges); - const target = pickTargetIndex(graph); - expect(target).toBe(2); // b has highest retention (0.95) - }); - - it('picks first non-center node when center has no edges', () => { - const nodes = [ - makeNode('center', 0, { isCenter: true }), - makeNode('a', 1), - makeNode('b', 2), - ]; - const edges = [makeEdge(1, 2)]; // no edges from center - const graph = makeGraph(nodes, edges); - const target = pickTargetIndex(graph); - expect(target).toBe(1); // first non-center - }); - - it('picks center node when graph has only the center', () => { - const nodes = [makeNode('center', 0, { isCenter: true })]; - const graph = makeGraph(nodes, []); - const target = pickTargetIndex(graph); - expect(target).toBe(0); - }); - - it('picks center when all neighbors have equal retention', () => { - const nodes = [ - makeNode('center', 0, { isCenter: true }), - makeNode('a', 1, { retention: 0.5 }), - makeNode('b', 2, { retention: 0.5 }), - ]; - const edges = [makeEdge(0, 1), makeEdge(0, 2)]; - const graph = makeGraph(nodes, edges); - const target = pickTargetIndex(graph); - // Both have same retention (0.5), so picks first neighbor (a, index 1) - expect(target).toBe(1); - }); -}); - -describe('buildBirthPlan', () => { - let graph: ObservatoryGraph; - - beforeEach(() => { - const nodes = [ - makeNode('center', 0, { isCenter: true, retention: 0.9 }), - makeNode('a', 1, { retention: 0.8 }), - makeNode('b', 2, { retention: 0.95 }), - makeNode('c', 3, { retention: 0.7 }), - makeNode('d', 4, { retention: 0.6 }), - ]; - const edges = [ - makeEdge(0, 1), - makeEdge(0, 2), - makeEdge(1, 3), - makeEdge(2, 4), - ]; - graph = makeGraph(nodes, edges); - }); - - it('produces deterministic particles for same graph + seed', () => { - const plan1 = buildBirthPlan(graph, 'test-seed', 1024); - const plan2 = buildBirthPlan(graph, 'test-seed', 1024); - - expect(plan1.particles).toEqual(plan2.particles); - expect(plan1.edgeSteps).toEqual(plan2.edgeSteps); - expect(plan1.targetIndex).toBe(plan2.targetIndex); - }); - - it('produces different particles for different seeds', () => { - const plan1 = buildBirthPlan(graph, 'seed-a', 1024); - const plan2 = buildBirthPlan(graph, 'seed-b', 1024); - - // Same target, different particle positions - expect(plan1.targetIndex).toBe(plan2.targetIndex); - // But particles should differ - let different = false; - for (let i = 0; i < plan1.particles.length; i++) { - if (plan1.particles[i] !== plan2.particles[i]) { - different = true; - break; - } - } - expect(different).toBe(true); - }); - - it('always picks the same target regardless of seed', () => { - const plan1 = buildBirthPlan(graph, 'seed-a', 1024); - const plan2 = buildBirthPlan(graph, 'seed-b', 1024); - const plan3 = buildBirthPlan(graph, 'seed-c', 1024); - - expect(plan1.targetIndex).toBe(plan2.targetIndex); - expect(plan2.targetIndex).toBe(plan3.targetIndex); - // Should be index 2 (b, highest retention neighbor of center) - expect(plan1.targetIndex).toBe(2); - }); - - it('has correct particle array size', () => { - const plan = buildBirthPlan(graph, 'test', 2048); - expect(plan.particles.length).toBe(2048 * 16); // 16 floats per particle - }); - - it('has correct edge step array size', () => { - const plan = buildBirthPlan(graph, 'test'); - // 2 edges incident to center (0-1, 0-2) - expect(plan.edgeSteps.length).toBe(2 * 4); // 4 u32 per step - }); - - it('has valid timeline beats', () => { - const plan = buildBirthPlan(graph, 'test'); - expect(plan.timeline.length).toBeGreaterThan(0); - for (const beat of plan.timeline) { - expect(beat.label).toBeTruthy(); - expect(beat.startFrame).toBeGreaterThanOrEqual(0); - expect(beat.endFrame).toBeGreaterThanOrEqual(beat.startFrame); - expect(beat.endFrame).toBeLessThan(720); - } - }); - - it('center-only graph produces valid target and zero edge steps', () => { - const centerOnly = makeGraph( - [makeNode('center', 0, { isCenter: true })], - [] - ); - const plan = buildBirthPlan(centerOnly, 'center-test', 512); - expect(plan.targetIndex).toBe(0); - expect(plan.targetNodeId).toBe('center'); - expect(plan.edgeSteps.length).toBe(0); // zero incident edges = zero steps (plan L464) - }); - - it('does not use Math.random() — verified by code inspection', () => { - // This test documents the constraint: buildBirthPlan uses DemoClock - // (xmur3 + mulberry32) exclusively. Math.random() is never called. - // The test passes by construction — if Math.random() were used, - // determinism would break, which is caught by the determinism test above. - expect(true).toBe(true); - }); - - it('default particle count is 8192', () => { - const plan = buildBirthPlan(graph, 'test'); - expect(plan.particles.length).toBe(8192 * 16); - }); - - it('custom particle count works', () => { - const plan = buildBirthPlan(graph, 'test', 4096); - expect(plan.particles.length).toBe(4096 * 16); - }); -}); diff --git a/apps/dashboard/src/lib/observatory/__tests__/demo-clock.test.ts b/apps/dashboard/src/lib/observatory/__tests__/demo-clock.test.ts deleted file mode 100644 index a1a5182..0000000 --- a/apps/dashboard/src/lib/observatory/__tests__/demo-clock.test.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { DemoClock, deterministicSpherePosition } from '../demo-clock'; - -describe('DemoClock', () => { - it('should start at frame 0 with phase 0', () => { - const clock = new DemoClock({ seed: 'test-seed' }); - const state = clock.state; - expect(state.frame).toBe(0); - expect(state.phase).toBe(0); - expect(state.totalFrames).toBe(0); - }); - - it('should advance frame by 1 on each tick', () => { - const clock = new DemoClock({ seed: 'test-seed' }); - clock.tick(); - expect(clock.state.frame).toBe(1); - clock.tick(); - expect(clock.state.frame).toBe(2); - }); - - it('should wrap frame at loopFrames (default 720)', () => { - const clock = new DemoClock({ seed: 'test-seed' }); - // Advance to loopFrames - 1 - for (let i = 0; i < 719; i++) { - clock.tick(); - } - expect(clock.state.frame).toBe(719); - // One more tick should wrap to 0 - clock.tick(); - expect(clock.state.frame).toBe(0); - }); - - it('should compute correct phase (frame / loopFrames)', () => { - const clock = new DemoClock({ seed: 'test-seed' }); - clock.tick(); - expect(clock.state.phase).toBeCloseTo(1 / 720, 6); - clock.tick(); - expect(clock.state.phase).toBeCloseTo(2 / 720, 6); - }); - - it('should produce identical PRNG sequences for the same seed', () => { - const clock1 = new DemoClock({ seed: 'identical-seed' }); - const clock2 = new DemoClock({ seed: 'identical-seed' }); - const values1: number[] = []; - const values2: number[] = []; - for (let i = 0; i < 100; i++) { - clock1.tick(); - values1.push(clock1.state.rng()); - clock2.tick(); - values2.push(clock2.state.rng()); - } - expect(values1).toEqual(values2); - }); - - it('should produce different PRNG sequences for different seeds', () => { - const clock1 = new DemoClock({ seed: 'seed-a' }); - const clock2 = new DemoClock({ seed: 'seed-b' }); - clock1.tick(); - clock2.tick(); - expect(clock1.state.rng()).not.toBe(clock2.state.rng()); - }); - - it('should produce different positions for different seeds', () => { - const clock1 = new DemoClock({ seed: 'pos-seed-a' }); - const clock2 = new DemoClock({ seed: 'pos-seed-b' }); - const pos1 = deterministicSpherePosition(0, 100, 50, clock1.state.rng); - const pos2 = deterministicSpherePosition(0, 100, 50, clock2.state.rng); - expect(pos1).not.toEqual(pos2); - }); - - it('should produce identical positions for the same seed', () => { - const clock1 = new DemoClock({ seed: 'same-seed' }); - const clock2 = new DemoClock({ seed: 'same-seed' }); - const pos1 = deterministicSpherePosition(0, 100, 50, clock1.state.rng); - const pos2 = deterministicSpherePosition(0, 100, 50, clock2.state.rng); - expect(pos1).toEqual(pos2); - }); - - it('should reset to frame 0 and re-seed PRNG', () => { - const clock = new DemoClock({ seed: 'reset-seed' }); - // Advance 100 frames - for (let i = 0; i < 100; i++) { - clock.tick(); - } - expect(clock.state.frame).toBe(100); - expect(clock.state.totalFrames).toBe(100); - - // Reset - clock.reset(); - expect(clock.state.frame).toBe(0); - expect(clock.state.totalFrames).toBe(0); - - // After reset, the PRNG should produce the same values as the initial state - const afterResetRng = clock.state.rng(); - const initialRng = new DemoClock({ seed: 'reset-seed' }).state.rng(); - expect(afterResetRng).toBe(initialRng); - }); - - it('should respect custom loopFrames', () => { - const clock = new DemoClock({ seed: 'custom-loop', loopFrames: 360 }); - for (let i = 0; i < 360; i++) { - clock.tick(); - } - expect(clock.state.frame).toBe(0); - expect(clock.state.phase).toBe(0); - }); - - it('should respect custom fps (affects loopDuration)', () => { - const clock = new DemoClock({ seed: 'custom-fps', fps: 30, loopFrames: 300 }); - expect(clock.loopDuration).toBe(10); // 300 / 30 = 10s - }); - - it('exposes framesPerLoop for capture-mode frame normalization', () => { - expect(new DemoClock({ seed: 'x' }).framesPerLoop).toBe(720); - expect(new DemoClock({ seed: 'x', loopFrames: 360 }).framesPerLoop).toBe(360); - }); -}); - -describe('deterministicSpherePosition', () => { - it('should place nodes on a sphere surface', () => { - const clock = new DemoClock({ seed: 'sphere-test' }); - const pos = deterministicSpherePosition(0, 10, 50, clock.state.rng); - const [x, y, z] = pos; - const dist = Math.sqrt(x * x + y * y + z * z); - // Should be approximately at the given radius (some variance from golden angle) - expect(dist).toBeGreaterThan(40); - expect(dist).toBeLessThan(60); - }); - - it('should produce different positions for different indices', () => { - const clock = new DemoClock({ seed: 'sphere-test' }); - const pos0 = deterministicSpherePosition(0, 10, 50, clock.state.rng); - const pos1 = deterministicSpherePosition(1, 10, 50, clock.state.rng); - expect(pos0).not.toEqual(pos1); - }); -}); diff --git a/apps/dashboard/src/lib/observatory/__tests__/forgetting-plan.test.ts b/apps/dashboard/src/lib/observatory/__tests__/forgetting-plan.test.ts deleted file mode 100644 index 2f5afd3..0000000 --- a/apps/dashboard/src/lib/observatory/__tests__/forgetting-plan.test.ts +++ /dev/null @@ -1,439 +0,0 @@ -import { describe, it, expect } from 'vitest'; - -import { - buildForgettingPlan, - forgettingEnvelopes, - horizonDrift, - pickDrifting, - pickRescued, - rescueFrame, - FORGETTING_K, - FADING_BASE, - FADING_INTERVAL, - RESCUE_BASE, - RESCUE_INTERVAL, - SINK_BEAT_FRAME, - RETRIEVABLE_BEAT_FRAME -} from '../forgetting-plan'; -import { buildObservatoryGraph } from '../graph-upload'; -import { PATH_KIND } from '../types'; -import type { GraphNode, GraphEdge, GraphResponse } from '$types'; - -// --------------------------------------------------------------------------- -// Fixture helpers (cloned from rescue-plan.test.ts) -// --------------------------------------------------------------------------- - -function gn(id: string, opts: Partial = {}): GraphNode { - return { - id, - label: opts.label ?? `Memory ${id}`, - type: 'note', - retention: opts.retention ?? 0.5, - tags: opts.tags ?? [], - createdAt: opts.createdAt ?? '2026-01-15T00:00:00Z', - updatedAt: '2026-01-15T00:00:00Z', - isCenter: opts.isCenter ?? false, - suppression_count: opts.suppression_count - }; -} - -function ge(source: string, target: string, type = 'semantic'): GraphEdge { - return { source, target, weight: 1, type }; -} - -function gr(nodes: GraphNode[], edges: GraphEdge[], centerId = 'center'): GraphResponse { - return { - nodes, - edges, - center_id: centerId, - depth: 3, - nodeCount: nodes.length, - edgeCount: edges.length - }; -} - -/** 10-node fixture: center + 9, varied retention → driftCount = max(3, round(2.25)) = 3. */ -function tenNodeFixture(): GraphResponse { - const nodes = [ - gn('center', { isCenter: true, retention: 0.9 }), - gn('a', { retention: 0.05, label: 'oldest forgotten fact' }), - gn('b', { retention: 0.12 }), - gn('c', { retention: 0.2 }), - gn('d', { retention: 0.55 }), - gn('e', { retention: 0.6 }), - gn('f', { retention: 0.65 }), - gn('g', { retention: 0.7 }), - gn('h', { retention: 0.75 }), - gn('i', { retention: 0.8 }) - ]; - const edges = [ - ge('center', 'a'), - ge('center', 'b'), - ge('b', 'c'), - ge('center', 'd'), - ge('d', 'e') - ]; - return gr(nodes, edges); -} - -/** 40-node fixture: center + 39 → driftCount = round(0.25·39) = 10, K = 3 rescued. */ -function fortyNodeFixture(): GraphResponse { - const nodes: GraphNode[] = [gn('center', { isCenter: true, retention: 0.95 })]; - const edges: GraphEdge[] = []; - for (let i = 0; i < 39; i++) { - const id = `n${String(i).padStart(2, '0')}`; - nodes.push(gn(id, { retention: 0.04 + i * 0.02, label: `memory ${id}` })); - edges.push(ge('center', id)); - } - // Extra connectivity inside the drifting band so rescue scores separate. - edges.push(ge('n07', 'n08')); - edges.push(ge('n07', 'n09')); - edges.push(ge('n08', 'n09')); - return gr(nodes, edges); -} - -function planFor(response: GraphResponse) { - const graph = buildObservatoryGraph(response); - return { graph, plan: buildForgettingPlan(graph) }; -} - -// --------------------------------------------------------------------------- -// 1. Determinism -// --------------------------------------------------------------------------- - -describe('determinism', () => { - it('same graph → identical plan, byte-identical typed arrays', () => { - const r = fortyNodeFixture(); - const { plan: p1 } = planFor(r); - const { plan: p2 } = planFor(r); - expect(p1).toEqual(p2); - expect(Array.from(p1.horizonData)).toEqual(Array.from(p2.horizonData)); - expect(Array.from(p1.pathData)).toEqual(Array.from(p2.pathData)); - }); -}); - -// --------------------------------------------------------------------------- -// 2. pickDrifting — 25% clamp formula, center excluded, tie → lower index -// --------------------------------------------------------------------------- - -describe('pickDrifting', () => { - it('10-node graph → driftCount 3 (floor of min(3, n−1) beats round(0.25·9)=2)', () => { - const graph = buildObservatoryGraph(tenNodeFixture()); - const drifting = pickDrifting(graph); - expect(drifting.length).toBe(3); - expect(drifting).not.toContain(graph.centerIndex); - // lowest retention first - const ids = drifting.map((i) => graph.nodes[i].id); - expect(ids).toEqual(['a', 'b', 'c']); - }); - - it('40-node graph → driftCount round(0.25·39) = 10', () => { - const graph = buildObservatoryGraph(fortyNodeFixture()); - const drifting = pickDrifting(graph); - expect(drifting.length).toBe(10); - expect(drifting).not.toContain(graph.centerIndex); - // retention non-decreasing along the rank order - for (let i = 1; i < drifting.length; i++) { - expect(graph.nodes[drifting[i]].retention).toBeGreaterThanOrEqual( - graph.nodes[drifting[i - 1]].retention - ); - } - }); - - it('retention tie → lower node index drifts first', () => { - const r = gr( - [ - gn('center', { isCenter: true, retention: 0.9 }), - gn('a', { retention: 0.2 }), - gn('b', { retention: 0.2 }), - gn('c', { retention: 0.8 }) - ], - [] - ); - const graph = buildObservatoryGraph(r); - const drifting = pickDrifting(graph); - const ia = graph.indexById.get('a')!; - const ib = graph.indexById.get('b')!; - expect(drifting.indexOf(ia)).toBeLessThan(drifting.indexOf(ib)); - }); -}); - -// --------------------------------------------------------------------------- -// 3. pickRescued — subset of drifting, K = min(3, driftCount), score ordering -// --------------------------------------------------------------------------- - -describe('pickRescued', () => { - it('rescued ⊆ drifting, K = min(3, driftCount), highest 2·retention + degree/8 wins', () => { - const graph = buildObservatoryGraph(fortyNodeFixture()); - const drifting = pickDrifting(graph); - const rescued = pickRescued(graph, drifting); - expect(rescued.length).toBe(Math.min(FORGETTING_K, drifting.length)); - for (const idx of rescued) expect(drifting).toContain(idx); - - // recompute scores and verify the rescued are the top-K - const degree = new Uint32Array(graph.nodes.length); - for (const e of graph.edges) { - degree[e.sourceIndex]++; - degree[e.targetIndex]++; - } - const score = (i: number) => 2 * graph.nodes[i].retention + Math.min(degree[i], 8) / 8; - const expected = drifting.slice().sort((a, b) => score(b) - score(a) || a - b).slice(0, 3); - expect(rescued).toEqual(expected); - }); - - it('driftCount 1 → K = 1, the sole drifter is rescued', () => { - const r = gr([gn('center', { isCenter: true }), gn('a', { retention: 0.1 })], [ - ge('center', 'a') - ]); - const graph = buildObservatoryGraph(r); - const drifting = pickDrifting(graph); - expect(drifting.length).toBe(1); - const rescued = pickRescued(graph, drifting); - expect(rescued).toEqual(drifting); - }); -}); - -// --------------------------------------------------------------------------- -// 4. Packing round-trip -// --------------------------------------------------------------------------- - -describe('horizonData packing', () => { - it('rank/isDrifting/isRescued/slot decode; non-drifting words are exactly 0', () => { - const { graph, plan } = planFor(fortyNodeFixture()); - expect(plan.viable).toBe(true); - const driftCount = plan.driftingIndices.length; - - plan.driftingIndices.forEach((idx, i) => { - const w = plan.horizonData[idx]; - expect(w & 0x100).not.toBe(0); - expect(w & 0xff).toBe(Math.round((255 * i) / Math.max(1, driftCount - 1))); - }); - plan.rescuedIndices.forEach((idx, k) => { - const w = plan.horizonData[idx]; - expect(w & 0x200).not.toBe(0); - expect(w & 0x100).not.toBe(0); // rescued word also carries isDrifting - expect((w >>> 10) & 0x3).toBe(k); - }); - const roleSet = new Set(plan.driftingIndices); - for (let i = 0; i < graph.nodes.length; i++) { - if (!roleSet.has(i)) expect(plan.horizonData[i]).toBe(0); - } - expect(plan.horizonData[graph.centerIndex]).toBe(0); - }); -}); - -// --------------------------------------------------------------------------- -// 5. SEAM PROOF — every word, frames 0 and 719 all-zero -// --------------------------------------------------------------------------- - -describe('loop seam', () => { - it('forgettingEnvelopes is exactly zero at frames 0 and 719 for EVERY packed word', () => { - const { plan } = planFor(fortyNodeFixture()); - expect(plan.viable).toBe(true); - for (const packed of plan.horizonData) { - for (const frame of [0, 719]) { - const e = forgettingEnvelopes(frame, packed); - expect(Math.abs(e.x)).toBeLessThan(1e-6); - expect(Math.abs(e.y)).toBeLessThan(1e-6); - expect(Math.abs(e.z)).toBeLessThan(1e-6); - expect(Math.abs(e.w)).toBeLessThan(1e-6); - } - } - }); -}); - -// --------------------------------------------------------------------------- -// 6. Envelopes fire on the beat map -// --------------------------------------------------------------------------- - -describe('envelopes fire', () => { - it('unrescued drift: z > 0.5 @420, > 0.95 @655, ≤ 1+1e-6 at all frames', () => { - const { plan } = planFor(fortyNodeFixture()); - const rescuedSet = new Set(plan.rescuedIndices); - const unrescued = plan.driftingIndices.filter((i) => !rescuedSet.has(i)); - expect(unrescued.length).toBeGreaterThan(0); - for (const idx of unrescued) { - const w = plan.horizonData[idx]; - expect(forgettingEnvelopes(420, w).z).toBeGreaterThan(0.5); - expect(forgettingEnvelopes(655, w).z).toBeGreaterThan(0.95); - for (let f = 0; f < 720; f++) { - expect(forgettingEnvelopes(f, w).z).toBeLessThanOrEqual(1 + 1e-6); - } - } - }); - - it('rescued slot k: z < 1e-6 @rk+6, x > 0.95 @rk, x < 1e-6 @rk+140', () => { - const { plan } = planFor(fortyNodeFixture()); - plan.rescuedIndices.forEach((idx, k) => { - const w = plan.horizonData[idx]; - const rk = rescueFrame(k); - expect(Math.abs(forgettingEnvelopes(rk + 6, w).z)).toBeLessThan(1e-6); - expect(forgettingEnvelopes(rk, w).x).toBeGreaterThan(0.95); - expect(Math.abs(forgettingEnvelopes(rk + 140, w).x)).toBeLessThan(1e-6); - }); - }); -}); - -// --------------------------------------------------------------------------- -// 7. Monotone unrescued sink over 0..655 -// --------------------------------------------------------------------------- - -describe('monotone sink', () => { - it('unrescued z is non-decreasing over frames 0..655', () => { - const { plan } = planFor(fortyNodeFixture()); - const rescuedSet = new Set(plan.rescuedIndices); - const unrescued = plan.driftingIndices.filter((i) => !rescuedSet.has(i)); - for (const idx of unrescued) { - const w = plan.horizonData[idx]; - let prev = forgettingEnvelopes(0, w).z; - for (let f = 1; f <= 655; f++) { - const z = forgettingEnvelopes(f, w).z; - expect(z).toBeGreaterThanOrEqual(prev - 1e-9); - prev = z; - } - } - }); -}); - -// --------------------------------------------------------------------------- -// 8. horizonDrift — CPU mirror of the demo-3 vertex displacement -// --------------------------------------------------------------------------- - -describe('horizonDrift', () => { - it('[0,0,0] at dz=0; ~40.5 units at dz=1, downward, away from axis; no NaN on-axis', () => { - expect(horizonDrift(0, [30, 10, 40])).toEqual([0, 0, 0]); - - const p: [number, number, number] = [30, 10, 40]; - const d = horizonDrift(1, p); - const mag = Math.hypot(d[0], d[1], d[2]); - expect(mag).toBeGreaterThanOrEqual(38); - expect(mag).toBeLessThanOrEqual(42); - expect(d[1]).toBeLessThan(0); - // away·p ≥ 0: the drift pushes outward, never inward - expect(d[0] * p[0] + d[2] * p[2]).toBeGreaterThanOrEqual(0); - - // exactly on the y axis: r_xz clamps to 0.001, no NaN - const axis = horizonDrift(1, [0, 55, 0]); - expect(Number.isNaN(axis[0])).toBe(false); - expect(Number.isNaN(axis[1])).toBe(false); - expect(Number.isNaN(axis[2])).toBe(false); - expect(axis).toEqual([0, -34, 0]); - - // dz clamps to 1 - const over = horizonDrift(2, p); - expect(Math.hypot(over[0], over[1], over[2])).toBeCloseTo(mag, 9); - }); -}); - -// --------------------------------------------------------------------------- -// 9. Ribbon window + step shape -// --------------------------------------------------------------------------- - -describe('path steps', () => { - it('K kind-0 steps, src = center, dst = rescued_k, bf = 318+60k, window inside the loop', () => { - const { graph, plan } = planFor(fortyNodeFixture()); - const count = plan.pathData.length / 4; - expect(count).toBe(plan.rescuedIndices.length); - expect(plan.pathMetas.length).toBe(count); - - plan.rescuedIndices.forEach((idx, k) => { - expect(plan.pathData[k * 4 + 0]).toBe(graph.centerIndex); - expect(plan.pathData[k * 4 + 1]).toBe(idx); - expect(plan.pathData[k * 4 + 2]).toBe(RESCUE_BASE + RESCUE_INTERVAL * k); - expect(plan.pathData[k * 4 + 3]).toBe(PATH_KIND.recall); - const bf = plan.pathData[k * 4 + 2]; - expect(bf - 46).toBeGreaterThanOrEqual(0); - expect(bf + 90).toBeLessThanOrEqual(719); - }); - }); -}); - -// --------------------------------------------------------------------------- -// 10. Spine beats -// --------------------------------------------------------------------------- - -describe('spine beats', () => { - it('strictly increasing unique frames; fading/recalled labels; sink + retrievable', () => { - const { graph, plan } = planFor(fortyNodeFixture()); - const frames = plan.spineBeats.map((b) => b.beatFrame); - for (let i = 1; i < frames.length; i++) { - expect(frames[i]).toBeGreaterThan(frames[i - 1]); - } - expect(new Set(frames).size).toBe(frames.length); - expect(frames[0]).toBe(FADING_BASE); - expect(frames).toContain(SINK_BEAT_FRAME); - expect(frames[frames.length - 1]).toBe(RETRIEVABLE_BEAT_FRAME); - - const labels = plan.spineBeats.map((b) => b.label); - // fading beats carry real labels + retention percent - const rescuedSet = new Set(plan.rescuedIndices); - const fading = plan.driftingIndices.filter((i) => !rescuedSet.has(i)).slice(0, 3); - fading.forEach((idx, j) => { - const pct = Math.round(graph.nodes[idx].retention * 100); - expect(labels[j]).toBe( - `fading: ${graph.nodes[idx].label} · retention ${pct}%` - ); - expect(plan.spineBeats[j].beatFrame).toBe(FADING_BASE + FADING_INTERVAL * j); - }); - expect(labels.some((l) => l.startsWith('recalled: '))).toBe(true); - expect(labels).toContain('the unrecalled sink · nothing is deleted'); - expect(labels).toContain('every memory still retrievable'); - }); - - it('labels truncate at 64 chars with an ellipsis', () => { - const r = fortyNodeFixture(); - // lowest-retention node (n00) is unrescued → becomes the first fading beat - const n00 = r.nodes.find((n) => n.id === 'n00')!; - n00.label = 'x'.repeat(100); - const { plan } = planFor(r); - const fadingLabel = plan.spineBeats[0].label; - expect(fadingLabel.startsWith('fading: ')).toBe(true); - const memLabel = fadingLabel.slice('fading: '.length).split(' · ')[0]; - expect(memLabel.length).toBe(65); - expect(memLabel.endsWith('…')).toBe(true); - }); -}); - -// --------------------------------------------------------------------------- -// 11. Degenerates survive -// --------------------------------------------------------------------------- - -describe('degenerate graphs', () => { - it('0-node and 1-node → viable:false, min pathData, no throw; 2-node → viable', () => { - const { plan: p0 } = planFor(gr([], [], '')); - expect(p0.viable).toBe(false); - expect(p0.pathData.length).toBe(4); - expect(p0.pathMetas).toEqual([]); - expect(p0.spineBeats).toEqual([]); - - const { plan: p1 } = planFor(gr([gn('center', { isCenter: true })], [])); - expect(p1.viable).toBe(false); - expect(Array.from(p1.horizonData)).toEqual([0]); - - const { graph: g2, plan: p2 } = planFor( - gr([gn('center', { isCenter: true }), gn('a', { retention: 0.1 })], [ge('center', 'a')]) - ); - expect(p2.viable).toBe(true); - expect(p2.driftingIndices).toEqual([g2.indexById.get('a')!]); - expect(p2.rescuedIndices).toEqual([g2.indexById.get('a')!]); - expect(p2.pathData.length).toBe(4); - expect(p2.pathData[2]).toBe(RESCUE_BASE); - }); -}); - -// --------------------------------------------------------------------------- -// 12. Lane hygiene — the rescue/firewall grammars can NEVER fire in demo 3 -// --------------------------------------------------------------------------- - -describe('lane hygiene', () => { - it('y and w are exactly 0 for every word at frames 0..719 step 7', () => { - const { plan } = planFor(fortyNodeFixture()); - for (const packed of plan.horizonData) { - for (let f = 0; f < 720; f += 7) { - const e = forgettingEnvelopes(f, packed); - expect(e.y).toBe(0); - expect(e.w).toBe(0); - } - } - }); -}); diff --git a/apps/dashboard/src/lib/observatory/__tests__/graph-upload.test.ts b/apps/dashboard/src/lib/observatory/__tests__/graph-upload.test.ts deleted file mode 100644 index f1e9c54..0000000 --- a/apps/dashboard/src/lib/observatory/__tests__/graph-upload.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { DemoClock } from '../demo-clock'; -import { - buildObservatoryGraph, - buildNodeStateArray, - buildEdgeIndexArray, - hexToRgb01, - nodeBaseColor -} from '../graph-upload'; -import { FLOATS_PER_NODE, NODE_LANE, NODE_FLAG, toObservatoryNode } from '../types'; -import type { GraphResponse, GraphNode } from '$types'; - -function node(partial: Partial & { id: string }): GraphNode { - return { - label: partial.id, - type: 'note', - retention: 0.8, - tags: [], - createdAt: '2026-07-01T00:00:00Z', - updatedAt: '2026-07-01T00:00:00Z', - isCenter: false, - ...partial - }; -} - -function response(nodes: GraphNode[], edges: GraphResponse['edges'] = []): GraphResponse { - return { - nodes, - edges, - center_id: nodes.find((n) => n.isCenter)?.id ?? '', - depth: 3, - nodeCount: nodes.length, - edgeCount: edges.length - }; -} - -describe('buildObservatoryGraph', () => { - it('orders center first, then by id — independent of API order', () => { - const a = response([node({ id: 'zz' }), node({ id: 'aa' }), node({ id: 'mm', isCenter: true })]); - const b = response([node({ id: 'mm', isCenter: true }), node({ id: 'zz' }), node({ id: 'aa' })]); - const ga = buildObservatoryGraph(a); - const gb = buildObservatoryGraph(b); - expect(ga.nodes.map((n) => n.id)).toEqual(['mm', 'aa', 'zz']); - expect(gb.nodes.map((n) => n.id)).toEqual(['mm', 'aa', 'zz']); - expect(ga.centerIndex).toBe(0); - }); - - it('maps edges to stable indices and drops dangling/self edges', () => { - const g = buildObservatoryGraph( - response( - [node({ id: 'a' }), node({ id: 'b' })], - [ - { source: 'a', target: 'b', weight: 1, type: 'semantic' }, - { source: 'a', target: 'ghost', weight: 1, type: 'semantic' }, - { source: 'b', target: 'b', weight: 1, type: 'semantic' } - ] - ) - ); - expect(g.edges).toHaveLength(1); - expect(g.edges[0].sourceIndex).toBe(g.indexById.get('a')); - expect(g.edges[0].targetIndex).toBe(g.indexById.get('b')); - }); -}); - -describe('buildNodeStateArray determinism', () => { - const resp = response([ - node({ id: 'center', isCenter: true }), - node({ id: 'n1', retention: 0.9 }), - node({ id: 'n2', retention: 0.2 }), - node({ id: 'n3', retention: 0.05 }) - ]); - - it('same seed → identical field; different seed → different field', () => { - const g = buildObservatoryGraph(resp); - const a = buildNodeStateArray(g, new DemoClock({ seed: 'A' }).state.rng); - const b = buildNodeStateArray(g, new DemoClock({ seed: 'A' }).state.rng); - const c = buildNodeStateArray(g, new DemoClock({ seed: 'B' }).state.rng); - expect(Array.from(a.data)).toEqual(Array.from(b.data)); - expect(Array.from(a.data)).not.toEqual(Array.from(c.data)); - }); - - it('anchors the center node at the origin with the largest radius', () => { - const g = buildObservatoryGraph(resp); - const { data } = buildNodeStateArray(g, new DemoClock({ seed: 'A' }).state.rng); - expect(data[NODE_LANE.posRadius]).toBe(0); - expect(data[NODE_LANE.posRadius + 1]).toBe(0); - expect(data[NODE_LANE.posRadius + 2]).toBe(0); - const centerRadius = data[NODE_LANE.posRadius + 3]; - for (let i = 1; i < g.nodes.length; i++) { - expect(centerRadius).toBeGreaterThan(data[i * FLOATS_PER_NODE + NODE_LANE.posRadius + 3]); - } - }); - - it('stores retention and packs flags', () => { - const g = buildObservatoryGraph( - response([ - node({ id: 'c', isCenter: true }), - node({ id: 's', suppression_count: 2, retention: 0.5 }), - node({ id: 'aha', tags: ['aha'], retention: 0.6 }) - ]) - ); - const { data } = buildNodeStateArray(g, new DemoClock({ seed: 'A' }).state.rng); - const byId = (id: string) => g.indexById.get(id)! * FLOATS_PER_NODE; - expect(data[byId('s') + NODE_LANE.velRetention + 3]).toBeCloseTo(0.5, 6); - expect(data[byId('c') + NODE_LANE.colorFlags + 3] & NODE_FLAG.isCenter).toBeTruthy(); - expect(data[byId('s') + NODE_LANE.colorFlags + 3] & NODE_FLAG.suppressed).toBeTruthy(); - expect(data[byId('aha') + NODE_LANE.colorFlags + 3] & NODE_FLAG.isAha).toBeTruthy(); - }); -}); - -describe('meaning-layer palette (visual DNA §7.1)', () => { - it('maps FSRS retention buckets to the real dashboard palette', () => { - const active = toObservatoryNode(node({ id: 'a', retention: 0.9 }), 0); - const dormant = toObservatoryNode(node({ id: 'd', retention: 0.5 }), 1); - const silent = toObservatoryNode(node({ id: 's', retention: 0.2 }), 2); - const gone = toObservatoryNode(node({ id: 'u', retention: 0.01 }), 3); - expect(nodeBaseColor(active)).toEqual(hexToRgb01('#10b981')); // emerald - expect(nodeBaseColor(dormant)).toEqual(hexToRgb01('#f59e0b')); // amber - expect(nodeBaseColor(silent)).toEqual(hexToRgb01('#8b5cf6')); // violet - expect(nodeBaseColor(gone)).toEqual(hexToRgb01('#6b7280')); // slate - }); - - it('aha tag overrides with gold', () => { - const aha = toObservatoryNode(node({ id: 'x', retention: 0.9, tags: ['aha'] }), 0); - expect(nodeBaseColor(aha)).toEqual(hexToRgb01('#FFD700')); - }); - - it('hexToRgb01 falls back to slate on malformed input', () => { - expect(hexToRgb01('not-a-color')).toEqual(hexToRgb01('#6b7280')); - }); -}); - -describe('buildEdgeIndexArray', () => { - it('emits source/target index pairs', () => { - const g = buildObservatoryGraph( - response( - [node({ id: 'a' }), node({ id: 'b' }), node({ id: 'c' })], - [ - { source: 'a', target: 'b', weight: 1, type: 'semantic' }, - { source: 'b', target: 'c', weight: 1, type: 'semantic' } - ] - ) - ); - const arr = buildEdgeIndexArray(g); - expect(arr).toHaveLength(4); - expect(arr[0]).toBe(g.indexById.get('a')); - expect(arr[1]).toBe(g.indexById.get('b')); - }); -}); diff --git a/apps/dashboard/src/lib/observatory/__tests__/mip-plan.test.ts b/apps/dashboard/src/lib/observatory/__tests__/mip-plan.test.ts deleted file mode 100644 index ee62e5b..0000000 --- a/apps/dashboard/src/lib/observatory/__tests__/mip-plan.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { planBloomMips } from '../post/mip-plan'; - -describe('planBloomMips', () => { - it('M1 Max 3024×1964: half-res base, 6 mips, halving widths', () => { - const p = planBloomMips(3024, 1964); - expect(p.baseW).toBe(1512); - expect(p.baseH).toBe(982); - expect(p.mipCount).toBe(6); - expect(p.sizes.map(([w]) => w)).toEqual([1512, 756, 378, 189, 94, 47]); - }); - - it('64×64 → 3 mips', () => { - const p = planBloomMips(64, 64); - expect(p.baseW).toBe(32); - expect(p.baseH).toBe(32); - expect(p.mipCount).toBe(3); - expect(p.sizes).toEqual([ - [32, 32], - [16, 16], - [8, 8] - ]); - }); - - it('2×2 → 1 mip (1×1 base)', () => { - const p = planBloomMips(2, 2); - expect(p.baseW).toBe(1); - expect(p.baseH).toBe(1); - expect(p.mipCount).toBe(1); - expect(p.sizes).toEqual([[1, 1]]); - }); - - it('1×1 → 1 mip (degenerate; the up-loop runs zero times — harmless)', () => { - const p = planBloomMips(1, 1); - expect(p.baseW).toBe(1); - expect(p.baseH).toBe(1); - expect(p.mipCount).toBe(1); - }); - - it('sizes halve monotonically and never drop below 1', () => { - for (const [w, h] of [ - [3024, 1964], - [1920, 1080], - [800, 600], - [375, 812], - [7, 3] - ] as const) { - const p = planBloomMips(w, h); - expect(p.sizes[0]).toEqual([p.baseW, p.baseH]); - for (let i = 1; i < p.sizes.length; i++) { - const [pw, ph] = p.sizes[i - 1]; - const [cw, ch] = p.sizes[i]; - expect(cw).toBe(Math.max(1, pw >> 1)); - expect(ch).toBe(Math.max(1, ph >> 1)); - expect(cw).toBeGreaterThanOrEqual(1); - expect(ch).toBeGreaterThanOrEqual(1); - } - } - }); - - it('smallest mip keeps min dimension ≥ 8 when the base allows it', () => { - for (const [w, h] of [ - [3024, 1964], - [1920, 1080], - [375, 812], - [256, 128], - [64, 64] - ] as const) { - const p = planBloomMips(w, h); - const [lw, lh] = p.sizes[p.mipCount - 1]; - expect(Math.min(lw, lh)).toBeGreaterThanOrEqual(8); - } - }); - - it('mipCount is clamped to [1, 6]', () => { - expect(planBloomMips(1, 1).mipCount).toBe(1); - expect(planBloomMips(4, 4).mipCount).toBe(1); - expect(planBloomMips(8192, 8192).mipCount).toBe(6); - expect(planBloomMips(16384, 16384).mipCount).toBe(6); - }); -}); diff --git a/apps/dashboard/src/lib/observatory/__tests__/path-builder.test.ts b/apps/dashboard/src/lib/observatory/__tests__/path-builder.test.ts deleted file mode 100644 index 8838911..0000000 --- a/apps/dashboard/src/lib/observatory/__tests__/path-builder.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { buildRecallPath, beatFrameFor } from '../path-builder'; -import { buildObservatoryGraph } from '../graph-upload'; -import { UINTS_PER_PATHSTEP, PATH_KIND } from '../types'; -import type { GraphResponse, GraphNode, GraphEdge } from '$types'; - -function node(id: string, extra: Partial = {}): GraphNode { - return { - id, - label: `label-${id}`, - type: 'note', - retention: 0.8, - tags: [], - createdAt: '2026-07-01T00:00:00Z', - updatedAt: '2026-07-01T00:00:00Z', - isCenter: false, - ...extra - }; -} - -function edge(source: string, target: string, weight = 1, type = 'semantic'): GraphEdge { - return { source, target, weight, type }; -} - -// A small star graph around a center with a spur — enough for a real story. -function response(): GraphResponse { - const nodes = [ - node('center', { isCenter: true }), - node('a'), - node('b'), - node('c'), - node('d', { createdAt: '2026-07-02T00:00:00Z', updatedAt: '2026-07-02T00:00:00Z' }) - ]; - const edges = [ - edge('center', 'a', 5), - edge('center', 'b', 3), - edge('a', 'c', 2), - edge('c', 'd', 1) - ]; - return { - nodes, - edges, - center_id: 'center', - depth: 3, - nodeCount: nodes.length, - edgeCount: edges.length - }; -} - -describe('beatFrameFor', () => { - it('starts at frame 60 and advances 60 per beat', () => { - expect(beatFrameFor(0)).toBe(60); - expect(beatFrameFor(3)).toBe(240); - }); - - it('an 8-beat story + afterglow fits inside the 720-frame loop', () => { - // last beat at 60 + 7·60 = 480; afterglow envelope ends ≤ +200 frames - expect(beatFrameFor(7) + 200).toBeLessThan(720); - }); -}); - -describe('buildRecallPath', () => { - it('produces steps that reference valid stable node indices', () => { - const resp = response(); - const graph = buildObservatoryGraph(resp); - const { steps, data } = buildRecallPath(resp, graph); - - expect(steps.length).toBeGreaterThan(1); - for (const s of steps) { - expect(s.targetIndex).toBeGreaterThanOrEqual(0); - expect(s.targetIndex).toBeLessThan(graph.nodes.length); - expect(s.sourceIndex).toBeGreaterThanOrEqual(0); - expect(s.sourceIndex).toBeLessThan(graph.nodes.length); - } - expect(data.length).toBe(steps.length * UINTS_PER_PATHSTEP); - }); - - it('starts the story at the center with itself as source', () => { - const resp = response(); - const graph = buildObservatoryGraph(resp); - const { steps } = buildRecallPath(resp, graph); - expect(steps[0].nodeId).toBe('center'); - expect(steps[0].sourceIndex).toBe(steps[0].targetIndex); - expect(steps[0].beatFrame).toBe(60); - }); - - it('is deterministic — same data → identical steps', () => { - const resp = response(); - const graph = buildObservatoryGraph(resp); - const a = buildRecallPath(resp, graph); - const b = buildRecallPath(resp, graph); - expect(a.steps).toEqual(b.steps); - expect(Array.from(a.data)).toEqual(Array.from(b.data)); - }); - - it('marks contradiction beats as backward-cause hops', () => { - const resp = response(); - // pathfinder treats 'contradicts' edge type as a contradiction - resp.edges.push({ source: 'b', target: 'd', weight: 4, type: 'contradicts' }); - const graph = buildObservatoryGraph(resp); - const { steps } = buildRecallPath(resp, graph); - const kinds = new Set(steps.map((s) => s.beatKind)); - if (kinds.has('contradiction')) { - const c = steps.find((s) => s.beatKind === 'contradiction')!; - expect(c.kind).toBe(PATH_KIND.backwardCause); - } - // every step kind is one of the two GPU lanes either way - for (const s of steps) { - expect([PATH_KIND.recall, PATH_KIND.backwardCause]).toContain(s.kind); - } - }); - - it('survives an empty graph', () => { - const resp: GraphResponse = { - nodes: [], - edges: [], - center_id: '', - depth: 3, - nodeCount: 0, - edgeCount: 0 - }; - const graph = buildObservatoryGraph(resp); - const { steps, data } = buildRecallPath(resp, graph); - expect(steps).toHaveLength(0); - expect(data.length).toBeGreaterThan(0); // placeholder lane, no zero-size buffer - }); -}); diff --git a/apps/dashboard/src/lib/observatory/__tests__/rescue-plan.test.ts b/apps/dashboard/src/lib/observatory/__tests__/rescue-plan.test.ts deleted file mode 100644 index 3c2db1b..0000000 --- a/apps/dashboard/src/lib/observatory/__tests__/rescue-plan.test.ts +++ /dev/null @@ -1,619 +0,0 @@ -import { describe, it, expect } from 'vitest'; - -import { - buildRescuePlan, - rescueEnvelopes, - pickFailureIndex, - bfsFromFailure, - pickCauseIndex, - pickLookalikes, - layoutPositions, - hopSlotFor, - waveArrivalFrame, - lookalikeFrame, - UNREACHED, - MAX_WAVE_STEPS, - RESCUE_K, - ARC_FRAME, - VERDICT_START, - DETONATE_FRAME -} from '../rescue-plan'; -import { buildObservatoryGraph } from '../graph-upload'; -import { FLOATS_PER_NODE } from '../types'; -import type { GraphNode, GraphEdge, GraphResponse } from '$types'; - -// --------------------------------------------------------------------------- -// Fixture helpers -// --------------------------------------------------------------------------- - -function gn(id: string, opts: Partial = {}): GraphNode { - return { - id, - label: opts.label ?? `Memory ${id}`, - type: 'note', - retention: opts.retention ?? 0.5, - tags: opts.tags ?? [], - createdAt: opts.createdAt ?? '2026-01-15T00:00:00Z', - updatedAt: '2026-01-15T00:00:00Z', - isCenter: opts.isCenter ?? false, - suppression_count: opts.suppression_count - }; -} - -function ge(source: string, target: string, type = 'semantic'): GraphEdge { - return { source, target, weight: 1, type }; -} - -function gr(nodes: GraphNode[], edges: GraphEdge[], centerId = 'center'): GraphResponse { - return { - nodes, - edges, - center_id: centerId, - depth: 3, - nodeCount: nodes.length, - edgeCount: edges.length - }; -} - -/** - * Main fixture. Stable indices after buildObservatoryGraph (center first, - * then id-sorted): center=0, cause=1, fail=2, h1=3, h1b=4, h2=5, l1..l4=6..9. - * - * fail —(temporal)— h1 —(causal)— h2 —(causal)— cause (cause depth 3) - * fail — h1b, fail — center, center — l1..l4 - */ -function mainFixture(): GraphResponse { - const nodes = [ - gn('center', { isCenter: true, retention: 0.9 }), - gn('fail', { tags: ['failure'], retention: 0.8, label: 'checkout 500s on submit' }), - gn('h1', { retention: 0.6 }), - gn('h1b', { retention: 0.7 }), - gn('h2', { retention: 0.55 }), - gn('cause', { - retention: 0.1, - createdAt: '2025-03-01T12:00:00Z', - label: 'schema migration dropped index' - }), - gn('l1', { retention: 0.62 }), - gn('l2', { retention: 0.63 }), - gn('l3', { retention: 0.64 }), - gn('l4', { retention: 0.65 }) - ]; - const edges = [ - ge('fail', 'h1', 'temporal'), - ge('h1', 'h2', 'causal'), - ge('h2', 'cause', 'causal'), - ge('fail', 'h1b'), - ge('fail', 'center'), - ge('center', 'l1'), - ge('center', 'l2'), - ge('center', 'l3'), - ge('center', 'l4') - ]; - return gr(nodes, edges); -} - -const SEED = 'vestige-observatory-v1'; - -function planFor(response: GraphResponse, seed = SEED) { - const graph = buildObservatoryGraph(response); - return { graph, plan: buildRescuePlan(response, graph, seed) }; -} - -// --------------------------------------------------------------------------- -// 1. Determinism -// --------------------------------------------------------------------------- - -describe('determinism', () => { - it('same graph + seed → identical plan, byte-identical typed arrays', () => { - const r = mainFixture(); - const { plan: p1 } = planFor(r); - const { plan: p2 } = planFor(r); - expect(p1).toEqual(p2); - expect(Array.from(p1.waveData)).toEqual(Array.from(p2.waveData)); - expect(Array.from(p1.pathData)).toEqual(Array.from(p2.pathData)); - expect(Array.from(p1.hopDepths)).toEqual(Array.from(p2.hopDepths)); - }); -}); - -// --------------------------------------------------------------------------- -// 2. Failure selection -// --------------------------------------------------------------------------- - -describe('pickFailureIndex', () => { - it('never the center; prefers failure-tagged well-connected node; degree ≥ 2 when available', () => { - const r = mainFixture(); - const graph = buildObservatoryGraph(r); - const positions = layoutPositions(graph, SEED); - const failure = pickFailureIndex(graph, positions); - expect(failure).not.toBe(graph.centerIndex); - expect(graph.nodes[failure].id).toBe('fail'); // tagged 'failure', degree 3 - }); - - it('falls back to a non-center node when nothing is tagged and degrees are low', () => { - const r = gr( - [gn('center', { isCenter: true }), gn('a'), gn('b')], - [ge('a', 'b')] - ); - const graph = buildObservatoryGraph(r); - const failure = pickFailureIndex(graph, layoutPositions(graph, SEED)); - expect(failure).not.toBe(graph.centerIndex); - }); -}); - -// --------------------------------------------------------------------------- -// 3. BFS exactness -// --------------------------------------------------------------------------- - -describe('bfsFromFailure', () => { - it('exact depths on a chain, disconnected node UNREACHED and absent from pathData', () => { - // chain: center — a(failure) — b — c — d — e, plus disconnected g - const r = gr( - [ - gn('center', { isCenter: true }), - gn('a', { tags: ['failure'] }), - gn('b'), - gn('c'), - gn('d', { retention: 0.9 }), - gn('e', { retention: 0.2 }), - gn('g') - ], - [ge('center', 'a'), ge('a', 'b'), ge('b', 'c'), ge('c', 'd'), ge('d', 'e')] - ); - const graph = buildObservatoryGraph(r); - const idx = (id: string) => graph.indexById.get(id)!; - const { depths } = bfsFromFailure(graph, idx('a')); - expect(depths[idx('a')]).toBe(0); - expect(depths[idx('center')]).toBe(1); - expect(depths[idx('b')]).toBe(1); - expect(depths[idx('c')]).toBe(2); - expect(depths[idx('d')]).toBe(3); - expect(depths[idx('e')]).toBe(4); - expect(depths[idx('g')]).toBe(UNREACHED); - - const plan = buildRescuePlan(r, graph, SEED); - expect(plan.viable).toBe(true); - // A disconnected node can be a LOOKALIKE (nearest in layout — "looks - // similar, causally unrelated" is the story), so kind-2 probe beams may - // target it. But the backward wave walks REAL graph edges: no kind-1 - // wave/arc step may ever touch an unreached node. - for (let s = 0; s < plan.pathData.length / 4; s++) { - if (plan.pathData[s * 4 + 3] !== 1) continue; - expect(plan.pathData[s * 4 + 0]).not.toBe(idx('g')); - expect(plan.pathData[s * 4 + 1]).not.toBe(idx('g')); - } - }); -}); - -// --------------------------------------------------------------------------- -// 4. Cause selection -// --------------------------------------------------------------------------- - -describe('pickCauseIndex', () => { - it('depth ≥ 3 when available; lowest retention among depth ≥ 3 wins', () => { - const r = mainFixture(); - const graph = buildObservatoryGraph(r); - const idx = (id: string) => graph.indexById.get(id)!; - const { depths } = bfsFromFailure(graph, idx('fail')); - const cause = pickCauseIndex(r, graph, depths, idx('fail')); - expect(cause.index).toBe(idx('cause')); - expect(cause.depth).toBe(3); - }); - - it('retention tie → older createdAt wins', () => { - // two depth-3 candidates with equal retention, different ages - const r = gr( - [ - gn('center', { isCenter: true }), - gn('f', { tags: ['failure'] }), - gn('x'), - gn('y'), - gn('old', { retention: 0.2, createdAt: '2025-01-01T00:00:00Z' }), - gn('new', { retention: 0.2, createdAt: '2026-06-01T00:00:00Z' }) - ], - [ - ge('center', 'f'), - ge('f', 'x'), - ge('x', 'y'), - ge('y', 'old'), - ge('y', 'new') - ] - ); - const graph = buildObservatoryGraph(r); - const idx = (id: string) => graph.indexById.get(id)!; - const { depths } = bfsFromFailure(graph, idx('f')); - expect(depths[idx('old')]).toBe(3); - expect(depths[idx('new')]).toBe(3); - const cause = pickCauseIndex(r, graph, depths, idx('f')); - expect(cause.index).toBe(idx('old')); - }); -}); - -// --------------------------------------------------------------------------- -// 5. Edge-type-preferring parents -// --------------------------------------------------------------------------- - -describe('edge-type parents', () => { - it('equal-hop dual parents: the causal edge wins the parent chain', () => { - // f — u1 (semantic) — v ; f — u2 (semantic) — v via causal edge u2—v - const r = gr( - [ - gn('center', { isCenter: true }), - gn('f', { tags: ['failure'] }), - gn('u1'), - gn('u2'), - gn('v', { retention: 0.2 }) - ], - [ - ge('center', 'f'), - ge('f', 'u1', 'semantic'), - ge('f', 'u2', 'semantic'), - ge('u1', 'v', 'semantic'), - ge('u2', 'v', 'causal') - ] - ); - const graph = buildObservatoryGraph(r); - const idx = (id: string) => graph.indexById.get(id)!; - const { depths, parents } = bfsFromFailure(graph, idx('f')); - expect(depths[idx('v')]).toBe(2); - expect(parents[idx('v')]).toBe(idx('u2')); - - // pathData contains the (u2 → v) wave step - const plan = buildRescuePlan(r, graph, SEED); - let found = false; - for (let s = 0; s < plan.pathData.length / 4; s++) { - if ( - plan.pathData[s * 4 + 0] === idx('u2') && - plan.pathData[s * 4 + 1] === idx('v') && - plan.pathData[s * 4 + 3] === 1 - ) { - found = true; - } - } - expect(found).toBe(true); - }); -}); - -// --------------------------------------------------------------------------- -// 6. Relaxation ladder + non-viable -// --------------------------------------------------------------------------- - -describe('cause relaxation ladder', () => { - it('relaxes depth ≥ 3 → 2 on a short chain', () => { - // center — f(failure) — x — y : deepest candidate at depth 2 - const r = gr( - [ - gn('center', { isCenter: true }), - gn('f', { tags: ['failure'] }), - gn('x'), - gn('y', { retention: 0.3 }) - ], - [ge('center', 'f'), ge('f', 'x'), ge('x', 'y')] - ); - const { graph, plan } = planFor(r); - expect(plan.viable).toBe(true); - expect(plan.causeIndex).toBe(graph.indexById.get('y')!); - expect(plan.causeDepth).toBe(2); - }); - - it('1-node graph → viable:false, no throw', () => { - const r = gr([gn('center', { isCenter: true })], []); - const { plan } = planFor(r); - expect(plan.viable).toBe(false); - expect(plan.pathMetas).toEqual([]); - expect(plan.spineBeats).toEqual([]); - }); -}); - -// --------------------------------------------------------------------------- -// 7. Lookalikes -// --------------------------------------------------------------------------- - -describe('pickLookalikes', () => { - it('K = min(4, eligible), layout distances non-decreasing, excludes failure/cause/center', () => { - const r = mainFixture(); - const graph = buildObservatoryGraph(r); - const plan = buildRescuePlan(r, graph, SEED); - expect(plan.viable).toBe(true); - expect(plan.lookalikeIndices.length).toBe(Math.min(RESCUE_K, graph.nodes.length - 3)); - - // Recompute via the REAL layout function + same seed. - const positions = layoutPositions(graph, SEED); - const fi = plan.failureIndex; - const d2 = (i: number) => { - const dx = positions[i * FLOATS_PER_NODE + 0] - positions[fi * FLOATS_PER_NODE + 0]; - const dy = positions[i * FLOATS_PER_NODE + 1] - positions[fi * FLOATS_PER_NODE + 1]; - const dz = positions[i * FLOATS_PER_NODE + 2] - positions[fi * FLOATS_PER_NODE + 2]; - return dx * dx + dy * dy + dz * dz; - }; - for (let k = 1; k < plan.lookalikeIndices.length; k++) { - expect(d2(plan.lookalikeIndices[k])).toBeGreaterThanOrEqual( - d2(plan.lookalikeIndices[k - 1]) - ); - } - for (const li of plan.lookalikeIndices) { - expect(li).not.toBe(plan.failureIndex); - expect(li).not.toBe(plan.causeIndex); - expect(li).not.toBe(graph.centerIndex); - } - - // direct call agrees with the plan - const direct = pickLookalikes( - positions, - graph.nodes.length, - plan.failureIndex, - plan.causeIndex, - graph.centerIndex - ); - expect(direct).toEqual(plan.lookalikeIndices); - }); -}); - -// --------------------------------------------------------------------------- -// 8. SEAM PROOF — every node, frames 0 and 719 all-zero -// --------------------------------------------------------------------------- - -describe('loop seam', () => { - it('rescueEnvelopes is exactly zero at frames 0 and 719 for EVERY packed word', () => { - const r = mainFixture(); - const { plan } = planFor(r); - expect(plan.viable).toBe(true); - for (const packed of plan.waveData) { - for (const frame of [0, 719]) { - const e = rescueEnvelopes(frame, packed, plan.consts); - expect(Math.abs(e.x)).toBeLessThan(1e-6); - expect(Math.abs(e.y)).toBeLessThan(1e-6); - expect(Math.abs(e.z)).toBeLessThan(1e-6); - expect(Math.abs(e.w)).toBeLessThan(1e-6); - } - } - }); -}); - -// --------------------------------------------------------------------------- -// 9. Envelopes fire on the beat map -// --------------------------------------------------------------------------- - -describe('envelopes fire', () => { - it('y, x, z, w peak on their beats', () => { - const r = mainFixture(); - const { plan } = planFor(r); - const c = plan.consts; - - // searchlight: y > 0.9 at Fk on lookalike k - plan.lookalikeIndices.forEach((li, k) => { - const e = rescueEnvelopes(lookalikeFrame(k), plan.waveData[li], c); - expect(e.y).toBeGreaterThan(0.9); - }); - - // cause ignition: x > 0.99 at frame 580 - const cw = plan.waveData[plan.causeIndex]; - expect(rescueEnvelopes(580, cw, c).x).toBeGreaterThan(0.99); - - // backward wave: max z > 0.7 within [W(d), W(d)+28] on a depth-1 node - const d1 = plan.hopDepths.findIndex((d, i) => d === 1 && i !== plan.failureIndex); - expect(d1).toBeGreaterThanOrEqual(0); - const wd = waveArrivalFrame(1, plan.hopSlot); - let zMax = 0; - for (let f = wd; f <= wd + 28; f++) { - zMax = Math.max(zMax, rescueEnvelopes(f, plan.waveData[d1], c).z); - } - expect(zMax).toBeGreaterThan(0.7); - - // detonation: w > 0.9 at frame 105 on the failure - expect(rescueEnvelopes(105, plan.waveData[plan.failureIndex], c).w).toBeGreaterThan(0.9); - }); -}); - -// --------------------------------------------------------------------------- -// 10. Packing round-trip -// --------------------------------------------------------------------------- - -describe('waveData packing', () => { - it('depth/roles/k decode for failure, cause, lookalikes, plain and unreached nodes', () => { - // use the BFS-chain fixture with a disconnected node - const r = gr( - [ - gn('center', { isCenter: true }), - gn('a', { tags: ['failure'] }), - gn('b'), - gn('c'), - gn('d', { retention: 0.9 }), - gn('e', { retention: 0.2 }), - gn('g') - ], - [ge('center', 'a'), ge('a', 'b'), ge('b', 'c'), ge('c', 'd'), ge('d', 'e')] - ); - const graph = buildObservatoryGraph(r); - const idx = (id: string) => graph.indexById.get(id)!; - const plan = buildRescuePlan(r, graph, SEED); - expect(plan.viable).toBe(true); - - const fw = plan.waveData[plan.failureIndex]; - expect(fw & 0xffff).toBe(0); - expect(fw & 0x10000).not.toBe(0); - expect(fw & 0x20000).toBe(0); - - const cw = plan.waveData[plan.causeIndex]; - expect(cw & 0xffff).toBe(plan.causeDepth); - expect(cw & 0x20000).not.toBe(0); - expect(cw & 0x10000).toBe(0); - - plan.lookalikeIndices.forEach((li, k) => { - const w = plan.waveData[li]; - expect(w & 0x40000).not.toBe(0); - expect((w >>> 19) & 0x7).toBe(k); - expect(w & 0xffff).toBe(plan.hopDepths[li]); - }); - - // unreached node round-trips 0xFFFF and is never failure/cause - const gw = plan.waveData[idx('g')]; - expect(gw & 0xffff).toBe(UNREACHED); - expect(gw & 0x30000).toBe(0); - - // plain node (no role bits at all) — main fixture has 3 non-role nodes - const rich = mainFixture(); - const richGraph = buildObservatoryGraph(rich); - const richPlan = buildRescuePlan(rich, richGraph, SEED); - const roles = new Set([ - richPlan.failureIndex, - richPlan.causeIndex, - ...richPlan.lookalikeIndices - ]); - const plain = richGraph.nodes.findIndex( - (n) => !roles.has(n.index) && n.index !== richGraph.centerIndex - ); - expect(plain).toBeGreaterThanOrEqual(0); - expect(richPlan.waveData[plain] & 0x7f0000).toBe(0); - expect(richPlan.waveData[plain] & 0xffff).toBe(richPlan.hopDepths[plain]); - }); -}); - -// --------------------------------------------------------------------------- -// 11. Ribbon-window invariant + step ordering -// --------------------------------------------------------------------------- - -describe('path steps', () => { - it('every beat frame keeps its ribbon window inside [0, 719]; probes first, arc last', () => { - const r = mainFixture(); - const { graph, plan } = planFor(r); - const count = plan.pathData.length / 4; - expect(count).toBeLessThanOrEqual(RESCUE_K + MAX_WAVE_STEPS + 1); - - for (let s = 0; s < count; s++) { - const bf = plan.pathData[s * 4 + 2]; - expect(bf - 46).toBeGreaterThanOrEqual(0); - expect(bf + 90).toBeLessThanOrEqual(719); - } - - // probes first, kind 2, beats 138/166/194/222 - const K = plan.lookalikeIndices.length; - for (let k = 0; k < K; k++) { - expect(plan.pathData[k * 4 + 0]).toBe(plan.failureIndex); - expect(plan.pathData[k * 4 + 1]).toBe(plan.lookalikeIndices[k]); - expect(plan.pathData[k * 4 + 2]).toBe(138 + 28 * k); - expect(plan.pathData[k * 4 + 3]).toBe(2); - } - - // wave steps: kind 1 with bf = W(depth(dst)) - for (let s = K; s < count - 1; s++) { - expect(plan.pathData[s * 4 + 3]).toBe(1); - const dst = plan.pathData[s * 4 + 1]; - expect(plan.pathData[s * 4 + 2]).toBe( - waveArrivalFrame(plan.hopDepths[dst], plan.hopSlot) - ); - expect(plan.pathData[s * 4 + 2]).toBeLessThanOrEqual(514); - } - - // arc last: cause → failure at 560, kind 1 - const last = count - 1; - expect(plan.pathData[last * 4 + 0]).toBe(plan.causeIndex); - expect(plan.pathData[last * 4 + 1]).toBe(plan.failureIndex); - expect(plan.pathData[last * 4 + 2]).toBe(ARC_FRAME); - expect(plan.pathData[last * 4 + 3]).toBe(1); - void graph; - }); -}); - -// --------------------------------------------------------------------------- -// 12. setPathSteps contract -// --------------------------------------------------------------------------- - -describe('pathMetas contract', () => { - it('pathMetas is 1:1 with pathData steps (draw count = metas.length)', () => { - const { plan } = planFor(mainFixture()); - expect(plan.pathMetas.length).toBe(plan.pathData.length / 4); - }); -}); - -// --------------------------------------------------------------------------- -// 13. Spine beats -// --------------------------------------------------------------------------- - -describe('spine beats', () => { - it('beatFrames strictly increasing and unique; real labels present', () => { - const { plan } = planFor(mainFixture()); - const frames = plan.spineBeats.map((b) => b.beatFrame); - for (let i = 1; i < frames.length; i++) { - expect(frames[i]).toBeGreaterThan(frames[i - 1]); - } - expect(new Set(frames).size).toBe(frames.length); - expect(frames[0]).toBe(DETONATE_FRAME); - expect(frames[frames.length - 1]).toBe(VERDICT_START); - - const labels = plan.spineBeats.map((b) => b.label); - expect(labels[0]).toContain('checkout 500s on submit'); - expect(labels.some((l) => l.includes('lookalike ✗'))).toBe(true); - expect(labels.some((l) => l.includes('schema migration dropped index'))).toBe(true); - expect(labels[labels.length - 1]).toBe('root cause found'); - }); -}); - -// --------------------------------------------------------------------------- -// 14. Verdict receipt -// --------------------------------------------------------------------------- - -describe('verdict', () => { - it('real labels, real date, hop count and K', () => { - const { plan } = planFor(mainFixture()); - expect(plan.verdict.causeLabel).toBe('schema migration dropped index'); - expect(plan.verdict.failureLabel).toBe('checkout 500s on submit'); - expect(plan.verdict.causeDate).toBe('2025-03-01'); - expect(plan.verdict.hops).toBe(3); - expect(plan.verdict.k).toBe(4); - expect(plan.verdict.receipt).toBe('3 hops back · 2025-03-01 · vector search: 0 for 4'); - }); - - it('labels truncate at 64 chars with an ellipsis', () => { - const long = 'x'.repeat(100); - const r = mainFixture(); - const causeNode = r.nodes.find((n) => n.id === 'cause')!; - causeNode.label = long; - const { plan } = planFor(r); - expect(plan.verdict.causeLabel.length).toBe(65); - expect(plan.verdict.causeLabel.endsWith('…')).toBe(true); - }); -}); - -// --------------------------------------------------------------------------- -// 15. Degenerates survive -// --------------------------------------------------------------------------- - -describe('degenerate graphs', () => { - it('0-node, 2-node and edgeless graphs → viable:false, no throw, min pathData', () => { - const zero = gr([], [], ''); - const { plan: p0 } = planFor(zero); - expect(p0.viable).toBe(false); - expect(p0.pathData.length).toBe(4); - - const two = gr([gn('center', { isCenter: true }), gn('a')], []); - const { plan: p2 } = planFor(two); - expect(p2.viable).toBe(false); - expect(p2.pathData.length).toBe(4); - - const edgeless = gr([gn('center', { isCenter: true }), gn('a'), gn('b'), gn('c')], []); - const { plan: pe } = planFor(edgeless); - expect(pe.viable).toBe(false); - expect(pe.spineBeats).toEqual([]); - }); -}); - -// --------------------------------------------------------------------------- -// 16. hopSlot clamping -// --------------------------------------------------------------------------- - -describe('hopSlot', () => { - it('D=3 → 84; D=18 → 14 (clamped); W(D) ≤ 514', () => { - expect(hopSlotFor(3)).toBe(84); - expect(hopSlotFor(18)).toBe(14); - expect(waveArrivalFrame(3, hopSlotFor(3))).toBe(512); - expect(waveArrivalFrame(18, hopSlotFor(18))).toBe(512); - for (let d = 1; d <= 20; d++) { - expect(waveArrivalFrame(d, hopSlotFor(d))).toBeLessThanOrEqual(514); - } - // the main fixture's plan agrees - const { plan } = planFor(mainFixture()); - expect(plan.hopSlot).toBe(84); - expect(plan.consts.hopSlot).toBe(84); - }); -}); diff --git a/apps/dashboard/src/lib/observatory/__tests__/tone-reference.test.ts b/apps/dashboard/src/lib/observatory/__tests__/tone-reference.test.ts deleted file mode 100644 index 7d6a89d..0000000 --- a/apps/dashboard/src/lib/observatory/__tests__/tone-reference.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { pbrNeutralReference, VOID_CLEAR_HDR } from '../post/tone-reference'; -import { BLOOM_STRENGTH } from '../post/post-chain'; - -const hexToRgb = (hex: string): [number, number, number] => [ - parseInt(hex.slice(1, 3), 16) / 255, - parseInt(hex.slice(3, 5), 16) / 255, - parseInt(hex.slice(5, 7), 16) / 255 -]; - -const argmax = (v: readonly number[]) => v.indexOf(Math.max(...v)); -const argmin = (v: readonly number[]) => v.indexOf(Math.min(...v)); - -describe('pbrNeutralReference', () => { - it('void gate: tonemap(clear · (1 + BLOOM_STRENGTH)) is exactly #05060a', () => { - // The normalized bloom chain has flat-field gain exactly 1, so a void - // pixel enters the tonemap as (1 + BLOOM_STRENGTH) · VOID_CLEAR_HDR. - const s = 1 + BLOOM_STRENGTH; - const out = pbrNeutralReference([ - VOID_CLEAR_HDR.r * s, - VOID_CLEAR_HDR.g * s, - VOID_CLEAR_HDR.b * s - ]); - expect(Math.abs(out[0] - 5 / 255)).toBeLessThan(1e-6); - expect(Math.abs(out[1] - 6 / 255)).toBeLessThan(1e-6); - expect(Math.abs(out[2] - 10 / 255)).toBeLessThan(1e-6); - }); - - it('void preimage stays below the compression knee (early-return branch)', () => { - const s = 1 + BLOOM_STRENGTH; - const peak = Math.max(VOID_CLEAR_HDR.r, VOID_CLEAR_HDR.g, VOID_CLEAR_HDR.b) * s; - expect(peak).toBeLessThan(0.76); - }); - - it('below the knee is NOT identity: uniform −0.04 offset when min ≥ 0.08', () => { - // (0.5, 0.3, 0.2): min = 0.2 ≥ 0.08 → offset = 0.04, peak 0.46 < 0.76. - const out = pbrNeutralReference([0.5, 0.3, 0.2]); - expect(out[0]).toBeCloseTo(0.46, 12); - expect(out[1]).toBeCloseTo(0.26, 12); - expect(out[2]).toBeCloseTo(0.16, 12); - }); - - it('black offset: min < 0.08 → out_min = 6.25·min²', () => { - // (0.05, 0.3, 0.2): offset = 0.05 − 6.25·0.05² = 0.034375. - const out = pbrNeutralReference([0.05, 0.3, 0.2]); - expect(out[0]).toBeCloseTo(6.25 * 0.05 * 0.05, 12); - expect(out[1]).toBeCloseTo(0.3 - 0.034375, 12); - expect(out[2]).toBeCloseTo(0.2 - 0.034375, 12); - }); - - it('FSRS mint #10b981 stays below the knee: pure offset, order preserved', () => { - const rgb = hexToRgb('#10b981'); - const x = rgb[0]; // min channel (16/255 < 0.08) - const offset = x - 6.25 * x * x; - // peak after offset ≈ 0.687 < 0.76 → early return, deltas = offset. - expect(Math.max(...rgb) - offset).toBeLessThan(0.76); - const out = pbrNeutralReference(rgb); - expect(out[0]).toBeCloseTo(rgb[0] - offset, 12); - expect(out[1]).toBeCloseTo(rgb[1] - offset, 12); - expect(out[2]).toBeCloseTo(rgb[2] - offset, 12); - expect(argmax(out)).toBe(argmax(rgb)); - expect(argmin(out)).toBe(argmin(rgb)); - }); - - it('FSRS amber #f59e0b and violet #8b5cf6 hit compression: hue order still preserved', () => { - // NOTE deviation from the design brief, which claimed all three FSRS - // colors take the below-knee branch: after the black offset, amber's - // peak ≈ 0.929 and violet's ≈ 0.925 — both ≥ 0.76, so the compression - // branch runs. Hue preservation (channel ordering) is the real guard. - for (const hex of ['#f59e0b', '#8b5cf6']) { - const rgb = hexToRgb(hex); - const x = Math.min(...rgb); - const offset = x < 0.08 ? x - 6.25 * x * x : 0.04; - expect(Math.max(...rgb) - offset).toBeGreaterThanOrEqual(0.76); - const out = pbrNeutralReference(rgb); - expect(argmax(out)).toBe(argmax(rgb)); - expect(argmin(out)).toBe(argmin(rgb)); - // Compressed: peak shrinks, everything stays inside (0, 1). - expect(Math.max(...out)).toBeLessThan(Math.max(...rgb)); - for (const ch of out) { - expect(ch).toBeGreaterThan(0); - expect(ch).toBeLessThan(1); - } - } - }); - - it('compression: HDR greys 2/4/8 map monotonically, all below 1', () => { - const g2 = pbrNeutralReference([2, 2, 2])[0]; - const g4 = pbrNeutralReference([4, 4, 4])[0]; - const g8 = pbrNeutralReference([8, 8, 8])[0]; - expect(g2).toBeLessThan(g4); - expect(g4).toBeLessThan(g8); - expect(g8).toBeLessThan(1); - // Greys stay grey (hue-preserving on the neutral axis). - expect(pbrNeutralReference([4, 4, 4])).toEqual([g4, g4, g4]); - }); - - it('hue: argmax stable through compression for (1.5, 0.4, 0.2)', () => { - const out = pbrNeutralReference([1.5, 0.4, 0.2]); - expect(argmax(out)).toBe(0); - expect(Math.max(...out)).toBeLessThan(1); - }); -}); diff --git a/apps/dashboard/src/lib/observatory/birth-plan.ts b/apps/dashboard/src/lib/observatory/birth-plan.ts deleted file mode 100644 index 5b65ce4..0000000 --- a/apps/dashboard/src/lib/observatory/birth-plan.ts +++ /dev/null @@ -1,381 +0,0 @@ -/** - * Cognitive Observatory — deterministic birth-plan CPU helpers (Moment B, Task B1). - * - * Pure CPU: pick a birth target, precompute deterministic `BirthParticle` initial - * arrays, build birth beat metadata. No GPU code, no Math.random(). - * - * Particle layout (16 floats / 64 bytes per particle): - * start_life : xyz start position, w seed/life scalar (phase offset) - * target_size : xyz target position, w base size (1.0 + rng * 1.8) - * color_phase : rgb target/base node color, w phase offset - * state : xyz current position (shader computes), w alpha - * - * All start positions form a deterministic hollow shell around the target: - * 70% spherical shell (radius 110-180) - * 20% tendrils along incident edge directions - * 10% near-camera dust plane - */ - -import { DemoClock, deterministicSpherePosition } from './demo-clock'; -import type { ObservatoryGraph } from './types'; - -// --------------------------------------------------------------------------- -// Public types -// --------------------------------------------------------------------------- - -export interface TimelineBeat { - label: string; - startFrame: number; - endFrame: number; -} - -export interface BirthPlan { - targetIndex: number; - targetNodeId: string; - /** 16 floats per particle (64 bytes). */ - particles: Float32Array; - /** 4 u32 per edge step (source, target, beatFrame, kind). */ - edgeSteps: Uint32Array; - timeline: TimelineBeat[]; -} - -// --------------------------------------------------------------------------- -// Constants -// --------------------------------------------------------------------------- - -const FLOATS_PER_BIRTH_PARTICLE = 16; -const UINTS_PER_BIRTH_EDGE_STEP = 4; - -// Shell radii -const SHELL_MIN_RADIUS = 110; -const SHELL_MAX_RADIUS = 180; - -// Particle distribution fractions -const FRACTION_SHELL = 0.70; -const FRACTION_TENDRIL = 0.20; -const FRACTION_DUST = 0.10; - -// Edge engraving: each incident edge gets a pulse starting at frame 360 -const ENGRAVE_START_FRAME = 360; -const ENGRAVE_INTERVAL = 18; // frames between successive edge pulses - -// --------------------------------------------------------------------------- -// Target selection -// --------------------------------------------------------------------------- - -/** - * Pick the birth target deterministically from the graph. - * - * Priority: - * 1. Center node's highest-retention neighbor (if graph has edges). - * 2. First non-center node after stable graph ordering. - * 3. Center node. - */ -export function pickTargetIndex(graph: ObservatoryGraph): number { - // 1. Prefer center's highest-retention neighbor - if (graph.edges.length > 0) { - const centerIdx = graph.centerIndex; - const incidentEdges = graph.edges.filter( - (e) => e.sourceIndex === centerIdx || e.targetIndex === centerIdx - ); - if (incidentEdges.length > 0) { - let bestNeighborIdx = -1; - let bestRetention = -1; - for (const edge of incidentEdges) { - const neighborIdx = - edge.sourceIndex === centerIdx ? edge.targetIndex : edge.sourceIndex; - const neighbor = graph.nodes[neighborIdx]; - if (neighbor && neighbor.retention > bestRetention) { - bestRetention = neighbor.retention; - bestNeighborIdx = neighborIdx; - } - } - if (bestNeighborIdx >= 0) return bestNeighborIdx; - } - } - - // 2. First non-center node after stable ordering - for (let i = 0; i < graph.nodes.length; i++) { - if (i !== graph.centerIndex) return i; - } - - // 3. Center node (fallback) - return graph.centerIndex; -} - -// --------------------------------------------------------------------------- -// Particle precomputation -// --------------------------------------------------------------------------- - -/** - * Build the deterministic particle array for a birth event. - * - * Uses a fresh DemoClock seeded with `seed + ':birth:' + targetNodeId` so - * the same graph + seed always produces the same layout. - */ -export function buildBirthPlan( - graph: ObservatoryGraph, - seed: string, - particleCount = 8192 -): BirthPlan { - const targetIndex = pickTargetIndex(graph); - const targetNode = graph.nodes[targetIndex]; - const targetNodeId = targetNode.id; - - // Get target node position from the graph (will be in the node state buffer) - const targetPos = getNodePosition(graph, targetIndex); - - // Build a fresh DemoClock for deterministic particle placement - const clock = new DemoClock({ seed: seed + ':birth:' + targetNodeId }); - const rng = clock.state.rng; - - // Build particles - const particles = new Float32Array(particleCount * FLOATS_PER_BIRTH_PARTICLE); - - const shellCount = Math.floor(particleCount * FRACTION_SHELL); - const tendrilCount = Math.floor(particleCount * FRACTION_TENDRIL); - const dustCount = particleCount - shellCount - tendrilCount; - - // --- 70%: spherical shell around target --- - for (let i = 0; i < shellCount; i++) { - const base = i * FLOATS_PER_BIRTH_PARTICLE; - - // Deterministic position on a sphere around the target - const [sx, sy, sz] = deterministicSpherePosition( - i, - shellCount, - SHELL_MIN_RADIUS + rng() * (SHELL_MAX_RADIUS - SHELL_MIN_RADIUS), - rng - ); - - // World-space start position = target + shell offset - particles[base + 0] = targetPos[0] + sx; - particles[base + 1] = targetPos[1] + sy; - particles[base + 2] = targetPos[2] + sz; - // w: phase offset (stagger) - particles[base + 3] = rng(); - - // Target position (same for all particles — convergence target) - particles[base + 4] = targetPos[0]; - particles[base + 5] = targetPos[1]; - particles[base + 6] = targetPos[2]; - // w: base size - particles[base + 7] = 1.0 + rng() * 1.8; - - // Color: violet dust base (0.55, 0.32, 1.00) - particles[base + 8] = 0.55; - particles[base + 9] = 0.32; - particles[base + 10] = 1.00; - // w: phase offset for spectral rim - particles[base + 11] = rng(); - - // state: zeroed (shader computes current position) - particles[base + 12] = 0; - particles[base + 13] = 0; - particles[base + 14] = 0; - particles[base + 15] = 0; - } - - // --- 20%: tendrils along incident edge directions --- - const incidentEdges = graph.edges.filter( - (e) => e.sourceIndex === targetIndex || e.targetIndex === targetIndex - ); - - for (let i = 0; i < tendrilCount; i++) { - const base = (shellCount + i) * FLOATS_PER_BIRTH_PARTICLE; - - // Skip tendrils when no incident edges (center-only graph) - if (incidentEdges.length === 0) { - // Just place as shell particles (already done above) - continue; - } - - // Pick an incident edge direction (cycle through edges) - const edgeIdx = i % incidentEdges.length; - const edge = incidentEdges[edgeIdx]; - - // Direction from target to neighbor - const neighborIdx = - edge.sourceIndex === targetIndex ? edge.targetIndex : edge.sourceIndex; - const neighborPos = getNodePosition(graph, neighborIdx); - const dx = neighborPos[0] - targetPos[0]; - const dy = neighborPos[1] - targetPos[1]; - const dz = neighborPos[2] - targetPos[2]; - const len = Math.sqrt(dx * dx + dy * dy + dz * dz) || 1; - - // Place along the edge direction, spread out - const t = (i / Math.max(1, tendrilCount)) * 2.0 + 0.5; // 0.5 to 2.5 - const spread = rng() * 30; // perpendicular spread - - // Perpendicular offset (simple cross with a fixed axis) - const px = -dy * spread / (len || 1); - const py = dx * spread / (len || 1); - const pz = 0; - - particles[base + 0] = targetPos[0] + (dx / len) * t * 80 + px; - particles[base + 1] = targetPos[1] + (dy / len) * t * 80 + py; - particles[base + 2] = targetPos[2] + (dz / len) * t * 80 + pz; - particles[base + 3] = rng(); - - particles[base + 4] = targetPos[0]; - particles[base + 5] = targetPos[1]; - particles[base + 6] = targetPos[2]; - particles[base + 7] = 1.0 + rng() * 1.8; - - particles[base + 8] = 0.55; - particles[base + 9] = 0.32; - particles[base + 10] = 1.00; - particles[base + 11] = rng(); - - particles[base + 12] = 0; - particles[base + 13] = 0; - particles[base + 14] = 0; - particles[base + 15] = 0; - } - - // --- 10%: near-camera dust plane for depth sparkle --- - // Camera orbits around the field; a "near camera" plane is roughly at - // orbit distance. We place these in front of the target along the - // camera's approximate view direction (z-axis in our coordinate system). - const ORBIT_DISTANCE = 300; - for (let i = 0; i < dustCount; i++) { - const base = (shellCount + tendrilCount + i) * FLOATS_PER_BIRTH_PARTICLE; - - // Spread in a plane near the camera orbit distance - const angle = rng() * Math.PI * 2; - const spread = rng() * 120; - - particles[base + 0] = targetPos[0] + Math.cos(angle) * spread; - particles[base + 1] = targetPos[1] + Math.sin(angle) * spread; - particles[base + 2] = targetPos[2] + ORBIT_DISTANCE * 0.6 + rng() * 40; - particles[base + 3] = rng(); - - particles[base + 4] = targetPos[0]; - particles[base + 5] = targetPos[1]; - particles[base + 6] = targetPos[2]; - particles[base + 7] = 1.0 + rng() * 1.8; - - particles[base + 8] = 0.55; - particles[base + 9] = 0.32; - particles[base + 10] = 1.00; - particles[base + 11] = rng(); - - particles[base + 12] = 0; - particles[base + 13] = 0; - particles[base + 14] = 0; - particles[base + 15] = 0; - } - - // --------------------------------------------------------------------------- - // Edge steps for engraving - // --------------------------------------------------------------------------- - - const edgeSteps = buildEdgeSteps(graph, targetIndex); - - // --------------------------------------------------------------------------- - // Timeline beats - // --------------------------------------------------------------------------- - - const timeline = buildTimeline(); - - return { - targetIndex, - targetNodeId, - particles, - edgeSteps, - timeline, - }; -} - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -/** - * Get the world-space position of a node from the graph. - * Uses deterministic sphere placement (same as graph-upload). - */ -function getNodePosition( - graph: ObservatoryGraph, - nodeIndex: number -): [number, number, number] { - const node = graph.nodes[nodeIndex]; - const n = graph.nodes.length; - - if (node.isCenter && graph.centerIndex === nodeIndex) { - return [0, 0, 0]; - } - - // Use a fixed seed for position (not the birth seed) so positions match - // what the NodeRenderer will produce. We use a deterministic "position seed" - // derived from the node's index and the graph's center. - // The actual NodeRenderer uses the demo seed for perturbation, but for - // the birth plan we need to know where the target node will be. - // We use a simple golden-angle placement with a fixed perturbation seed. - const goldenAngle = Math.PI * (3 - Math.sqrt(5)); - const y = 1 - (nodeIndex / (n - 1 || 1)) * 2; - const radiusAtY = Math.sqrt(1 - y * y); - const theta = goldenAngle * nodeIndex; - const fieldRadius = 120; - - // Fixed perturbation (no random — deterministic by index) - const px = ((nodeIndex * 7 + 3) % 100) / 100 * 0.1 * fieldRadius - 0.05 * fieldRadius; - const py = ((nodeIndex * 13 + 7) % 100) / 100 * 0.1 * fieldRadius - 0.05 * fieldRadius; - const pz = ((nodeIndex * 17 + 11) % 100) / 100 * 0.1 * fieldRadius - 0.05 * fieldRadius; - - return [ - Math.cos(theta) * radiusAtY * fieldRadius + px, - y * fieldRadius + py, - Math.sin(theta) * radiusAtY * fieldRadius + pz, - ]; -} - -/** - * Build edge steps for the engraving phase (frames 360+). - * Each incident edge from the target gets a pulse. - */ -function buildEdgeSteps( - graph: ObservatoryGraph, - targetIndex: number -): Uint32Array { - const incidentEdges = graph.edges.filter( - (e) => e.sourceIndex === targetIndex || e.targetIndex === targetIndex - ); - - const stepCount = incidentEdges.length; - // No incident edges → zero steps (plan L464): BirthRenderer skips the - // engrave buffer entirely rather than drawing a phantom 0→0 pulse. - if (stepCount === 0) { - return new Uint32Array(0); - } - - const data = new Uint32Array(stepCount * UINTS_PER_BIRTH_EDGE_STEP); - - for (let k = 0; k < stepCount; k++) { - const edge = incidentEdges[k]; - const neighborIdx = - edge.sourceIndex === targetIndex ? edge.targetIndex : edge.sourceIndex; - const beatFrame = ENGRAVE_START_FRAME + k * ENGRAVE_INTERVAL; - - data[k * UINTS_PER_BIRTH_EDGE_STEP + 0] = targetIndex; // source = target - data[k * UINTS_PER_BIRTH_EDGE_STEP + 1] = neighborIdx; // target = neighbor - data[k * UINTS_PER_BIRTH_EDGE_STEP + 2] = beatFrame; - data[k * UINTS_PER_BIRTH_EDGE_STEP + 3] = 0; // kind: normal - } - - return data; -} - -/** - * Build the timeline beats for the 720-frame birth loop. - * Matches the choreography schedule from the plan. - */ -function buildTimeline(): TimelineBeat[] { - return [ - { label: 'latent trace condensing', startFrame: 60, endFrame: 239 }, - { label: 'engram coalescence', startFrame: 240, endFrame: 329 }, - { label: 'memory ignition', startFrame: 330, endFrame: 359 }, - { label: 'associations engrave', startFrame: 360, endFrame: 509 }, - { label: 'stabilization', startFrame: 510, endFrame: 659 }, - ]; -} diff --git a/apps/dashboard/src/lib/observatory/birth-renderer.ts b/apps/dashboard/src/lib/observatory/birth-renderer.ts deleted file mode 100644 index 00560da..0000000 --- a/apps/dashboard/src/lib/observatory/birth-renderer.ts +++ /dev/null @@ -1,616 +0,0 @@ -/** - * Cognitive Observatory — birth particle renderer (Moment B, Tasks B3–B6). - * - * Registers as a FramePass on the engine ONLY when demoMode === 'engram-birth'. - * Handles: - * B3: compute entry (convergence choreography) - * B4: particle billboard render pipeline (instanced additive) - * B5: birth flash + target halo (frames 330–359, loop-seam safe) - * B6: edge engraving via path-ribbon reuse + TimelineSpine beats - * - * Integration: reads nodeStateBuffer and cameraUniformBuffer from NodeRenderer, - * creates its own particle buffer (initialized from buildBirthPlan), and - * dispatches compute + render passes each frame. - * - * Loop-seam: all time terms are integer-cycles per 720 frames. - * Capture mode: params._pad == 1.0 skips stateful particle integration. - * No Math.random() in new files. - */ - -import type { ObservatoryEngine, FramePass } from './engine'; -import { NodeRenderer } from './node-renderer'; -import { buildBirthPlan, type BirthPlan, type TimelineBeat } from './birth-plan'; -import { birthParticlesWGSL } from './shaders/birth-particles.wgsl'; -import { renderPathWGSL } from './shaders/render-path.wgsl'; - -// --------------------------------------------------------------------------- -// Constants -// --------------------------------------------------------------------------- - -const PARTICLE_FLOATS = 16; // 64 bytes per particle -const QUAD_VERTS = 6; // two triangles - -// Flash frames (B5) -const FLASH_START = 330; -const FLASH_END = 359; - -// Edge engraving start (B6) -const ENGRAVE_START = 360; - -// --------------------------------------------------------------------------- -// Public types -// --------------------------------------------------------------------------- - -export interface BirthRendererOptions { - engine: ObservatoryEngine; - nodeRenderer: NodeRenderer; - seed: string; -} - -// --------------------------------------------------------------------------- -// BirthRenderer -// --------------------------------------------------------------------------- - -export class BirthRenderer implements FramePass { - private engine: ObservatoryEngine; - private nodeRenderer: NodeRenderer; - private active: boolean; - - // Compute pipeline (B3) - private computePipeline: GPUComputePipeline | null = null; - private computeBindGroup: GPUBindGroup | null = null; - private particleBuffer: GPUBuffer | null = null; - private particleCount = 0; - - // Render pipeline (B4) — instanced additive billboards - private renderPipeline: GPURenderPipeline | null = null; - private renderBindGroup: GPUBindGroup | null = null; - - // Flash/halo (B5) — target glow ring - private haloPipeline: GPURenderPipeline | null = null; - private haloBindGroup: GPUBindGroup | null = null; - private haloIndexBuffer: GPUBuffer | null = null; - - // Edge engraving (B6) — reuse path-ribbon shader - private engravePipeline: GPURenderPipeline | null = null; - private engraveBindGroup: GPUBindGroup | null = null; - private engraveBuffer: GPUBuffer | null = null; - private engraveStepCount = 0; - - // Timeline beats for overlay (B6) - timeline: TimelineBeat[] = []; - - // Birth plan (CPU-side) - private birthPlan: BirthPlan | null = null; - - /** - * Engrave steps in PathStep layout (source, target, beatFrame, kind) — the - * route feeds these into the NodeRenderer path system so the proven - * wavefront machinery renders the outward engraving (and the recall sim - * blooms each neighbor as its edge lands). - */ - get engraveSteps(): Uint32Array { - return (this.birthPlan?.edgeSteps ?? new Uint32Array(0)) as Uint32Array; - } - - constructor(opts: BirthRendererOptions) { - this.engine = opts.engine; - this.nodeRenderer = opts.nodeRenderer; - // Only activate when demoMode === 'engram-birth' (demo_id === 1). - // We check this each frame in compute() so it's safe to always register. - this.active = false; - - this.engine.addPass(this); - } - - /** Initialize the birth plan and GPU resources. Call after upload(). */ - upload(seed: string): void { - const device = this.engine.gpuDevice; - if (!device || !this.nodeRenderer.nodeStateBuffer) return; - - const graph = this.nodeRenderer.graph; - if (!graph) return; - - // Build the deterministic birth plan (CPU). - this.birthPlan = buildBirthPlan(graph, seed); - this.timeline = this.birthPlan.timeline; - - const particleCount = this.birthPlan.particles.length / PARTICLE_FLOATS; - this.particleCount = particleCount; - - // Create particle storage buffer. - this.particleBuffer?.destroy(); - this.particleBuffer = device.createBuffer({ - label: 'observatory-birth-particles', - size: this.birthPlan.particles.byteLength, - usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST - }); - device.queue.writeBuffer(this.particleBuffer, 0, this.birthPlan.particles.buffer as ArrayBuffer); - - // Create edge engraving buffer (B6). - this.engraveBuffer?.destroy(); - this.engraveStepCount = this.birthPlan.edgeSteps.length / 4; - if (this.engraveStepCount > 0) { - this.engraveBuffer = device.createBuffer({ - label: 'observatory-birth-engrave', - size: this.birthPlan.edgeSteps.byteLength, - usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST - }); - device.queue.writeBuffer(this.engraveBuffer, 0, this.birthPlan.edgeSteps.buffer as ArrayBuffer); - } - - // Create compute pipeline (B3). - this.createComputePipeline(device); - - // Create render pipeline (B4). - this.createRenderPipeline(device); - - // Create flash/halo pipeline (B5). - this.createHaloPipeline(device); - - // Create edge engraving pipeline (B6). - this.createEngravePipeline(device); - } - - private createComputePipeline(device: GPUDevice): void { - const module = device.createShaderModule({ - label: 'observatory-birth-compute', - code: birthParticlesWGSL - }); - - this.computePipeline = device.createComputePipeline({ - label: 'observatory-birth-compute-pipeline', - layout: 'auto', - compute: { module, entryPoint: 'birth_compute' } - }); - - // The compute shader declares exactly bindings 0-1; binding anything the - // auto layout stripped (it has no binding 2) invalidates the bind group. - const entries: GPUBindGroupEntry[] = [ - { binding: 0, resource: { buffer: this.engine.paramsBuffer! } }, - { binding: 1, resource: { buffer: this.particleBuffer! } } - ]; - - this.computeBindGroup = device.createBindGroup({ - label: 'observatory-birth-compute-bind', - layout: this.computePipeline!.getBindGroupLayout(0), - entries - }); - } - - private createRenderPipeline(device: GPUDevice): void { - // Particle billboard shader (inline WGSL for render pass). - const particleRenderWGSL = /* wgsl */ ` -struct Params { - frame: f32, - loop_phase: f32, - node_count: f32, - edge_count: f32, - path_count: f32, - pulse: f32, - viewport_w: f32, - viewport_h: f32, - brightness: f32, - demo_id: f32, - time: f32, - _pad: f32, -}; - -struct Camera { - view_proj: mat4x4, - right: vec4, - up: vec4, -}; - -struct BirthParticle { - start_life: vec4, - target_size: vec4, - color_phase: vec4, - state: vec4, -}; - -@group(0) @binding(0) var params: Params; -@group(0) @binding(1) var camera: Camera; -@group(0) @binding(2) var particles: array; - -struct VSOut { - @builtin(position) clip: vec4, - @location(0) uv: vec2, - @location(1) @interpolate(flat) color: vec3, - @location(2) @interpolate(flat) misc: vec4, -}; - -const CORNERS = array, 6>( - vec2(-1.0, -1.0), - vec2( 1.0, -1.0), - vec2( 1.0, 1.0), - vec2(-1.0, -1.0), - vec2( 1.0, 1.0), - vec2(-1.0, 1.0) -); - -@vertex -fn vs_main( - @builtin(vertex_index) vi: u32, - @builtin(instance_index) ii: u32 -) -> VSOut { - var out: VSOut; - if (ii >= arrayLength(&particles)) { - out.clip = vec4(0.0, 0.0, 2.0, 1.0); - return out; - } - - let particle = particles[ii]; - let corner = CORNERS[vi]; - - // Current position from state.xyz. - let pos = particle.state.xyz; - - // Base size from target_size.w. - let baseSize = particle.target_size.w; - - // Flash boost during ignition (frames 330–359). - let frame = params.frame; - var flashBoost = 1.0; - if (frame >= 330.0 && frame <= 359.0) { - let flashT = (frame - 330.0) / 29.0; // 0..1 over flash frames - // Sharp flash: peaks at frame 345, fades by 359. - flashBoost = 1.0 + 3.0 * (1.0 - smoothstep(330.0, 345.0, frame)) - + 2.0 * smoothstep(345.0, 359.0, frame); - } - - // Size: base + flash boost + pulse breathing. - let breath = 1.0 + 0.06 * params.pulse; - let halfSize = baseSize * 4.0 * breath * flashBoost; - - let world = pos - + camera.right.xyz * corner.x * halfSize - + camera.up.xyz * corner.y * halfSize; - - out.clip = camera.view_proj * vec4(world, 1.0); - out.uv = corner; - - // Color: violet dust (0.55, 0.32, 1.00) with spectral rim. - let phase = particle.color_phase.w; - let spectralW = fract(params.loop_phase + phase); - var spectralColor: vec3; - var stops = array, 4>( - vec3(0.55, 0.32, 1.00), // violet base - vec3(0.40, 0.60, 1.00), // blue-violet - vec3(0.70, 0.45, 1.00), // magenta-violet - vec3(0.55, 0.32, 1.00) // wrap - ); - let f = spectralW * 4.0; - let i = u32(floor(f)) % 4u; - let frac = f - floor(f); - spectralColor = mix(stops[i], stops[(i + 1u) % 4u], frac); - - // Alpha from state.w (convergence progress + fade). - let alpha = particle.state.w; - - out.color = spectralColor; - out.misc = vec4(baseSize, 0.0, 0.0, alpha); - return out; -} - -@fragment -fn fs_main(in: VSOut) -> @location(0) vec4 { - let d = length(in.uv); - if (d > 1.0) { - discard; - } - - let alpha = in.misc.w; - let core = smoothstep(0.25, 0.0, d); - let halo = pow(max(1.0 - d, 0.0), 2.0); - - // Additive glow: core + halo. - let intensity = core * 1.5 + halo * 0.6; - - // Flash boost during ignition. - let frame = params.frame; - var flash = 0.0; - if (frame >= 330.0 && frame <= 359.0) { - flash = smoothstep(330.0, 345.0, frame) * 2.0; - } - - let color = in.color * (intensity + flash); - - return vec4(color * params.brightness, 1.0); -} -`; - - const module = device.createShaderModule({ - label: 'observatory-birth-render', - code: particleRenderWGSL - }); - - this.renderPipeline = device.createRenderPipeline({ - label: 'observatory-birth-render', - layout: 'auto', - vertex: { module, entryPoint: 'vs_main' }, - fragment: { - module, - entryPoint: 'fs_main', - targets: [ - { - format: this.engine.sceneFormat, - blend: { - color: { srcFactor: 'one', dstFactor: 'one', operation: 'add' }, - alpha: { srcFactor: 'one', dstFactor: 'one', operation: 'add' } - } - } - ] - }, - primitive: { topology: 'triangle-list' } - }); - - const cameraBuffer = this.nodeRenderer.cameraUniformBuffer; - this.renderBindGroup = device.createBindGroup({ - label: 'observatory-birth-render-bind', - layout: this.renderPipeline!.getBindGroupLayout(0), - entries: [ - { binding: 0, resource: { buffer: this.engine.paramsBuffer! } }, - { binding: 1, resource: { buffer: cameraBuffer! } }, - { binding: 2, resource: { buffer: this.particleBuffer! } } - ] - }); - } - - private createHaloPipeline(device: GPUDevice): void { - // Flash halo: a glowing ring around the target node (B5). - const haloWGSL = /* wgsl */ ` -struct Params { - frame: f32, - loop_phase: f32, - node_count: f32, - edge_count: f32, - path_count: f32, - pulse: f32, - viewport_w: f32, - viewport_h: f32, - brightness: f32, - demo_id: f32, - time: f32, - _pad: f32, -}; - -struct Camera { - view_proj: mat4x4, - right: vec4, - up: vec4, -}; - -struct Node { - pos_radius: vec4, - vel_retention: vec4, - color_flags: vec4, - demo: vec4, -}; - -@group(0) @binding(0) var params: Params; -@group(0) @binding(1) var camera: Camera; -@group(0) @binding(2) var nodes: array; - -struct VSOut { - @builtin(position) clip: vec4, - @location(0) uv: vec2, -}; - -@vertex -fn vs_main( - @builtin(vertex_index) vi: u32, - @builtin(instance_index) ii: u32 -) -> VSOut { - var out: VSOut; - if (ii >= u32(params.node_count)) { - out.clip = vec4(0.0, 0.0, 2.0, 1.0); - return out; - } - - let node = nodes[ii]; - let flags = u32(node.color_flags.w); - let is_target = (flags & 4u) != 0u; // flag 2: is birth target - - if (!is_target) { - out.clip = vec4(0.0, 0.0, 2.0, 1.0); - return out; - } - - // Flash halo: only visible during ignition (frames 330–359). - let frame = params.frame; - if (frame < 330.0 || frame > 359.0) { - out.clip = vec4(0.0, 0.0, 2.0, 1.0); - return out; - } - - // Halo ring: expands during flash, fades by frame 359. - let flashT = (frame - 330.0) / 29.0; // 0..1 - let ringRadius = 0.3 + flashT * 0.5; // expands 0.3 → 0.8 - - // Quad centered on target position. - let pos = node.pos_radius.xyz; - let cornerX = (f32(vi) / 3.0 - 1.0); // -1, 0, 1 (3 unique x) - let cornerY = (f32(vi % 3) / 1.5 - 1.0); // -1, 0, 1 - - // We use 4 vertices for a simple quad (vi 0..3). - let cx = cornerX * ringRadius; - let cy = cornerY * ringRadius; - - let world = pos - + camera.right.xyz * cx - + camera.up.xyz * cy; - - out.clip = camera.view_proj * vec4(world, 1.0); - - // UV for radial fade. - out.uv = vec2(cx / ringRadius, cy / ringRadius); - - return out; -} - -@fragment -fn fs_main(in: VSOut) -> @location(0) vec4 { - let d = length(in.uv); - if (d > 0.7) { - discard; - } - - // Flash: white-hot core, violet rim. - let flashIntensity = 1.0 - smoothstep(0.0, 0.7, d); - let color = vec3(0.7, 0.4, 1.0) * flashIntensity * 2.0; - - // Fade out as flash ends. - let frame = params.frame; - let fadeOut = 1.0 - smoothstep(345.0, 359.0, frame); - - return vec4(color * params.brightness * fadeOut, 1.0); -} -`; - - const module = device.createShaderModule({ - label: 'observatory-birth-halo', - code: haloWGSL - }); - - this.haloPipeline = device.createRenderPipeline({ - label: 'observatory-birth-halo', - layout: 'auto', - vertex: { module, entryPoint: 'vs_main' }, - fragment: { - module, - entryPoint: 'fs_main', - targets: [ - { - format: this.engine.sceneFormat, - blend: { - color: { srcFactor: 'one', dstFactor: 'one', operation: 'add' }, - alpha: { srcFactor: 'one', dstFactor: 'one', operation: 'add' } - } - } - ] - }, - primitive: { topology: 'triangle-list' } - }); - - const cameraBuffer = this.nodeRenderer.cameraUniformBuffer; - this.haloBindGroup = device.createBindGroup({ - label: 'observatory-birth-halo-bind', - layout: this.haloPipeline!.getBindGroupLayout(0), - entries: [ - { binding: 0, resource: { buffer: this.engine.paramsBuffer! } }, - { binding: 1, resource: { buffer: cameraBuffer! } }, - { binding: 2, resource: { buffer: this.nodeRenderer.nodeStateBuffer! } } - ] - }); - } - - private createEngravePipeline(device: GPUDevice): void { - // Reuse path-ribbon shader for edge engraving (B6). - // The birth engraving uses the same triangle-strip ribbon pattern - // but with different beat timing (starts at frame 360). - - if (this.engraveStepCount === 0 || !this.engraveBuffer) return; - - const pathModule = device.createShaderModule({ - label: 'observatory-birth-engrave', - code: renderPathWGSL - }); - - this.engravePipeline = device.createRenderPipeline({ - label: 'observatory-birth-engrave-pipeline', - layout: 'auto', - vertex: { module: pathModule, entryPoint: 'vs_main' }, - fragment: { - module: pathModule, - entryPoint: 'fs_main', - targets: [ - { - format: this.engine.sceneFormat, - blend: { - color: { srcFactor: 'one', dstFactor: 'one', operation: 'add' }, - alpha: { srcFactor: 'one', dstFactor: 'one', operation: 'add' } - } - } - ] - }, - primitive: { topology: 'triangle-list' } - }); - - this.engraveBindGroup = device.createBindGroup({ - label: 'observatory-birth-engrave-bind', - layout: this.engravePipeline!.getBindGroupLayout(0), - entries: [ - { binding: 0, resource: { buffer: this.engine.paramsBuffer! } }, - { binding: 1, resource: { buffer: this.nodeRenderer.cameraUniformBuffer! } }, - { binding: 2, resource: { buffer: this.nodeRenderer.nodeStateBuffer! } }, - { binding: 3, resource: { buffer: this.engraveBuffer! } } - ] - }); - } - - /** FramePass.compute — run birth particle simulation. */ - compute(encoder: GPUCommandEncoder, frame: number): void { - // Only active when demoMode === 'engram-birth' (demo_id === 1). - const demoId = this.engine.params[9]; - this.active = demoId === 1; - - if (!this.active || !this.computePipeline || !this.computeBindGroup) return; - - const pass = encoder.beginComputePass({ label: 'observatory-birth-compute' }); - pass.setPipeline(this.computePipeline); - pass.setBindGroup(0, this.computeBindGroup); - pass.dispatchWorkgroups(Math.ceil(this.particleCount / 64)); - pass.end(); - } - - /** FramePass.render — draw particles, flash halo, edge engraving. */ - render(pass: GPURenderPassEncoder, frame: number): void { - if (!this.active) return; - - // B4: Draw particle billboards (instanced additive). - if (this.renderPipeline && this.renderBindGroup && this.particleCount > 0) { - pass.setPipeline(this.renderPipeline); - pass.setBindGroup(0, this.renderBindGroup); - pass.draw(QUAD_VERTS, this.particleCount); - } - - // B5: Draw flash halo (only during frames 330–359). - if (this.haloPipeline && this.haloBindGroup && frame >= FLASH_START && frame <= FLASH_END) { - pass.setPipeline(this.haloPipeline); - pass.setBindGroup(0, this.haloBindGroup); - // Draw one halo quad per node (most will be degenerate). - pass.draw(4, this.nodeRenderer.nodeCountValue); - } - - // B6: Draw edge engraving ribbons (starts at frame 360). - if (this.engravePipeline && this.engraveBindGroup && this.engraveStepCount > 0 && frame >= ENGRAVE_START) { - pass.setPipeline(this.engravePipeline); - pass.setBindGroup(0, this.engraveBindGroup); - pass.draw(6, this.engraveStepCount); - } - } - - dispose(): void { - this.particleBuffer?.destroy(); - this.particleBuffer = null; - // GPUComputePipeline.destroy() exists at runtime but older TS types omit it. - (this.computePipeline as any)?.destroy?.(); - this.computePipeline = null; - this.computeBindGroup = null; - // GPURenderPipeline.destroy() exists at runtime but older TS types omit it. - (this.renderPipeline as any)?.destroy?.(); - this.renderPipeline = null; - this.renderBindGroup = null; - (this.haloPipeline as any)?.destroy?.(); - this.haloPipeline = null; - this.haloBindGroup = null; - this.haloIndexBuffer?.destroy(); - this.haloIndexBuffer = null; - (this.engravePipeline as any)?.destroy?.(); - this.engravePipeline = null; - this.engraveBindGroup = null; - this.engraveBuffer?.destroy(); - this.engraveBuffer = null; - } -} diff --git a/apps/dashboard/src/lib/observatory/camera.ts b/apps/dashboard/src/lib/observatory/camera.ts deleted file mode 100644 index 5b77b6a..0000000 --- a/apps/dashboard/src/lib/observatory/camera.ts +++ /dev/null @@ -1,139 +0,0 @@ -/** - * Cognitive Observatory — minimal column-major mat4 camera math. - * - * Spec §6: no new dependencies — small local helpers instead of three.js. - * All outputs are Float32Array(16) in WebGPU/WGSL column-major order. - */ - -export type Mat4 = Float32Array; - -/** Perspective projection (right-handed, depth 0..1 as WebGPU expects). */ -export function perspective(fovYRad: number, aspect: number, near: number, far: number): Mat4 { - const f = 1 / Math.tan(fovYRad / 2); - const nf = 1 / (near - far); - // column-major - const m = new Float32Array(16); - m[0] = f / aspect; - m[5] = f; - m[10] = far * nf; - m[11] = -1; - m[14] = far * near * nf; - return m; -} - -/** Right-handed lookAt view matrix. */ -export function lookAt( - eye: [number, number, number], - target: [number, number, number], - up: [number, number, number] -): Mat4 { - const [ex, ey, ez] = eye; - let zx = ex - target[0]; - let zy = ey - target[1]; - let zz = ez - target[2]; - let len = Math.hypot(zx, zy, zz) || 1; - zx /= len; - zy /= len; - zz /= len; - - // x = up × z - let xx = up[1] * zz - up[2] * zy; - let xy = up[2] * zx - up[0] * zz; - let xz = up[0] * zy - up[1] * zx; - len = Math.hypot(xx, xy, xz) || 1; - xx /= len; - xy /= len; - xz /= len; - - // y = z × x - const yx = zy * xz - zz * xy; - const yy = zz * xx - zx * xz; - const yz = zx * xy - zy * xx; - - const m = new Float32Array(16); - m[0] = xx; - m[1] = yx; - m[2] = zx; - m[4] = xy; - m[5] = yy; - m[6] = zy; - m[8] = xz; - m[9] = yz; - m[10] = zz; - m[12] = -(xx * ex + xy * ey + xz * ez); - m[13] = -(yx * ex + yy * ey + yz * ez); - m[14] = -(zx * ex + zy * ey + zz * ez); - m[15] = 1; - return m; -} - -/** out = a × b (column-major). */ -export function multiply(a: Mat4, b: Mat4): Mat4 { - const out = new Float32Array(16); - for (let c = 0; c < 4; c++) { - for (let r = 0; r < 4; r++) { - out[c * 4 + r] = - a[r] * b[c * 4] + - a[4 + r] * b[c * 4 + 1] + - a[8 + r] * b[c * 4 + 2] + - a[12 + r] * b[c * 4 + 3]; - } - } - return out; -} - -export interface OrbitCamera { - viewProj: Mat4; - /** world-space camera right vector (billboarding) */ - right: [number, number, number]; - /** world-space camera up vector (billboarding) */ - up: [number, number, number]; - eye: [number, number, number]; -} - -/** - * Deterministic slow orbit camera: angle driven by the loop phase (frames), - * never wall clock — the same frame always yields the same view. Returns the - * view-projection plus the camera basis for GPU billboards. - */ -export function orbitCamera( - phase: number, - aspect: number, - distance: number, - elevation = 0.35 -): OrbitCamera { - const angle = phase * Math.PI * 2; - const eye: [number, number, number] = [ - Math.sin(angle) * distance, - distance * elevation, - Math.cos(angle) * distance - ]; - const proj = perspective((50 * Math.PI) / 180, aspect, 0.1, 4000); - const view = lookAt(eye, [0, 0, 0], [0, 1, 0]); - - // camera basis: forward = normalize(target - eye), right = f × up, up = r × f - let fx = -eye[0], - fy = -eye[1], - fz = -eye[2]; - let len = Math.hypot(fx, fy, fz) || 1; - fx /= len; - fy /= len; - fz /= len; - let rx = fy * 0 - fz * 1; - let ry = fz * 0 - fx * 0; - let rz = fx * 1 - fy * 0; - len = Math.hypot(rx, ry, rz) || 1; - rx /= len; - ry /= len; - rz /= len; - const ux = ry * fz - rz * fy; - const uy = rz * fx - rx * fz; - const uz = rx * fy - ry * fx; - - return { - viewProj: multiply(proj, view), - right: [rx, ry, rz], - up: [ux, uy, uz], - eye - }; -} diff --git a/apps/dashboard/src/lib/observatory/demo-clock.ts b/apps/dashboard/src/lib/observatory/demo-clock.ts deleted file mode 100644 index c02d38f..0000000 --- a/apps/dashboard/src/lib/observatory/demo-clock.ts +++ /dev/null @@ -1,137 +0,0 @@ -/** - * Deterministic demo clock for the Cognitive Observatory. - * - * Fixed 60fps loop, 720-frame period (12 seconds), seeded PRNG. - * No Math.random() or performance.now() for simulation state. - * performance.now() only schedules frames; positions/colors/path are deterministic. - * - * Pattern: https://gafferongames.com/post/fix_your_timestep/ - */ - -// ---- MurmurHash3-compatible 32-bit hash (xmur3) ---- -// Used to hash a seed string into a 32-bit integer for mulberry32. -function xmur3(str: string): () => number { - let h = 1779033703 ^ str.length; - for (let i = 0; i < str.length; i++) { - h = Math.imul(h ^ str.charCodeAt(i), 2654435761); - h = (h << 13) | (h >>> 19); - } - return function () { - let t = (h += 0x6d2b79f5); - t = Math.imul(t ^ (t >>> 15), t | 1); - t ^= Math.imul(t ^ (t >>> 7), t | 61); - return ((t ^ (t >>> 14)) >>> 0) / 4294967296; - }; -} - -// ---- Mulberry32 seeded PRNG ---- -// Fast, deterministic, 32-bit. Good enough for demo visuals. -function mulberry32(seed: number) { - return function () { - let t = (seed += 0x6d2b79f5); - t = Math.imul(t ^ (t >>> 15), t | 1); - t ^= Math.imul(t ^ (t >>> 7), t | 61); - return ((t ^ (t >>> 14)) >>> 0) / 4294967296; - }; -} - -// ---- DemoClock ---- -export interface DemoClockConfig { - /** Frames per second (fixed) */ - fps?: number; - /** Frames per loop (default 720 = 12s at 60fps) */ - loopFrames?: number; - /** Seed string for deterministic PRNG */ - seed: string; -} - -export interface DemoClockState { - /** Current frame (integer, wraps at loopFrames) */ - frame: number; - /** Loop phase: 0..1 */ - phase: number; - /** PRNG function seeded from the provided seed */ - rng: () => number; - /** Total frames elapsed (monotonic, does not wrap) */ - totalFrames: number; -} - -export class DemoClock { - private readonly fps: number; - private readonly loopFrames: number; - private readonly seedStr: string; - private _frame: number; - private _totalFrames: number; - private _rng: () => number; - - constructor(config: DemoClockConfig) { - this.fps = config.fps ?? 60; - this.loopFrames = config.loopFrames ?? 720; - this.seedStr = config.seed; - this._frame = 0; - this._totalFrames = 0; - // Hash the seed string into a 32-bit integer, then create a mulberry32 PRNG - const hash = xmur3(this.seedStr)(); - this._rng = mulberry32(Math.floor(hash * 2 ** 32)); - } - - /** Advance the clock by one frame. Returns the new state. */ - tick(): DemoClockState { - this._frame = (this._frame + 1) % this.loopFrames; - this._totalFrames++; - return this.state; - } - - /** Get the current clock state without advancing. */ - get state(): DemoClockState { - return { - frame: this._frame, - phase: this._frame / this.loopFrames, - rng: this._rng, - totalFrames: this._totalFrames - }; - } - - /** Reset the clock to frame 0. */ - reset(): void { - this._frame = 0; - this._totalFrames = 0; - // Re-seed the PRNG from the original seed - const hash = xmur3(this.seedStr)(); - this._rng = mulberry32(Math.floor(hash * 2 ** 32)); - } - - /** Get the loop duration in seconds. */ - get loopDuration(): number { - return this.loopFrames / this.fps; - } - - /** Frames per loop (capture mode needs this to freeze deterministically). */ - get framesPerLoop(): number { - return this.loopFrames; - } -} - -// ---- Utility: deterministic position on a golden-angle sphere ---- -// Golden-angle placement is deterministic by index. The rng provides -// a small seed-based perturbation so different seeds produce different layouts. -export function deterministicSpherePosition( - index: number, - total: number, - radius: number, - rng: () => number -): [number, number, number] { - const goldenAngle = Math.PI * (3 - Math.sqrt(5)); - const y = 1 - (index / (total - 1 || 1)) * 2; // -1 to 1 - const radiusAtY = Math.sqrt(1 - y * y); - const theta = goldenAngle * index; - const x = Math.cos(theta) * radiusAtY; - const z = Math.sin(theta) * radiusAtY; - - // Small seed-based perturbation (±5% of radius) - const px = (rng() - 0.5) * 0.1 * radius; - const py = (rng() - 0.5) * 0.1 * radius; - const pz = (rng() - 0.5) * 0.1 * radius; - - return [x * radius + px, y * radius + py, z * radius + pz]; -} diff --git a/apps/dashboard/src/lib/observatory/engine.ts b/apps/dashboard/src/lib/observatory/engine.ts deleted file mode 100644 index eb44dda..0000000 --- a/apps/dashboard/src/lib/observatory/engine.ts +++ /dev/null @@ -1,389 +0,0 @@ -/** - * Cognitive Observatory — WebGPU engine. - * - * Owns adapter/device/context lifecycle, the render loop, resize, and dispose. - * Increment 3 scope: boot WebGPU, clear to void #05060a, DPR-clamped resize, - * readable fallback when WebGPU is unavailable. Later increments register - * pipelines on top of this shell (spec §2, §3.4). - * - * Hard rules (spec §6): - * - No Math.random()/Date.now()/performance.now() deciding simulation state - * (the DemoClock is the only sim clock; rAF timestamps only schedule). - * - No GPU readback in the frame loop. - */ - -import { DemoClock } from './demo-clock'; -import { PARAMS_FLOATS, demoModeId, type DemoMode } from './types'; -import { PostChain, SCENE_FORMAT } from './post/post-chain'; -import { VOID_CLEAR_HDR } from './post/tone-reference'; - -/** - * Void background — visual DNA §7. #05060a as the DISPLAY value (public API, - * pre-post-stack). The HDR scene pass clears to VOID_CLEAR_HDR - * (post/tone-reference.ts), whose tonemapped composite result is exactly - * this color. - */ -export const VOID_CLEAR: GPUColor = { - r: 0x05 / 255, - g: 0x06 / 255, - b: 0x0a / 255, - a: 1 -}; - -export interface EngineOptions { - canvas: HTMLCanvasElement; - demo: DemoMode; - seed: string; - /** Max devicePixelRatio to render at (spec: DPR clamp). */ - maxDpr?: number; - /** Called once per completed frame with the loop frame index (telemetry). */ - onFrame?: (frame: number, fps: number) => void; - /** - * Capture mode (?frame=N): freeze the simulation at this exact loop frame. - * Same URL + frame → identical pixels, for shareable stills (spec §4 Inc 9). - */ - freezeFrame?: number | null; -} - -export type EngineStatus = - | { state: 'booting' } - | { state: 'running' } - | { state: 'unsupported'; reason: string } - | { state: 'error'; reason: string } - | { state: 'disposed' }; - -/** - * Frame hook contract for later increments: each registered pass gets the - * encoder + the main scene pass (offscreen HDR target) once per frame, after - * the clear. The post chain composites the scene to the swapchain afterwards. - */ -export interface FramePass { - /** Encode compute work (before the render pass). */ - compute?(encoder: GPUCommandEncoder, frame: number): void; - /** Encode draw calls inside the main render pass. */ - render?(pass: GPURenderPassEncoder, frame: number): void; -} - -export class ObservatoryEngine { - private canvas: HTMLCanvasElement; - private device: GPUDevice | null = null; - private context: GPUCanvasContext | null = null; - private format: GPUTextureFormat = 'bgra8unorm'; - - private clock: DemoClock; - private demo: DemoMode; - private freezeFrame: number | null; - private rafId = 0; - private running = false; - private disposed = false; - private maxDpr: number; - private onFrame?: (frame: number, fps: number) => void; - - /** Per-frame uniform data (layout: types.PARAMS_FLOATS). */ - readonly params = new Float32Array(PARAMS_FLOATS); - paramsBuffer: GPUBuffer | null = null; - - private passes: FramePass[] = []; - private post: PostChain | null = null; - private _status: EngineStatus = { state: 'booting' }; - private statusListeners = new Set<(s: EngineStatus) => void>(); - - // fps estimate for telemetry only (never sim state) - private lastRafTs = 0; - private fpsEstimate = 0; - - // Fixed-timestep accumulator (gafferongames.com/post/fix_your_timestep): - // wall clock ONLY schedules how many fixed 60Hz ticks to run — sim state - // stays a pure function of the frame index, so a 120Hz ProMotion display - // plays the same 12s loop as a 60Hz panel instead of double-speed. - private accumulatorMs = 0; - private static readonly FIXED_DT_MS = 1000 / 60; - - constructor(opts: EngineOptions) { - this.canvas = opts.canvas; - this.demo = opts.demo; - this.maxDpr = opts.maxDpr ?? 2; - this.onFrame = opts.onFrame; - this.clock = new DemoClock({ seed: opts.seed }); - this.freezeFrame = - typeof opts.freezeFrame === 'number' && Number.isFinite(opts.freezeFrame) - ? ((Math.floor(opts.freezeFrame) % this.clock.framesPerLoop) + - this.clock.framesPerLoop) % - this.clock.framesPerLoop - : null; - this.params[8] = 1; // brightness default — the void must never eat the field - } - - get status(): EngineStatus { - return this._status; - } - - get gpuDevice(): GPUDevice | null { - return this.device; - } - - get presentationFormat(): GPUTextureFormat { - return this.format; - } - - /** - * Format every FramePass render pipeline targets: the offscreen HDR scene - * texture (post stack input), NOT the swapchain. - */ - get sceneFormat(): GPUTextureFormat { - return SCENE_FORMAT; - } - - get demoClock(): DemoClock { - return this.clock; - } - - onStatus(cb: (s: EngineStatus) => void): () => void { - this.statusListeners.add(cb); - cb(this._status); - return () => this.statusListeners.delete(cb); - } - - private setStatus(s: EngineStatus) { - this._status = s; - for (const cb of this.statusListeners) cb(s); - } - - /** Register a frame pass (later increments: sim, nodes, edges, path, post). */ - addPass(pass: FramePass): void { - this.passes.push(pass); - } - - /** Boot WebGPU. Resolves true when running, false when unsupported/error. */ - async start(): Promise { - if (this.disposed) return false; - - const gpu = (navigator as Navigator & { gpu?: GPU }).gpu; - if (!gpu) { - this.setStatus({ - state: 'unsupported', - reason: 'WebGPU is not available in this browser.' - }); - return false; - } - - let adapter: GPUAdapter | null = null; - try { - adapter = await gpu.requestAdapter(); - } catch (e) { - this.setStatus({ - state: 'error', - reason: e instanceof Error ? e.message : 'requestAdapter failed' - }); - return false; - } - if (!adapter) { - this.setStatus({ - state: 'unsupported', - reason: 'No suitable GPU adapter found.' - }); - return false; - } - - try { - this.device = await adapter.requestDevice(); - } catch (e) { - this.setStatus({ - state: 'error', - reason: e instanceof Error ? e.message : 'requestDevice failed' - }); - return false; - } - if (this.disposed) { - // disposed while awaiting — release the device we just got - this.device?.destroy(); - this.device = null; - return false; - } - - this.device.lost.then((info) => { - if (this.disposed || info.reason === 'destroyed') return; - this.setStatus({ state: 'error', reason: `GPU device lost: ${info.message}` }); - this.stopLoop(); - }); - - // Surface shader/pipeline validation errors loudly — a silent black - // field is the worst failure mode an observatory can have. - this.device.onuncapturederror = (ev: GPUUncapturedErrorEvent) => { - console.error('[observatory] WebGPU error:', ev.error.message); - }; - - const context = this.canvas.getContext('webgpu'); - if (!context) { - this.setStatus({ state: 'error', reason: 'Could not get webgpu canvas context.' }); - return false; - } - this.context = context; - this.format = gpu.getPreferredCanvasFormat(); - this.configureContext(); - - // Per-frame uniform buffer (written by writeBuffer each frame; no readback). - this.paramsBuffer = this.device.createBuffer({ - label: 'observatory-params', - size: this.params.byteLength, - usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST - }); - - // Post stack (S1–S4): HDR scene → mip bloom → tonemap/grain/vignette. - // Sampler + explicit layouts + 4 pipelines build once here; textures - // are created on the first ensure() (resize or boot frame). - this.post = new PostChain(this.device, this.paramsBuffer, this.format); - - this.setStatus({ state: 'running' }); - this.running = true; - this.rafId = requestAnimationFrame(this.frame); - return true; - } - - /** Resize the drawing buffer to the canvas' CSS size × clamped DPR. */ - resize(): void { - if (!this.device || !this.context) return; - const dpr = Math.min(window.devicePixelRatio || 1, this.maxDpr); - const w = Math.max(1, Math.floor(this.canvas.clientWidth * dpr)); - const h = Math.max(1, Math.floor(this.canvas.clientHeight * dpr)); - if (this.canvas.width !== w || this.canvas.height !== h) { - this.canvas.width = w; - this.canvas.height = h; - // context.configure picks up the new size on next getCurrentTexture - this.configureContext(); - // HDR scene + bloom mip textures recreate on resize (idempotent). - this.post?.ensure(w, h); - } - } - - private configureContext(): void { - if (!this.device || !this.context) return; - this.context.configure({ - device: this.device, - format: this.format, - alphaMode: 'opaque' - }); - } - - private frame = (ts: number) => { - if (!this.running || !this.device || !this.context || !this.paramsBuffer || !this.post) - return; - - // fps estimate — telemetry only, never sim state - let deltaMs = 0; - if (this.lastRafTs > 0) { - deltaMs = ts - this.lastRafTs; - if (deltaMs > 0) this.fpsEstimate = Math.round(1000 / deltaMs); - } - this.lastRafTs = ts; - - // Fixed 60Hz timestep: advance the deterministic clock by however many - // whole ticks of wall time elapsed (clamped so a background tab doesn't - // fast-forward the story on return). The sequence of frames is identical - // on every display; only the scheduling reads the wall clock. - this.accumulatorMs += Math.min(deltaMs, 250); - let ticked = false; - while (this.accumulatorMs >= ObservatoryEngine.FIXED_DT_MS) { - this.clock.tick(); - this.accumulatorMs -= ObservatoryEngine.FIXED_DT_MS; - ticked = true; - } - // First rAF (deltaMs 0) still renders frame 0. - void ticked; - - // Capture mode (?frame=N) pins every derived value to one loop frame. - const state = this.clock.state; - const frame = this.freezeFrame ?? state.frame; - const phase = frame / this.clock.framesPerLoop; - - // Per-frame params (layout must match WGSL Params; types.ts doc block). - // EVERYTHING derives from the wrapped loop frame — never totalFrames — - // so the 720-frame loop is periodic by construction: loop k is pixel- - // identical to loop 1, recordings never pop at the seam, and a - // ?frame=N still matches what a live viewer sees at that playhead - // position forever. - const p = this.params; - p[0] = frame; - p[1] = phase; - // p[2] nodeCount, p[3] edgeCount, p[4] pathCount — set by graph upload (Inc 4+) - // Breath: exactly 4 cycles per 720-frame loop (0.333 Hz ≈ spec §7.2's - // ~0.32 Hz) — integer cycles/loop is what makes the seam invisible. - p[5] = 0.5 + 0.5 * Math.sin(2 * Math.PI * 4 * phase); - p[6] = this.canvas.width; - p[7] = this.canvas.height; - // p[8] brightness — set by the canvas component - p[9] = demoModeId(this.demo); - p[10] = frame / 60; // fixed sim seconds within the loop (wraps with it) - // p[11] capture_mode — 1.0 when freezeFrame is active (capture mode). - // When 1.0, the compute shader skips physics integration so the - // storage-buffer state stays frozen at the initial upload values, - // making same URL + frame → identical pixels (spec §4 Inc 9). - p[11] = this.freezeFrame !== null ? 1.0 : 0.0; - this.device.queue.writeBuffer(this.paramsBuffer, 0, p); - - let swapTex: GPUTexture; - try { - swapTex = this.context.getCurrentTexture(); - } catch { - // canvas hidden/zero-sized this frame — try again next frame - this.rafId = requestAnimationFrame(this.frame); - return; - } - // No-op unless the size changed — covers the boot frame before any resize. - this.post.ensure(swapTex.width, swapTex.height); - const swapView = swapTex.createView(); - - const encoder = this.device.createCommandEncoder({ label: 'observatory-frame' }); - - // Passes receive the freeze-adjusted frame — same value as params.frame, - // so capture mode pins hook-driven sim work too, not just uniforms. - for (const pass of this.passes) pass.compute?.(encoder, frame); - - const render = encoder.beginRenderPass({ - label: 'observatory-main', - colorAttachments: [ - { - view: this.post.sceneView, - clearValue: VOID_CLEAR_HDR, - loadOp: 'clear', - storeOp: 'store' - } - ] - }); - for (const pass of this.passes) pass.render?.(render, frame); - render.end(); - - // Post stack: bloom pyramid + composite (tonemap/grain/vignette) to the - // swapchain — same encoder, single submit, no readback. - this.post.encode(encoder, swapView); - - this.device.queue.submit([encoder.finish()]); - - this.onFrame?.(frame, this.fpsEstimate); - this.rafId = requestAnimationFrame(this.frame); - }; - - private stopLoop(): void { - this.running = false; - if (this.rafId !== 0) { - cancelAnimationFrame(this.rafId); - this.rafId = 0; - } - } - - dispose(): void { - if (this.disposed) return; - this.disposed = true; - this.stopLoop(); - this.paramsBuffer?.destroy(); - this.paramsBuffer = null; - this.post?.dispose(); - this.post = null; - this.device?.destroy(); - this.device = null; - this.context = null; - this.passes = []; - this.setStatus({ state: 'disposed' }); - this.statusListeners.clear(); - } -} diff --git a/apps/dashboard/src/lib/observatory/firewall-plan.ts b/apps/dashboard/src/lib/observatory/firewall-plan.ts deleted file mode 100644 index c47e327..0000000 --- a/apps/dashboard/src/lib/observatory/firewall-plan.ts +++ /dev/null @@ -1,358 +0,0 @@ -/** - * Cognitive Observatory — firewall demo plan (the immune response). - * - * Pure CPU: given the stable-indexed ObservatoryGraph + the demo seed, - * DETERMINISTICALLY pick the intruder (prefer failure/guardrail/confusion - * tags, else the lowest-retention leaf), precompute the per-node shockwave - * delays from the REAL layout (layoutPositions — never reimplemented), the - * severed-edge steps, the spine beats and the verdict copy. No Math.random(), - * no Date.now(). - * - * The 720-frame beat map (fixed 60Hz, seamless loop): - * 0-90 field at rest - * 90-150 INTRUSION — the suspicious memory flares sickly green-white - * (demo.y flare band (0..1], 36 integer sine cycles/loop) - * 150-330 CRIMSON SHOCKWAVE — a radial front expands from the intruder; - * per-node arrival A = 150 + delay, delay = round(144·dist/maxDist) - * (demo.w rim, amplitude fading with distance; all rims dead by 320) - * 330-480 QUARANTINE — probe beams to the intruder's neighbors flare then - * die one by one (kind 2, bf = 345 + 21k) while the MEMBRANE forms - * (demo.y membrane band [2.6..2.9] — the lane VALUE RANGE separates - * intrusion-flare (0..1] from membrane [2..3], one lane, two reads) - * 480-620 VERDICT overlay — "threat quarantined" (RescueVerdict, tone - * quarantine, fadeWindow 480/495/605/620) - * 620-720 every lane decays to EXACTLY zero (all releases r1 ≤ 680) - * - * `firewallEnvelopes` is the authoritative CPU mirror of - * shaders/firewall.wgsl.ts — the seam-zero unit test machine-checks the loop - * guarantee against it. - */ - -import { FLOATS_PER_NODE, PATH_KIND, type ObservatoryGraph } from './types'; -import type { PathStepMeta } from './path-builder'; -import { layoutPositions, truncateLabel } from './rescue-plan'; - -// --------------------------------------------------------------------------- -// Beat-map constants (shared with shaders/firewall.wgsl.ts — keep in lockstep) -// --------------------------------------------------------------------------- - -export const INTRUSION_FRAME = 90; -export const SHOCK_START = 150; -/** The front always crosses the field in exactly SHOCK_SPAN frames (adaptive speed). */ -export const SHOCK_SPAN = 144; -export const MEMBRANE_START = 330; -/** Sever step k lands at SEVER_BASE + k * SEVER_INTERVAL → 345..450. */ -export const SEVER_BASE = 345; -export const SEVER_INTERVAL = 21; -export const MAX_SEVERED = 6; -export const FIREWALL_VERDICT_START = 480; -export const FIREWALL_VERDICT_END = 620; -export const LOOP_FRAMES = 720; -/** Tags that mark a memory as suspicious (case-insensitive). */ -export const INTRUDER_TAGS: readonly string[] = ['failure', 'guardrail', 'confusion']; - -const TAU = Math.PI * 2; - -// --------------------------------------------------------------------------- -// Public types -// --------------------------------------------------------------------------- - -export interface FirewallVerdictCopy { - headline: 'threat quarantined'; - intruderLabel: string; - receipt: 'memory held in review · Memory PR opened'; -} - -export interface FirewallPlan { - /** false ⇒ field renders at rest, story suppressed (no fake intruder). */ - viable: boolean; - intruderIndex: number; - /** Unique intruder neighbors, ascending index, ≤ MAX_SEVERED. */ - severedNeighborIndices: number[]; - /** Per-node shock delay, round(SHOCK_SPAN·dist/maxDist), intruder 0. */ - shockDelays: number[]; - /** - * 1 u32/node: bits 0-7 shockDelay (0..144), bit 8 isIntruder, - * bit 9 isSeverNeighbor, bits 10-13 sever slot k. - */ - fireData: Uint32Array; - /** UINTS_PER_PATHSTEP u32 per step; min Uint32Array(4). */ - pathData: Uint32Array; - /** MUST be 1:1 with pathData steps (setPathSteps contract). */ - pathMetas: PathStepMeta[]; - /** Curated spine beats (route state only, NEVER sent to GPU). */ - spineBeats: PathStepMeta[]; - verdict: FirewallVerdictCopy; -} - -// --------------------------------------------------------------------------- -// Selection (exported for tests; all ties → ascending node index) -// --------------------------------------------------------------------------- - -/** - * Pick the intruder. Relaxation ladder, each tier ordered by - * (retention asc, index asc): - * 1. tagged (failure/guardrail/confusion) non-center unsuppressed - * 2. leaf (degree ≤ 1) non-center unsuppressed - * 3. any non-center unsuppressed - * 4. any non-center - * No candidate anywhere → -1 (plan viable:false). - */ -export function pickIntruderIndex(graph: ObservatoryGraph): number { - const n = graph.nodes.length; - if (n === 0) return -1; - const degree = new Uint32Array(n); - for (const e of graph.edges) { - degree[e.sourceIndex]++; - degree[e.targetIndex]++; - } - const isTagged = (i: number): boolean => - graph.nodes[i].tags.some((t) => INTRUDER_TAGS.includes(t.toLowerCase())); - - const tiers: Array<(i: number) => boolean> = [ - (i) => i !== graph.centerIndex && !graph.nodes[i].suppressed && isTagged(i), - (i) => i !== graph.centerIndex && !graph.nodes[i].suppressed && degree[i] <= 1, - (i) => i !== graph.centerIndex && !graph.nodes[i].suppressed, - (i) => i !== graph.centerIndex - ]; - for (const accept of tiers) { - let best = -1; - for (let i = 0; i < n; i++) { - if (!accept(i)) continue; - // strict < keeps the lowest index on retention ties (ascending scan) - if (best < 0 || graph.nodes[i].retention < graph.nodes[best].retention) best = i; - } - if (best >= 0) return best; - } - return -1; -} - -/** - * Per-node shockwave delay from LAYOUT distance to the intruder: - * delay_i = round(SHOCK_SPAN · dist_i / maxDist), clamped [0, 255], intruder 0. - * maxDist is floored 1e-6 → 1.0 (co-located degenerate: everything fires at - * once — safe, deterministic). Adaptive speed: the front always crosses the - * whole field in exactly SHOCK_SPAN frames. - */ -export function computeShockDelays( - positions: Float32Array, - nodeCount: number, - intruderIndex: number -): number[] { - const ix = positions[intruderIndex * FLOATS_PER_NODE + 0]; - const iy = positions[intruderIndex * FLOATS_PER_NODE + 1]; - const iz = positions[intruderIndex * FLOATS_PER_NODE + 2]; - const dists = new Array(nodeCount); - let maxDist = 0; - for (let i = 0; i < nodeCount; i++) { - const dx = positions[i * FLOATS_PER_NODE + 0] - ix; - const dy = positions[i * FLOATS_PER_NODE + 1] - iy; - const dz = positions[i * FLOATS_PER_NODE + 2] - iz; - const d = Math.sqrt(dx * dx + dy * dy + dz * dz); - dists[i] = d; - if (d > maxDist) maxDist = d; - } - if (maxDist < 1e-6) maxDist = 1.0; - const delays = new Array(nodeCount); - for (let i = 0; i < nodeCount; i++) { - delays[i] = Math.min(255, Math.max(0, Math.round((SHOCK_SPAN * dists[i]) / maxDist))); - } - delays[intruderIndex] = 0; - return delays; -} - -/** - * The severed edges: unique intruder neighbors (self-loops excluded — also - * already dropped by buildObservatoryGraph — and deduped), ascending index, - * capped at MAX_SEVERED. Edgeless intruder → [] (still viable: the membrane - * forms around a memory with nothing to sever). - */ -export function pickSeveredNeighbors(graph: ObservatoryGraph, intruderIndex: number): number[] { - const nbrs = new Set(); - for (const e of graph.edges) { - if (e.sourceIndex === intruderIndex && e.targetIndex !== intruderIndex) { - nbrs.add(e.targetIndex); - } - if (e.targetIndex === intruderIndex && e.sourceIndex !== intruderIndex) { - nbrs.add(e.sourceIndex); - } - } - return Array.from(nbrs) - .sort((a, b) => a - b) - .slice(0, MAX_SEVERED); -} - -export function severFrame(k: number): number { - return SEVER_BASE + SEVER_INTERVAL * k; -} - -// --------------------------------------------------------------------------- -// Envelope math — the authoritative CPU mirror of shaders/firewall.wgsl.ts -// --------------------------------------------------------------------------- - -function smooth(a: number, b: number, f: number): number { - const t = Math.min(1, Math.max(0, (f - a) / (b - a))); - return t * t * (3 - 2 * t); -} - -function env(f: number, a0: number, a1: number, r0: number, r1: number): number { - return smooth(a0, a1, f) * (1 - smooth(r0, r1, f)); -} - -/** - * Pure function of (frame, packed fire word) → the four demo lanes - * (x ALWAYS 0, y intruder flare/membrane, z ALWAYS 0, w shock rim/sever blink). - * Every envelope has attack a0 ≥ 90 and release r1 ≤ 680 ⇒ exactly 0 at - * frames 0 and 719 — the machine-checked seam guarantee. Sines are factors on - * zero-at-seam envelopes and run INTEGER cycles per loop (36 flare, 12 - * membrane). demo.y value ranges: intrusion flare (0..1], membrane - * [2.60..2.90] — the fragment shader separates the two reads by range. - * Keep in lockstep with firewall_choreo in shaders/firewall.wgsl.ts. - */ -export function firewallEnvelopes( - frame: number, - packed: number -): { x: number; y: number; z: number; w: number } { - const delay = packed & 0xff; - const isIntruder = (packed & 0x100) !== 0; - const isSever = (packed & 0x200) !== 0; - const k = (packed >>> 10) & 0xf; - const loopPhase = frame / LOOP_FRAMES; - - let y = 0; - let w = 0; - if (isIntruder) { - // Intrusion flare: sickly strobe, band (0..1]. C¹ handoff into the - // membrane over 330-332 — the rise sweeps the flare band exactly once - // (the condensation read is intentional). - y = env(frame, 90, 96, 310, 332) * (0.55 + 0.45 * Math.sin(TAU * 36 * loopPhase)); - // Membrane: sustained ring band [2.60..2.90], slow shimmer. - y += env(frame, 330, 352, 620, 680) * (2.75 + 0.15 * Math.sin(TAU * 12 * loopPhase)); - // Source detonation as the front leaves. - w = env(frame, 148, 153, 162, 196); - } else { - const a = SHOCK_START + delay; - const amp = 0.9 - 0.45 * (delay / SHOCK_SPAN); - // Crimson rim as the front passes; A ∈ [150, 294] ⇒ dead by 320 < 330. - w = amp * env(frame, a - 2, a + 3, a + 8, a + 26); - if (isSever) { - // Node-side receipt of the severed edge; last release 450+24 = 474. - const sk = severFrame(k); - w += 0.6 * env(frame, sk - 4, sk, sk + 6, sk + 24); - } - } - // x and z are hard 0.0: the recall/thin-film and horizon grammars can - // never fire in demo 4 (enforced by the lane-hygiene test). - return { x: 0, y, z: 0, w }; -} - -// --------------------------------------------------------------------------- -// Plan builder -// --------------------------------------------------------------------------- - -const UINTS_PER_STEP = 4; - -function emptyPlan(nodeCount: number): FirewallPlan { - return { - viable: false, - intruderIndex: -1, - severedNeighborIndices: [], - shockDelays: [], - fireData: new Uint32Array(nodeCount), - pathData: new Uint32Array(4), - pathMetas: [], - spineBeats: [], - verdict: { - headline: 'threat quarantined', - intruderLabel: '', - receipt: 'memory held in review · Memory PR opened' - } - }; -} - -/** - * Build the full deterministic firewall plan. Same graph + seed → identical - * plan (byte-identical typed arrays). Empty/center-only graphs survive with - * viable:false — the field breathes, nothing pretends to be a threat. - */ -export function buildFirewallPlan(graph: ObservatoryGraph, seed: string): FirewallPlan { - const n = graph.nodes.length; - const intruderIndex = pickIntruderIndex(graph); - if (n === 0 || intruderIndex < 0) return emptyPlan(n); - - // Shock delays come from the REAL layout (rescue-plan.layoutPositions — - // byte-identical to NodeRenderer.upload, never reimplemented). - const positions = layoutPositions(graph, seed); - const shockDelays = computeShockDelays(positions, n, intruderIndex); - const severed = pickSeveredNeighbors(graph, intruderIndex); - - // --- fireData packing (1 u32/node; every node carries its shock delay) --- - const fireData = new Uint32Array(n); - for (let i = 0; i < n; i++) fireData[i] = shockDelays[i] & 0xff; - fireData[intruderIndex] = 0x100; // intruder: delay forced 0 - severed.forEach((idx, k) => { - fireData[idx] |= 0x200 | (k << 10); - }); - - // --- PathStep emission: probe beams (kind 2) intruder → neighbor, one per - // severed edge — they flare then die (the visible severing). - // Window invariant: bf−46 ≥ 0 and bf+90 ≤ 719 for bf ∈ [345, 450]. - const pathData = new Uint32Array(Math.max(1, severed.length) * UINTS_PER_STEP); - const pathMetas: PathStepMeta[] = []; - severed.forEach((idx, k) => { - const bf = severFrame(k); - pathData[k * UINTS_PER_STEP + 0] = intruderIndex; - pathData[k * UINTS_PER_STEP + 1] = idx; - pathData[k * UINTS_PER_STEP + 2] = bf; - pathData[k * UINTS_PER_STEP + 3] = PATH_KIND.probe; - pathMetas.push({ - sourceIndex: intruderIndex, - targetIndex: idx, - beatFrame: bf, - kind: PATH_KIND.probe, - beatKind: 'sever', - nodeId: graph.nodes[idx].id, - label: truncateLabel(graph.nodes[idx].label) - }); - }); - - // --- Curated spine beats (unique, strictly increasing beatFrames) --- - const intruderLabel = truncateLabel(graph.nodes[intruderIndex].label); - const spineBeats: PathStepMeta[] = []; - const spine = (beatFrame: number, label: string, nodeId: string) => { - spineBeats.push({ - sourceIndex: intruderIndex, - targetIndex: intruderIndex, - beatFrame, - kind: 1, - beatKind: 'firewall', - nodeId, - label - }); - }; - spine(INTRUSION_FRAME, `intrusion · ${intruderLabel}`, graph.nodes[intruderIndex].id); - spine(SHOCK_START, 'immune response · shockwave', 'firewall-shock'); - spine(MEMBRANE_START, 'membrane forming', 'firewall-membrane'); - severed.forEach((idx, k) => { - spine(severFrame(k), `edge severed ✗ · ${truncateLabel(graph.nodes[idx].label)}`, graph.nodes[idx].id); - }); - spine(FIREWALL_VERDICT_START, 'threat quarantined', 'firewall-verdict'); - - const verdict: FirewallVerdictCopy = { - headline: 'threat quarantined', - intruderLabel, - receipt: 'memory held in review · Memory PR opened' - }; - - return { - viable: true, - intruderIndex, - severedNeighborIndices: severed, - shockDelays, - fireData, - pathData, - pathMetas, - spineBeats, - verdict - }; -} diff --git a/apps/dashboard/src/lib/observatory/firewall-renderer.ts b/apps/dashboard/src/lib/observatory/firewall-renderer.ts deleted file mode 100644 index 2a484a2..0000000 --- a/apps/dashboard/src/lib/observatory/firewall-renderer.ts +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Cognitive Observatory — firewall choreography pass. - * - * Compute-only FramePass (no render(): nodes and probe beams draw via the - * NodeRenderer's existing pipelines). Uploads the per-node packed fire word - * once (static), then each frame extends the choreography INTO the NodeState - * demo lanes as a pure function of (frame, role, shockDelay) — no stateful - * integration, so capture mode (?frame=N) works with zero special-casing. - * - * PASS ORDER IS LOAD-BEARING: this pass MUST be constructed AFTER the - * NodeRenderer (the route guarantees it: handleReady creates NodeRenderer, - * the upload $effect creates FirewallRenderer) so firewall_choreo encodes - * AFTER recall_sim in the same encoder and its demo-lane overwrite wins. - * recall_sim rewrites demo.x every frame with an afterglow window of - * bf+40..bf+200 — the k=5 sever beam at bf=450 would otherwise carry residual - * ignition into the verdict window. - * - * Three independent walls keep OTHER demos pixel-identical: - * (a) the route constructs this renderer only in the firewall branch, - * (b) compute() gates on params[9] === 4 ('firewall' demo index), - * (c) the demo-4 vertex/fragment terms in render-nodes.wgsl are themselves - * gated on params.demo_id == 4.0. - */ - -import type { ObservatoryEngine, FramePass } from './engine'; -import type { NodeRenderer } from './node-renderer'; -import type { FirewallPlan } from './firewall-plan'; -import { firewallWGSL } from './shaders/firewall.wgsl'; - -/** DEMO_MODES.indexOf('firewall') — types.ts, verified index 4. */ -const FIREWALL_DEMO_ID = 4; - -export interface FirewallRendererOptions { - engine: ObservatoryEngine; - nodeRenderer: NodeRenderer; - plan: FirewallPlan; -} - -export class FirewallRenderer implements FramePass { - private engine: ObservatoryEngine; - private nodeRenderer: NodeRenderer; - private plan: FirewallPlan; - - private pipeline: GPUComputePipeline | null = null; - private bindGroup: GPUBindGroup | null = null; - private fireBuffer: GPUBuffer | null = null; - - constructor(opts: FirewallRendererOptions) { - this.engine = opts.engine; - this.nodeRenderer = opts.nodeRenderer; - this.plan = opts.plan; - this.engine.addPass(this); - } - - /** Create the fire buffer + compute pipeline. Call after NodeRenderer.upload(). */ - upload(): void { - const device = this.engine.gpuDevice; - if (!device || !this.engine.paramsBuffer) return; - if (!this.plan.viable) return; - if (!this.nodeRenderer.nodeStateBuffer || this.nodeRenderer.nodeCountValue === 0) return; - - this.fireBuffer?.destroy(); - this.fireBuffer = device.createBuffer({ - label: 'observatory-firewall-fire', - size: Math.max(4, this.plan.fireData.byteLength), - usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST - }); - device.queue.writeBuffer(this.fireBuffer, 0, this.plan.fireData.buffer as ArrayBuffer); - - const module = device.createShaderModule({ - label: 'observatory-firewall-choreo', - code: firewallWGSL - }); - this.pipeline = device.createComputePipeline({ - label: 'observatory-firewall-choreo', - layout: 'auto', - compute: { module, entryPoint: 'firewall_choreo' } - }); - - // EXACTLY the 3 declared bindings — auto layout strips unused bindings - // and binding anything extra invalidates the group (the BirthRenderer - // lesson, birth-renderer.ts createComputePipeline). - this.bindGroup = device.createBindGroup({ - label: 'observatory-firewall-bind', - layout: this.pipeline.getBindGroupLayout(0), - entries: [ - { binding: 0, resource: { buffer: this.engine.paramsBuffer } }, - { binding: 1, resource: { buffer: this.nodeRenderer.nodeStateBuffer } }, - { binding: 2, resource: { buffer: this.fireBuffer } } - ] - }); - } - - /** FramePass — overwrite the four demo lanes for this frame (pure of frame). */ - compute(encoder: GPUCommandEncoder): void { - if (this.engine.params[9] !== FIREWALL_DEMO_ID) return; - if (!this.pipeline || !this.bindGroup) return; - const n = this.nodeRenderer.nodeCountValue; - if (n === 0) return; - - const pass = encoder.beginComputePass({ label: 'observatory-firewall-choreo' }); - pass.setPipeline(this.pipeline); - pass.setBindGroup(0, this.bindGroup); - pass.dispatchWorkgroups(Math.ceil(n / 64)); - pass.end(); - } - - dispose(): void { - this.fireBuffer?.destroy(); - this.fireBuffer = null; - this.pipeline = null; - this.bindGroup = null; - } -} diff --git a/apps/dashboard/src/lib/observatory/forgetting-plan.ts b/apps/dashboard/src/lib/observatory/forgetting-plan.ts deleted file mode 100644 index 5b5aaf2..0000000 --- a/apps/dashboard/src/lib/observatory/forgetting-plan.ts +++ /dev/null @@ -1,307 +0,0 @@ -/** - * Cognitive Observatory — forgetting-horizon demo plan (FSRS as a living system). - * - * Pure CPU: given the stable-indexed ObservatoryGraph, DETERMINISTICALLY pick - * the drifting set (the lowest-retention ~25% of memories), the 3 rescued - * memories (mid-retention, well-connected — the ones a recall would plausibly - * save), the packed per-node role words, the recall PathSteps and the spine - * beats. No Math.random(), no Date.now(), no layout needed — selection is a - * pure function of the graph. - * - * The 720-frame beat map (fixed 60Hz, seamless loop): - * 0-90 field at rest - * 90-420 THE DRIFT — drifting memories dim and fall toward the horizon - * (demo.z rises to the 0.55 plateau, staggered by retention rank) - * 300-480 THE RESCUES — 3 fading memories are recalled one by one (recall - * ribbons land at 318/378/438; demo.z snaps back, demo.x ignites) - * 480-660 the unrescued sink to near-black (demo.z → exactly 1.0 by 640; - * the fragment floor keeps them at ~6% — never gone, always - * retrievable) - * 660-720 master release — every lane decays to EXACTLY zero by frame 712 - * - * `forgettingEnvelopes` and `horizonDrift` are the authoritative CPU mirrors - * of shaders/forgetting.wgsl.ts and the demo-3 branch of render-nodes.wgsl.ts; - * the seam-zero unit test machine-checks the loop guarantee against them. - */ - -import { PATH_KIND, type ObservatoryGraph } from './types'; -import type { PathStepMeta } from './path-builder'; -import { truncateLabel } from './rescue-plan'; - -// --------------------------------------------------------------------------- -// Beat-map constants (shared with shaders/forgetting.wgsl.ts — keep in lockstep) -// --------------------------------------------------------------------------- - -export const DRIFT_ONSET_BASE = 90; -export const DRIFT_ONSET_SPREAD = 42; -export const DRIFT_ENGULF = 210; -export const PHASE1_LEVEL = 0.55; -export const PHASE2_LEVEL = 0.45; -export const PHASE2_BASE = 480; -export const PHASE2_STAGGER = 24; -export const PHASE2_END = 640; -/** Rescue k ribbon lands at RESCUE_BASE + k * RESCUE_INTERVAL → 318/378/438. */ -export const RESCUE_BASE = 318; -export const RESCUE_INTERVAL = 60; -export const MASTER_R0 = 660; -export const MASTER_R1 = 712; -export const LOOP_FRAMES = 720; -export const FORGETTING_K = 3; -/** Fading spine beat k lands at FADING_BASE + k * FADING_INTERVAL → 132/192/252. */ -export const FADING_BASE = 132; -export const FADING_INTERVAL = 60; -export const SINK_BEAT_FRAME = 540; -export const RETRIEVABLE_BEAT_FRAME = 660; - -// --------------------------------------------------------------------------- -// Public types -// --------------------------------------------------------------------------- - -export interface ForgettingPlan { - /** false ⇒ field renders at rest, story suppressed (no fake drift). */ - viable: boolean; - /** Drifting node indices, retention asc (rank order). */ - driftingIndices: number[]; - /** Rescued node indices, slot order k = 0..K-1. Always ⊆ driftingIndices. */ - rescuedIndices: number[]; - /** - * 1 u32/node: bits 0-7 rank (0..255 across the drifting set), - * bit 8 isDrifting, bit 9 isRescued, bits 10-11 rescue slot k. - * Non-drifting nodes (incl. the center) are exactly 0. - */ - horizonData: Uint32Array; - /** UINTS_PER_PATHSTEP u32 per step; min Uint32Array(4). */ - pathData: Uint32Array; - /** MUST be 1:1 with pathData steps (setPathSteps contract). */ - pathMetas: PathStepMeta[]; - /** Curated spine beats (route state only, NEVER sent to GPU). */ - spineBeats: PathStepMeta[]; -} - -// --------------------------------------------------------------------------- -// Selection (exported for tests; all ties → ascending node index) -// --------------------------------------------------------------------------- - -/** - * The drifting set: exclude the center, sort (retention asc, index asc), take - * driftCount = min(n−1, max(min(3, n−1), round(0.25·(n−1)))). Suppressed - * memories are eligible — suppression is not forgetting. - */ -export function pickDrifting(graph: ObservatoryGraph): number[] { - const cand: number[] = []; - for (let i = 0; i < graph.nodes.length; i++) { - if (i === graph.centerIndex) continue; - cand.push(i); - } - cand.sort((a, b) => graph.nodes[a].retention - graph.nodes[b].retention || a - b); - const eligible = cand.length; - if (eligible === 0) return []; - const driftCount = Math.min( - eligible, - Math.max(Math.min(FORGETTING_K, eligible), Math.round(0.25 * eligible)) - ); - return cand.slice(0, driftCount); -} - -/** - * The rescued: within the drifting set, the K = min(3, driftCount) most - * plausible recall targets — score = 2·retention + min(degree, 8)/8 - * (mid-retention, well-connected), sort (score desc, index asc). - */ -export function pickRescued(graph: ObservatoryGraph, drifting: number[]): number[] { - const degree = new Uint32Array(graph.nodes.length); - for (const e of graph.edges) { - degree[e.sourceIndex]++; - degree[e.targetIndex]++; - } - const score = (i: number): number => - 2 * graph.nodes[i].retention + Math.min(degree[i], 8) / 8; - const sorted = drifting.slice().sort((a, b) => score(b) - score(a) || a - b); - return sorted.slice(0, Math.min(FORGETTING_K, drifting.length)); -} - -export function rescueFrame(k: number): number { - return RESCUE_BASE + RESCUE_INTERVAL * k; -} - -// --------------------------------------------------------------------------- -// Envelope math — the authoritative CPU mirror of shaders/forgetting.wgsl.ts -// --------------------------------------------------------------------------- - -function smooth(a: number, b: number, f: number): number { - const t = Math.min(1, Math.max(0, (f - a) / (b - a))); - return t * t * (3 - 2 * t); -} - -function env(f: number, a0: number, a1: number, r0: number, r1: number): number { - return smooth(a0, a1, f) * (1 - smooth(r0, r1, f)); -} - -/** - * Pure function of (frame, packed horizon word) → the four demo lanes - * (x rescue ignition, y ALWAYS 0, z horizon fade-and-fall, w ALWAYS 0). - * Every term has attack a0 ≥ 90 (⇒ exactly 0 at frame 0) and is multiplied by - * the master release 1−smoothstep(660, 712, f) (⇒ exactly 0 at frame 719) — - * the machine-checked seam guarantee. No sines anywhere in this moment. - * Keep in lockstep with forgetting_choreo in shaders/forgetting.wgsl.ts. - */ -export function forgettingEnvelopes( - frame: number, - packed: number -): { x: number; y: number; z: number; w: number } { - const isDrifting = (packed & 0x100) !== 0; - if (!isDrifting) return { x: 0, y: 0, z: 0, w: 0 }; - - const rank01 = (packed & 0xff) / 255; - const isRescued = (packed & 0x200) !== 0; - const k = (packed >>> 10) & 0x3; - - const onset = DRIFT_ONSET_BASE + DRIFT_ONSET_SPREAD * rank01; - const master = 1 - smooth(MASTER_R0, MASTER_R1, frame); - const phase1 = PHASE1_LEVEL * smooth(onset, onset + DRIFT_ENGULF, frame); - - let x = 0; - let z = 0; - if (isRescued) { - const rk = rescueFrame(k); - // Snap-back starts 22 frames before the recall ribbon lands at rk — - // the memory visibly rises to meet the call; exactly 0 from rk+6. - z = master * phase1 * (1 - smooth(rk - 22, rk + 6, frame)); - // Ignition rides the EXISTING recall response (thin-film + white core - // + 0.9·recall swell in render-nodes.wgsl) for free; released ≤ rk+130. - x = master * env(frame, rk - 26, rk, rk + 60, rk + 130); - } else { - const phase2 = - PHASE2_LEVEL * smooth(PHASE2_BASE + PHASE2_STAGGER * rank01, PHASE2_END, frame); - // Plateau 0.55 by ≤342, then sink to exactly 1.0 over 640..660. - z = master * (phase1 + phase2); - } - - return { x, y: 0, z, w: 0 }; -} - -/** - * CPU mirror of the demo-3 VERTEX displacement in render-nodes.wgsl.ts. - * Visual-only, world-space: down (−34·dz) and away from the field axis - * (+22·dz radially in the xz plane) ⇒ |drift| ≈ 40.5 units at dz = 1. - * pos_radius is NEVER written — the force sim owns positions. - */ -export function horizonDrift( - dz: number, - p: [number, number, number] -): [number, number, number] { - if (dz <= 0) return [0, 0, 0]; - const dzc = Math.min(1, Math.max(0, dz)); - const rXz = Math.max(Math.hypot(p[0], p[2]), 0.001); - const ax = p[0] / rXz; - const az = p[2] / rXz; - return [dzc * ax * 22, dzc * -34, dzc * az * 22]; -} - -// --------------------------------------------------------------------------- -// Plan builder -// --------------------------------------------------------------------------- - -const UINTS_PER_STEP = 4; - -function emptyPlan(nodeCount: number): ForgettingPlan { - return { - viable: false, - driftingIndices: [], - rescuedIndices: [], - horizonData: new Uint32Array(nodeCount), - pathData: new Uint32Array(4), - pathMetas: [], - spineBeats: [] - }; -} - -/** - * Build the full deterministic forgetting-horizon plan. Same graph → - * identical plan (byte-identical typed arrays). Empty/1-node graphs survive - * with viable:false — the field breathes, nothing pretends to be forgotten. - */ -export function buildForgettingPlan(graph: ObservatoryGraph): ForgettingPlan { - const n = graph.nodes.length; - const drifting = pickDrifting(graph); - if (n < 2 || drifting.length < 1) return emptyPlan(n); - - const rescued = pickRescued(graph, drifting); - const driftCount = drifting.length; - - // --- horizonData packing (1 u32/node; non-drifting stays exactly 0) --- - const horizonData = new Uint32Array(n); - drifting.forEach((idx, i) => { - const rank = Math.round((255 * i) / Math.max(1, driftCount - 1)); - horizonData[idx] = (rank & 0xff) | 0x100; - }); - rescued.forEach((idx, k) => { - horizonData[idx] |= 0x200 | (k << 10); - }); - - // --- PathStep emission: K recall ribbons, center → rescued_k --- - // Window invariant: bf−46 ≥ 0 and bf+90 ≤ 719 for bf ∈ {318, 378, 438}. - const pathData = new Uint32Array(Math.max(1, rescued.length) * UINTS_PER_STEP); - const pathMetas: PathStepMeta[] = []; - rescued.forEach((idx, k) => { - const bf = rescueFrame(k); - pathData[k * UINTS_PER_STEP + 0] = graph.centerIndex; - pathData[k * UINTS_PER_STEP + 1] = idx; - pathData[k * UINTS_PER_STEP + 2] = bf; - pathData[k * UINTS_PER_STEP + 3] = PATH_KIND.recall; - pathMetas.push({ - sourceIndex: graph.centerIndex, - targetIndex: idx, - beatFrame: bf, - kind: PATH_KIND.recall, - beatKind: 'recall', - nodeId: graph.nodes[idx].id, - label: truncateLabel(graph.nodes[idx].label) - }); - }); - - // --- Curated spine beats (unique, strictly increasing beatFrames) --- - const spineBeats: PathStepMeta[] = []; - const spine = (beatFrame: number, kind: number, label: string, nodeId: string) => { - spineBeats.push({ - sourceIndex: graph.centerIndex, - targetIndex: graph.centerIndex, - beatFrame, - kind, - beatKind: 'horizon', - nodeId, - label - }); - }; - - // ≤3 lowest-retention NON-rescued drifting memories narrate the fade - // (drifting is already retention-asc; emit only beats whose subject exists). - const rescuedSet = new Set(rescued); - const fading = drifting.filter((i) => !rescuedSet.has(i)).slice(0, 3); - fading.forEach((idx, j) => { - const pct = Math.round(graph.nodes[idx].retention * 100); - spine( - FADING_BASE + FADING_INTERVAL * j, - 1, - `fading: ${truncateLabel(graph.nodes[idx].label)} · retention ${pct}%`, - graph.nodes[idx].id - ); - }); - rescued.forEach((idx, k) => { - spine(rescueFrame(k), 0, `recalled: ${truncateLabel(graph.nodes[idx].label)}`, graph.nodes[idx].id); - }); - if (fading.length > 0) { - spine(SINK_BEAT_FRAME, 1, 'the unrecalled sink · nothing is deleted', 'horizon-sink'); - } - spine(RETRIEVABLE_BEAT_FRAME, 0, 'every memory still retrievable', 'horizon-retrievable'); - - return { - viable: true, - driftingIndices: drifting, - rescuedIndices: rescued, - horizonData, - pathData, - pathMetas, - spineBeats - }; -} diff --git a/apps/dashboard/src/lib/observatory/forgetting-renderer.ts b/apps/dashboard/src/lib/observatory/forgetting-renderer.ts deleted file mode 100644 index eb5615e..0000000 --- a/apps/dashboard/src/lib/observatory/forgetting-renderer.ts +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Cognitive Observatory — forgetting-horizon choreography pass. - * - * Compute-only FramePass (no render(): nodes and ribbons draw via the - * NodeRenderer's existing pipelines). Uploads the per-node packed horizon word - * once (static), then each frame extends the choreography INTO the NodeState - * demo lanes as a pure function of (frame, role, rank) — no stateful - * integration, so capture mode (?frame=N) works with zero special-casing. - * - * PASS ORDER IS LOAD-BEARING: this pass MUST be constructed AFTER the - * NodeRenderer (the route guarantees it: handleReady creates NodeRenderer, - * the upload $effect creates ForgettingRenderer) so forgetting_choreo encodes - * AFTER recall_sim in the same encoder and its demo-lane overwrite wins. - * recall_sim rewrites demo.x every frame with an afterglow window of - * bf+40..bf+200 — the k=2 rescue ribbon at bf=438 would otherwise carry - * residual ignition into the master release. - * - * Three independent walls keep OTHER demos pixel-identical: - * (a) the route constructs this renderer only in the forgetting-horizon branch, - * (b) compute() gates on params[9] === 3 ('forgetting-horizon' demo index), - * (c) the demo-3 vertex/fragment terms in render-nodes.wgsl are themselves - * gated on params.demo_id == 3.0. - */ - -import type { ObservatoryEngine, FramePass } from './engine'; -import type { NodeRenderer } from './node-renderer'; -import type { ForgettingPlan } from './forgetting-plan'; -import { forgettingWGSL } from './shaders/forgetting.wgsl'; - -/** DEMO_MODES.indexOf('forgetting-horizon') — types.ts, verified index 3. */ -const HORIZON_DEMO_ID = 3; - -export interface ForgettingRendererOptions { - engine: ObservatoryEngine; - nodeRenderer: NodeRenderer; - plan: ForgettingPlan; -} - -export class ForgettingRenderer implements FramePass { - private engine: ObservatoryEngine; - private nodeRenderer: NodeRenderer; - private plan: ForgettingPlan; - - private pipeline: GPUComputePipeline | null = null; - private bindGroup: GPUBindGroup | null = null; - private horizonBuffer: GPUBuffer | null = null; - - constructor(opts: ForgettingRendererOptions) { - this.engine = opts.engine; - this.nodeRenderer = opts.nodeRenderer; - this.plan = opts.plan; - this.engine.addPass(this); - } - - /** Create the horizon buffer + compute pipeline. Call after NodeRenderer.upload(). */ - upload(): void { - const device = this.engine.gpuDevice; - if (!device || !this.engine.paramsBuffer) return; - if (!this.plan.viable) return; - if (!this.nodeRenderer.nodeStateBuffer || this.nodeRenderer.nodeCountValue === 0) return; - - this.horizonBuffer?.destroy(); - this.horizonBuffer = device.createBuffer({ - label: 'observatory-forgetting-horizon', - size: Math.max(4, this.plan.horizonData.byteLength), - usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST - }); - device.queue.writeBuffer(this.horizonBuffer, 0, this.plan.horizonData.buffer as ArrayBuffer); - - const module = device.createShaderModule({ - label: 'observatory-forgetting-choreo', - code: forgettingWGSL - }); - this.pipeline = device.createComputePipeline({ - label: 'observatory-forgetting-choreo', - layout: 'auto', - compute: { module, entryPoint: 'forgetting_choreo' } - }); - - // EXACTLY the 3 declared bindings — auto layout strips unused bindings - // and binding anything extra invalidates the group (the BirthRenderer - // lesson, birth-renderer.ts createComputePipeline). - this.bindGroup = device.createBindGroup({ - label: 'observatory-forgetting-bind', - layout: this.pipeline.getBindGroupLayout(0), - entries: [ - { binding: 0, resource: { buffer: this.engine.paramsBuffer } }, - { binding: 1, resource: { buffer: this.nodeRenderer.nodeStateBuffer } }, - { binding: 2, resource: { buffer: this.horizonBuffer } } - ] - }); - } - - /** FramePass — overwrite the four demo lanes for this frame (pure of frame). */ - compute(encoder: GPUCommandEncoder): void { - if (this.engine.params[9] !== HORIZON_DEMO_ID) return; - if (!this.pipeline || !this.bindGroup) return; - const n = this.nodeRenderer.nodeCountValue; - if (n === 0) return; - - const pass = encoder.beginComputePass({ label: 'observatory-forgetting-choreo' }); - pass.setPipeline(this.pipeline); - pass.setBindGroup(0, this.bindGroup); - pass.dispatchWorkgroups(Math.ceil(n / 64)); - pass.end(); - } - - dispose(): void { - this.horizonBuffer?.destroy(); - this.horizonBuffer = null; - this.pipeline = null; - this.bindGroup = null; - } -} diff --git a/apps/dashboard/src/lib/observatory/graph-upload.ts b/apps/dashboard/src/lib/observatory/graph-upload.ts deleted file mode 100644 index eb89401..0000000 --- a/apps/dashboard/src/lib/observatory/graph-upload.ts +++ /dev/null @@ -1,143 +0,0 @@ -/** - * Cognitive Observatory — graph → GPU upload preparation. - * - * Converts the API GraphResponse into stable-indexed typed arrays matching the - * NodeState buffer layout (types.ts). Pure functions, fully unit-testable. - * - * Determinism contract (spec §6): node ordering is stable (sorted by id, center - * first), positions come from deterministicSpherePosition seeded by the - * DemoClock PRNG — same seed → identical layout, different seed → different. - * - * Visual DNA §7.1 — meaning layer: base color = FSRS state color from the real - * dashboard palette (lib/graph/nodes.ts, used verbatim). Aha/failure/confusion - * tags override, exactly like the Graph3D 'ahagraph' mode. - */ - -import type { GraphResponse } from '$types'; -import { getMemoryState, getAhaGraphColor, MEMORY_STATE_COLORS } from '$lib/graph/nodes'; -import { - FLOATS_PER_NODE, - NODE_LANE, - NODE_FLAG, - UINTS_PER_EDGE, - toObservatoryNode, - type ObservatoryGraph, - type ObservatoryNode, - type ObservatoryEdge -} from './types'; -import { deterministicSpherePosition } from './demo-clock'; - -/** Parse '#rrggbb' to 0..1 rgb. Falls back to slate on malformed input. */ -export function hexToRgb01(hex: string): [number, number, number] { - const m = /^#?([0-9a-fA-F]{6})$/.exec(hex.trim()); - if (!m) return [0x6b / 255, 0x72 / 255, 0x80 / 255]; // unavailable slate - const v = parseInt(m[1], 16); - return [((v >> 16) & 0xff) / 255, ((v >> 8) & 0xff) / 255, (v & 0xff) / 255]; -} - -/** - * Meaning-layer base color for a node (visual DNA §7.1). - * Tag kinds override via the REAL Graph3D mapping (getAhaGraphColor — case- - * insensitive, aha gold → confusion red → failure/guardrail grey), else the - * FSRS state palette. Reused verbatim so Observatory and Graph3D never drift. - */ -export function nodeBaseColor(node: ObservatoryNode): [number, number, number] { - const tagColor = getAhaGraphColor({ tags: node.tags }); - if (tagColor) return hexToRgb01(tagColor); - return hexToRgb01(MEMORY_STATE_COLORS[getMemoryState(node.retention)]); -} - -/** - * Build the stable-indexed observatory graph. - * Ordering: center node first (index 0), then remaining nodes sorted by id — - * deterministic regardless of API response order. - */ -export function buildObservatoryGraph(response: GraphResponse): ObservatoryGraph { - const sorted = [...response.nodes].sort((a, b) => { - if (a.isCenter !== b.isCenter) return a.isCenter ? -1 : 1; - return a.id < b.id ? -1 : a.id > b.id ? 1 : 0; - }); - - const nodes: ObservatoryNode[] = sorted.map((n, i) => toObservatoryNode(n, i)); - const indexById = new Map(); - for (const n of nodes) indexById.set(n.id, n.index); - - const edges: ObservatoryEdge[] = []; - for (const e of response.edges) { - const s = indexById.get(e.source); - const t = indexById.get(e.target); - if (s === undefined || t === undefined || s === t) continue; - edges.push({ sourceIndex: s, targetIndex: t, weight: e.weight, type: e.type }); - } - - const centerIndex = nodes.findIndex((n) => n.isCenter); - return { nodes, edges, indexById, centerIndex: centerIndex < 0 ? 0 : centerIndex }; -} - -export interface NodeStateBuild { - /** FLOATS_PER_NODE floats per node, ready for the storage buffer. */ - data: Float32Array; - nodeCount: number; -} - -/** - * Fill the NodeState array. `rng` must come from a fresh DemoClock seeded with - * the demo seed so the layout is reproducible. - */ -export function buildNodeStateArray( - graph: ObservatoryGraph, - rng: () => number, - fieldRadius = 120 -): NodeStateBuild { - const n = graph.nodes.length; - const data = new Float32Array(n * FLOATS_PER_NODE); - - for (let i = 0; i < n; i++) { - const node = graph.nodes[i]; - const base = i * FLOATS_PER_NODE; - - // lane 0: position + visual radius - const [x, y, z] = - node.isCenter && graph.centerIndex === i - ? [0, 0, 0] // the center memory anchors the field - : deterministicSpherePosition(i, n, fieldRadius, rng); - // radius: retention-weighted, center node reads largest (meaning layer) - const radius = node.isCenter ? 4.2 : 1.4 + node.retention * 1.8; - data[base + NODE_LANE.posRadius + 0] = x; - data[base + NODE_LANE.posRadius + 1] = y; - data[base + NODE_LANE.posRadius + 2] = z; - data[base + NODE_LANE.posRadius + 3] = radius; - - // lane 1: velocity (rest) + retention - data[base + NODE_LANE.velRetention + 3] = node.retention; - - // lane 2: base color + packed flags - const [r, g, b] = nodeBaseColor(node); - let flags = 0; - if (node.isCenter) flags |= NODE_FLAG.isCenter; - if (node.suppressed) flags |= NODE_FLAG.suppressed; - // case-insensitive, mirroring getAhaGraphColor's tag semantics - const tags = new Set(node.tags.map((t) => t.toLowerCase())); - if (tags.has('aha')) flags |= NODE_FLAG.isAha; - if (tags.has('failure') || tags.has('guardrail')) flags |= NODE_FLAG.isFailure; - if (tags.has('confusion') || tags.has('weak-spot')) flags |= NODE_FLAG.isConfusion; - data[base + NODE_LANE.colorFlags + 0] = r; - data[base + NODE_LANE.colorFlags + 1] = g; - data[base + NODE_LANE.colorFlags + 2] = b; - data[base + NODE_LANE.colorFlags + 3] = flags; - - // lane 3: demo activations start at zero - } - - return { data, nodeCount: n }; -} - -/** array> edge index buffer. */ -export function buildEdgeIndexArray(graph: ObservatoryGraph): Uint32Array { - const data = new Uint32Array(Math.max(1, graph.edges.length) * UINTS_PER_EDGE); - graph.edges.forEach((e, i) => { - data[i * UINTS_PER_EDGE] = e.sourceIndex; - data[i * UINTS_PER_EDGE + 1] = e.targetIndex; - }); - return data; -} diff --git a/apps/dashboard/src/lib/observatory/node-renderer.ts b/apps/dashboard/src/lib/observatory/node-renderer.ts deleted file mode 100644 index e250dad..0000000 --- a/apps/dashboard/src/lib/observatory/node-renderer.ts +++ /dev/null @@ -1,416 +0,0 @@ -/** - * Cognitive Observatory — instanced node renderer (Increment 4). - * - * Owns the NodeState/EdgeIndex GPU buffers and the additive billboard - * pipeline. Registers as a FramePass on the engine: camera uniforms are - * written from the deterministic loop phase each frame (orbit), nodes draw - * as instanced soft-glow sprites straight from the storage buffer. - * - * The RENDER LOOP does no GPU readback and holds no wall-clock state - * (spec §6 — deterministic frames). pickAt() is the one sanctioned, - * input-driven readback: click-frequency only, never per-frame. - */ - -import type { GraphResponse } from '$types'; -import type { ObservatoryEngine, FramePass } from './engine'; -import { DemoClock } from './demo-clock'; -import { orbitCamera } from './camera'; -import { - buildObservatoryGraph, - buildNodeStateArray, - buildEdgeIndexArray -} from './graph-upload'; -import { FLOATS_PER_NODE, NODE_LANE, type ObservatoryGraph } from './types'; -import { renderNodesWGSL } from './shaders/render-nodes.wgsl'; -import { simulateWGSL } from './shaders/simulate.wgsl'; -import { renderPathWGSL } from './shaders/render-path.wgsl'; -import { buildRecallPath, type PathStepMeta } from './path-builder'; - -/** mat4 (16) + right vec4 (4) + up vec4 (4) floats. */ -const CAMERA_FLOATS = 24; - -/** Orbit distance fitted to the default field radius (graph-upload). */ -const ORBIT_DISTANCE = 300; - -export class NodeRenderer implements FramePass { - private engine: ObservatoryEngine; - private pipeline: GPURenderPipeline | null = null; - private bindGroup: GPUBindGroup | null = null; - private cameraBuffer: GPUBuffer | null = null; - private nodeBuffer: GPUBuffer | null = null; - private edgeBuffer: GPUBuffer | null = null; - private cameraData = new Float32Array(CAMERA_FLOATS); - private nodeCount = 0; - - // Recall-path simulation (Increment 5) - private simPipeline: GPUComputePipeline | null = null; - private simBindGroup: GPUBindGroup | null = null; - private pathBuffer: GPUBuffer | null = null; - - // Path edge wavefront (Increment 6) - private pathPipeline: GPURenderPipeline | null = null; - private pathBindGroup: GPUBindGroup | null = null; - private pathStepCount = 0; - - graph: ObservatoryGraph | null = null; - /** Beat metadata for the timeline spine overlay (Increment 6). */ - pathSteps: PathStepMeta[] = []; - - constructor(engine: ObservatoryEngine) { - this.engine = engine; - engine.addPass(this); - } - - /** - * Upload the graph into GPU buffers. Layout is deterministic: positions - * come from a fresh DemoClock PRNG seeded with `seed` (same seed → - * identical field, Increment 4 gate). - */ - upload(response: GraphResponse, seed: string, opts?: { recallPath?: boolean }): void { - const device = this.engine.gpuDevice; - if (!device) return; - // Other demo modes (engram-birth, …) bring their own choreography and - // upload with recallPath: false so the recall wave stays quiet. - const includeRecallPath = opts?.recallPath ?? true; - - const graph = buildObservatoryGraph(response); - this.graph = graph; - - const layoutClock = new DemoClock({ seed }); - const { data, nodeCount } = buildNodeStateArray(graph, layoutClock.state.rng); - this.nodeCount = nodeCount; - - this.nodeBuffer?.destroy(); - // COPY_SRC exists solely for pickAt()'s click-time readback. - this.nodeBuffer = device.createBuffer({ - label: 'observatory-node-state', - size: Math.max(data.byteLength, 64), - usage: - GPUBufferUsage.STORAGE | - GPUBufferUsage.COPY_DST | - GPUBufferUsage.COPY_SRC | - GPUBufferUsage.VERTEX - }); - device.queue.writeBuffer(this.nodeBuffer, 0, data.buffer as ArrayBuffer); - - const edgeData = buildEdgeIndexArray(graph); - this.edgeBuffer?.destroy(); - this.edgeBuffer = device.createBuffer({ - label: 'observatory-edge-index', - size: edgeData.byteLength, - usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST - }); - device.queue.writeBuffer(this.edgeBuffer, 0, edgeData.buffer as ArrayBuffer); - - // Recall path: deterministic story beats → PathStep storage buffer. - // (Skipped for demo modes with their own choreography — the buffer is - // still created so the sim bind group stays valid, just with 0 steps.) - const recall = includeRecallPath - ? buildRecallPath(response, graph) - : { steps: [], data: new Uint32Array(4) }; - this.pathSteps = recall.steps; - this.pathBuffer?.destroy(); - this.pathBuffer = device.createBuffer({ - label: 'observatory-path-steps', - size: recall.data.byteLength, - usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST - }); - device.queue.writeBuffer(this.pathBuffer, 0, recall.data.buffer as ArrayBuffer); - this.pathStepCount = this.pathSteps.length; - - // Per-frame counts for every shader that reads Params. - this.engine.params[2] = nodeCount; - this.engine.params[3] = graph.edges.length; - this.engine.params[4] = this.pathSteps.length; - - if (!this.cameraBuffer) { - this.cameraBuffer = device.createBuffer({ - label: 'observatory-camera', - size: this.cameraData.byteLength, - usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST - }); - } - - this.createPipeline(device); - } - - /** - * Replace the PathStep buffer after upload (Moment B: the birth engrave - * steps ride the same wavefront machinery as recall). Rebuilds the - * pipelines/bind groups so they reference the new buffer. - */ - setPathSteps(data: Uint32Array, steps: PathStepMeta[]): void { - const device = this.engine.gpuDevice; - if (!device) return; - this.pathSteps = steps; - this.pathStepCount = steps.length; - this.pathBuffer?.destroy(); - this.pathBuffer = device.createBuffer({ - label: 'observatory-path-steps', - size: Math.max(data.byteLength, 16), - usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST - }); - device.queue.writeBuffer(this.pathBuffer, 0, data.buffer as ArrayBuffer); - this.engine.params[4] = steps.length; - this.createPipeline(device); - } - - private createPipeline(device: GPUDevice): void { - if (!this.engine.paramsBuffer || !this.cameraBuffer || !this.nodeBuffer) return; - - // Recall-path simulation pipeline (compute-boids pattern, §1). - if (this.pathBuffer) { - const simModule = device.createShaderModule({ - label: 'observatory-simulate', - code: simulateWGSL - }); - this.simPipeline = device.createComputePipeline({ - label: 'observatory-recall-sim', - layout: 'auto', - compute: { module: simModule, entryPoint: 'recall_sim' } - }); - const simEntries: GPUBindGroupEntry[] = [ - { binding: 0, resource: { buffer: this.engine.paramsBuffer } }, - { binding: 1, resource: { buffer: this.nodeBuffer } }, - { binding: 2, resource: { buffer: this.pathBuffer } } - ]; - if (this.edgeBuffer) { - simEntries.push({ binding: 3, resource: { buffer: this.edgeBuffer } }); - } - this.simBindGroup = device.createBindGroup({ - label: 'observatory-recall-sim-bind', - layout: this.simPipeline.getBindGroupLayout(0), - entries: simEntries - }); - } - - const module = device.createShaderModule({ - label: 'observatory-render-nodes', - code: renderNodesWGSL - }); - - this.pipeline = device.createRenderPipeline({ - label: 'observatory-nodes', - layout: 'auto', - vertex: { module, entryPoint: 'vs_main' }, - fragment: { - module, - entryPoint: 'fs_main', - targets: [ - { - format: this.engine.sceneFormat, - // Additive: light accumulates on the void (§7.2). - blend: { - color: { srcFactor: 'one', dstFactor: 'one', operation: 'add' }, - alpha: { srcFactor: 'one', dstFactor: 'one', operation: 'add' } - } - } - ] - }, - primitive: { topology: 'triangle-list' } - }); - - this.bindGroup = device.createBindGroup({ - label: 'observatory-nodes-bind', - layout: this.pipeline.getBindGroupLayout(0), - entries: [ - { binding: 0, resource: { buffer: this.engine.paramsBuffer } }, - { binding: 1, resource: { buffer: this.cameraBuffer } }, - { binding: 2, resource: { buffer: this.nodeBuffer } } - ] - }); - - // Path wavefront ribbons (Increment 6) — drawn after nodes, additive. - if (this.pathBuffer) { - const pathModule = device.createShaderModule({ - label: 'observatory-render-path', - code: renderPathWGSL - }); - this.pathPipeline = device.createRenderPipeline({ - label: 'observatory-path', - layout: 'auto', - vertex: { module: pathModule, entryPoint: 'vs_main' }, - fragment: { - module: pathModule, - entryPoint: 'fs_main', - targets: [ - { - format: this.engine.sceneFormat, - blend: { - color: { srcFactor: 'one', dstFactor: 'one', operation: 'add' }, - alpha: { srcFactor: 'one', dstFactor: 'one', operation: 'add' } - } - } - ] - }, - primitive: { topology: 'triangle-list' } - }); - this.pathBindGroup = device.createBindGroup({ - label: 'observatory-path-bind', - layout: this.pathPipeline.getBindGroupLayout(0), - entries: [ - { binding: 0, resource: { buffer: this.engine.paramsBuffer } }, - { binding: 1, resource: { buffer: this.cameraBuffer } }, - { binding: 2, resource: { buffer: this.nodeBuffer } }, - { binding: 3, resource: { buffer: this.pathBuffer } } - ] - }); - } - } - - /** - * FramePass — write the deterministic orbit camera, then run the - * recall-path simulation for this frame (compute before render). - */ - compute(encoder: GPUCommandEncoder): void { - const device = this.engine.gpuDevice; - if (!device || !this.cameraBuffer) return; - - const w = this.engine.params[6] || 1; - const h = this.engine.params[7] || 1; - const phase = this.engine.params[1]; - - const cam = orbitCamera(phase, w / h, ORBIT_DISTANCE); - this.cameraData.set(cam.viewProj, 0); - this.cameraData[16] = cam.right[0]; - this.cameraData[17] = cam.right[1]; - this.cameraData[18] = cam.right[2]; - this.cameraData[19] = 0; - this.cameraData[20] = cam.up[0]; - this.cameraData[21] = cam.up[1]; - this.cameraData[22] = cam.up[2]; - this.cameraData[23] = 0; - device.queue.writeBuffer(this.cameraBuffer, 0, this.cameraData); - - if (this.simPipeline && this.simBindGroup && this.nodeCount > 0) { - const pass = encoder.beginComputePass({ label: 'observatory-recall-sim' }); - pass.setPipeline(this.simPipeline); - pass.setBindGroup(0, this.simBindGroup); - pass.dispatchWorkgroups(Math.ceil(this.nodeCount / 64)); - pass.end(); - } - } - - /** FramePass — instanced additive draws: nodes, then path ribbons on top. */ - render(pass: GPURenderPassEncoder): void { - if (!this.pipeline || !this.bindGroup || this.nodeCount === 0) return; - pass.setPipeline(this.pipeline); - pass.setBindGroup(0, this.bindGroup); - pass.draw(6, this.nodeCount); - - if (this.pathPipeline && this.pathBindGroup && this.pathStepCount > 0) { - pass.setPipeline(this.pathPipeline); - pass.setBindGroup(0, this.pathBindGroup); - pass.draw(6, this.pathStepCount); - } - } - - // --------------------------------------------------------------------------- - // Read-only handles for BirthRenderer (Moment B, Task B2). - // No behavior change — these expose existing buffers for other passes. - // --------------------------------------------------------------------------- - - get nodeStateBuffer(): GPUBuffer | null { - return this.nodeBuffer; - } - - get cameraUniformBuffer(): GPUBuffer | null { - return this.cameraBuffer; - } - - get nodeCountValue(): number { - return this.nodeCount; - } - - get pathStepMeta(): PathStepMeta[] { - return this.pathSteps; - } - - /** - * Click picking — the ONE sanctioned GPU readback (input-driven, never - * per-frame, so the render loop's determinism contract holds). - * - * Copies the live NodeState buffer (post force-sim positions) to a staging - * buffer, reprojects every node through the SAME deterministic orbit camera - * the current frame used (engine params: phase + canvas size), and returns - * the nearest node whose projected disc contains the click. - * - * @param ndcX click x in NDC (-1..1, right = +) - * @param ndcY click y in NDC (-1..1, up = +) - */ - async pickAt(ndcX: number, ndcY: number): Promise<{ index: number; id: string } | null> { - const device = this.engine.gpuDevice; - if (!device || !this.nodeBuffer || !this.graph || this.nodeCount === 0) return null; - - const byteSize = this.nodeCount * FLOATS_PER_NODE * 4; - const staging = device.createBuffer({ - label: 'observatory-pick-staging', - size: byteSize, - usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ - }); - const encoder = device.createCommandEncoder({ label: 'observatory-pick-copy' }); - encoder.copyBufferToBuffer(this.nodeBuffer, 0, staging, 0, byteSize); - device.queue.submit([encoder.finish()]); - let data: Float32Array; - try { - await staging.mapAsync(GPUMapMode.READ); - data = new Float32Array(staging.getMappedRange().slice(0)); - } catch { - staging.destroy(); - return null; - } - staging.unmap(); - staging.destroy(); - - // Same camera inputs the frame pass uses (compute() above). - const w = this.engine.params[6] || 1; - const h = this.engine.params[7] || 1; - const phase = this.engine.params[1]; - const m = orbitCamera(phase, w / h, ORBIT_DISTANCE).viewProj; // column-major - - // Projected-disc hit test: fovY 50° → f = 1/tan(25°); a node of world - // radius r at clip-w distance projects to ~r·f/w in NDC y. A small - // floor keeps faint distant nodes clickable; score <1.6 allows a - // forgiving halo around the disc; lowest score (closest relative to - // its disc) wins. - const f = 1 / Math.tan((50 * Math.PI) / 360); - let best = -1; - let bestScore = Infinity; - for (let i = 0; i < this.nodeCount; i++) { - const b = i * FLOATS_PER_NODE + NODE_LANE.posRadius; - const x = data[b]; - const y = data[b + 1]; - const z = data[b + 2]; - const r = data[b + 3]; - const cw = m[3] * x + m[7] * y + m[11] * z + m[15]; - if (cw <= 0) continue; // behind the camera - const cx = (m[0] * x + m[4] * y + m[8] * z + m[12]) / cw; - const cy = (m[1] * x + m[5] * y + m[9] * z + m[13]) / cw; - const projR = Math.max((r * f) / cw, 0.012); - const score = Math.hypot(cx - ndcX, cy - ndcY) / projR; - if (score < 1.6 && score < bestScore) { - bestScore = score; - best = i; - } - } - if (best < 0) return null; - return { index: best, id: this.graph.nodes[best].id }; - } - - dispose(): void { - this.nodeBuffer?.destroy(); - this.edgeBuffer?.destroy(); - this.cameraBuffer?.destroy(); - this.pathBuffer?.destroy(); - this.nodeBuffer = null; - this.edgeBuffer = null; - this.cameraBuffer = null; - this.pathBuffer = null; - this.pipeline = null; - this.bindGroup = null; - this.simPipeline = null; - this.simBindGroup = null; - this.pathPipeline = null; - this.pathBindGroup = null; - } -} diff --git a/apps/dashboard/src/lib/observatory/overlays/RescueVerdict.svelte b/apps/dashboard/src/lib/observatory/overlays/RescueVerdict.svelte deleted file mode 100644 index aca3c96..0000000 --- a/apps/dashboard/src/lib/observatory/overlays/RescueVerdict.svelte +++ /dev/null @@ -1,119 +0,0 @@ - - -{#if opacity > 0.001} -
                    -
                    {verdict.headline}
                    -
                    {verdict.causeLabel}
                    -
                    {verdict.receipt}
                    -
                    -{/if} - - diff --git a/apps/dashboard/src/lib/observatory/overlays/TelemetryStrip.svelte b/apps/dashboard/src/lib/observatory/overlays/TelemetryStrip.svelte deleted file mode 100644 index 44fb4d9..0000000 --- a/apps/dashboard/src/lib/observatory/overlays/TelemetryStrip.svelte +++ /dev/null @@ -1,104 +0,0 @@ - - - -
                    -
                    - -
                    - - {demoMode} - - -
                    - - - - - -
                    - frame: {String(frameCount).padStart(3, ' ')} - {#if freezeFrame !== null} - CAPTURE - {:else if fpsEstimate > 0} - - {fpsEstimate}fps - - {/if} - -
                    -
                    -
                    - - - diff --git a/apps/dashboard/src/lib/observatory/overlays/TimelineSpine.svelte b/apps/dashboard/src/lib/observatory/overlays/TimelineSpine.svelte deleted file mode 100644 index 469fdc5..0000000 --- a/apps/dashboard/src/lib/observatory/overlays/TimelineSpine.svelte +++ /dev/null @@ -1,128 +0,0 @@ - - -{#if steps.length > 0} -
                    - {#if activeLabel} -
                    {activeLabel}
                    - {/if} -
                    - {#each steps as s (s.beatFrame)} -
                    0} - class:backward={s.kind === 1} - style="left: {pct(s.beatFrame)}%; opacity: {0.45 + - 0.55 * beatEnergy(s.beatFrame, frame)}" - title={s.label} - >
                    - {/each} -
                    -
                    -
                    -{/if} - - diff --git a/apps/dashboard/src/lib/observatory/path-builder.ts b/apps/dashboard/src/lib/observatory/path-builder.ts deleted file mode 100644 index e67c188..0000000 Binary files a/apps/dashboard/src/lib/observatory/path-builder.ts and /dev/null differ diff --git a/apps/dashboard/src/lib/observatory/post/mip-plan.ts b/apps/dashboard/src/lib/observatory/post/mip-plan.ts deleted file mode 100644 index f9c8c0e..0000000 --- a/apps/dashboard/src/lib/observatory/post/mip-plan.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Cognitive Observatory — bloom mip-chain plan (S2). - * - * Pure, GPU-free math so vitest can pin the pyramid shape. Mip 0 is HALF the - * swapchain resolution (the classic mip-bloom cost/quality point); the chain - * stops before the smallest dimension would drop below 8px (degenerate - * anisotropic mips smear) and is clamped to at most 6 levels. - * - * NOTE bloom RADIUS therefore varies with viewport size (more mips on bigger - * canvases = wider glow). Brightness does NOT — the composite normalizes the - * additive up-chain by textureNumLevels (post-chain.ts). - */ - -export interface BloomMipPlan { - baseW: number; - baseH: number; - mipCount: number; - sizes: Array<[number, number]>; -} - -export function planBloomMips(w: number, h: number): BloomMipPlan { - const baseW = Math.max(1, w >> 1); - const baseH = Math.max(1, h >> 1); // mip 0 = HALF res - // min-dim ≥ 8px at the smallest mip (avoids degenerate anisotropic mips). - const mipCount = Math.min( - 6, - Math.max(1, 1 + Math.floor(Math.log2(Math.min(baseW, baseH) / 8))) - ); - const sizes = Array.from({ length: mipCount }, (_, i): [number, number] => [ - Math.max(1, baseW >> i), - Math.max(1, baseH >> i) - ]); - return { baseW, baseH, mipCount, sizes }; -} diff --git a/apps/dashboard/src/lib/observatory/post/post-chain.ts b/apps/dashboard/src/lib/observatory/post/post-chain.ts deleted file mode 100644 index c5b1e63..0000000 --- a/apps/dashboard/src/lib/observatory/post/post-chain.ts +++ /dev/null @@ -1,340 +0,0 @@ -/** - * Cognitive Observatory — post-processing chain (S1–S4). - * - * Owns EVERY GPU resource of the post stack; the engine holds exactly one - * field (`post`). The scene renders into an offscreen HDR texture - * (rgba16float — core WebGPU, no feature gate) instead of the swapchain, then - * PostChain encodes, on the SAME encoder (single submit, zero readback): - * - * 1..N threshold-free mip bloom: progressive 13-tap Jimenez downsample - * (Karis average on the first hop kills fireflies), then 9-tap - * tent upsample ACCUMULATED additively up the chain, - * final composite → swapchain: scene + BLOOM_STRENGTH·bloom/mipCount → - * Khronos PBR Neutral → seeded grain → cos⁴ vignette. - * - * Bloom RADIUS varies with viewport size (mipCount grows with the canvas); - * BRIGHTNESS does not — the composite divides by textureNumLevels, so the - * up-chain's DC gain of exactly mipCount normalizes to 1. - * - * Zero per-frame uniforms/allocations: pipelines + explicit layouts build - * once in the constructor; textures/views/bind groups rebuild only when - * ensure() sees a new size; the shaders read sizes via textureDimensions / - * textureNumLevels straight from the bound views. - * - * SUBRESOURCE RULE (load-bearing): the blur chain binds ONLY single- - * subresource views (mipView[i]) — sampling mip i+1 while rendering mip i is - * valid only because the subresources are disjoint. The full-mip view exists - * ONLY in the composite bind group, where bloomTex is never an attachment. - */ - -import { planBloomMips, type BloomMipPlan } from './mip-plan'; -import { postWGSL } from './shaders/post.wgsl'; - -// Tuning constants — defined next to the WGSL they are interpolated into; -// re-exported here as the public constant surface of the post stack. -export { - BLOOM_STRENGTH, - BLOOM_CHROMATIC_TEXELS, - GRAIN_AMP, - VIGNETTE_LIFT, - VIGNETTE_TAN -} from './shaders/post.wgsl'; - -/** - * Format every FramePass render pipeline targets — the offscreen HDR scene - * texture. rgba16float is core WebGPU: render-attachable, blendable, and - * filterable with no feature gate. - */ -export const SCENE_FORMAT: GPUTextureFormat = 'rgba16float'; - -const ADDITIVE_BLEND: GPUBlendState = { - color: { srcFactor: 'one', dstFactor: 'one', operation: 'add' }, - alpha: { srcFactor: 'one', dstFactor: 'one', operation: 'add' } -}; - -export class PostChain { - private device: GPUDevice; - private paramsBuffer: GPUBuffer; - private samp: GPUSampler; - - private blurLayout: GPUBindGroupLayout; - private compositeLayout: GPUBindGroupLayout; - private pipeDownFirst: GPURenderPipeline; - private pipeDown: GPURenderPipeline; - private pipeUp: GPURenderPipeline; - private pipeComposite: GPURenderPipeline; - - // Size-dependent resources — (re)built by ensure() only. - private width = 0; - private height = 0; - private plan: BloomMipPlan | null = null; - private sceneTex: GPUTexture | null = null; - private _sceneView: GPUTextureView | null = null; - private bloomTex: GPUTexture | null = null; - private mipViews: GPUTextureView[] = []; - private bloomFullView: GPUTextureView | null = null; - private downBind: GPUBindGroup[] = []; - private upBind: GPUBindGroup[] = []; - private compositeBind: GPUBindGroup | null = null; - - constructor( - device: GPUDevice, - paramsBuffer: GPUBuffer, - presentationFormat: GPUTextureFormat - ) { - this.device = device; - this.paramsBuffer = paramsBuffer; - - this.samp = device.createSampler({ - label: 'observatory-post-sampler', - minFilter: 'linear', - magFilter: 'linear', - addressModeU: 'clamp-to-edge', - addressModeV: 'clamp-to-edge' - }); - - const module = device.createShaderModule({ - label: 'observatory-post', - code: postWGSL - }); - - // EXPLICIT layouts (WGSL trap #6 structurally dead): one blur layout - // serves all three blur pipelines — explicit layouts may contain entries - // an entry point ignores, so down/up bind groups are interchangeable. - this.blurLayout = device.createBindGroupLayout({ - label: 'observatory-post-blur-layout', - entries: [ - { - binding: 1, - visibility: GPUShaderStage.FRAGMENT, - texture: { sampleType: 'float', viewDimension: '2d' } - }, - { binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } } - ] - }); - this.compositeLayout = device.createBindGroupLayout({ - label: 'observatory-post-composite-layout', - entries: [ - { binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } }, - { binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } }, - { - binding: 3, - visibility: GPUShaderStage.FRAGMENT, - texture: { sampleType: 'float', viewDimension: '2d' } - }, - { - binding: 4, - visibility: GPUShaderStage.FRAGMENT, - texture: { sampleType: 'float', viewDimension: '2d' } - } - ] - }); - - const blurPipeLayout = device.createPipelineLayout({ - label: 'observatory-post-blur-pipe-layout', - bindGroupLayouts: [this.blurLayout] - }); - const compositePipeLayout = device.createPipelineLayout({ - label: 'observatory-post-composite-pipe-layout', - bindGroupLayouts: [this.compositeLayout] - }); - - const makePipe = ( - label: string, - layout: GPUPipelineLayout, - entryPoint: string, - format: GPUTextureFormat, - blend?: GPUBlendState - ): GPURenderPipeline => - device.createRenderPipeline({ - label, - layout, - vertex: { module, entryPoint: 'vs_fullscreen' }, - fragment: { module, entryPoint, targets: [{ format, blend }] }, - primitive: { topology: 'triangle-list' } - }); - - this.pipeDownFirst = makePipe( - 'observatory-post-down-karis', - blurPipeLayout, - 'fs_downsample_karis', - SCENE_FORMAT - ); - this.pipeDown = makePipe( - 'observatory-post-down', - blurPipeLayout, - 'fs_downsample', - SCENE_FORMAT - ); - this.pipeUp = makePipe( - 'observatory-post-up', - blurPipeLayout, - 'fs_upsample_tent', - SCENE_FORMAT, - ADDITIVE_BLEND - ); - // Composite targets the swapchain — presentation format comes from the - // constructor arg; never hardcode bgra8unorm. - this.pipeComposite = makePipe( - 'observatory-post-composite', - compositePipeLayout, - 'fs_composite', - presentationFormat - ); - } - - /** - * The main scene pass' color attachment (offscreen HDR). ensure() always - * precedes use in the engine's frame loop. - */ - get sceneView(): GPUTextureView { - if (!this._sceneView) { - throw new Error('PostChain.ensure() must run before sceneView is used'); - } - return this._sceneView; - } - - /** - * Idempotent size-compare: recreates textures/views/bind groups iff the - * size changed (clamped ≥ 1). Called from engine.resize() AND from the - * frame loop with the swapchain texture's dims (covers the boot frame). - */ - ensure(width: number, height: number): void { - const w = Math.max(1, Math.floor(width)); - const h = Math.max(1, Math.floor(height)); - if (w === this.width && h === this.height && this.sceneTex !== null) return; - this.width = w; - this.height = h; - - this.sceneTex?.destroy(); - this.bloomTex?.destroy(); - - this.sceneTex = this.device.createTexture({ - label: 'observatory-scene-hdr', - size: [w, h], - format: SCENE_FORMAT, - usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING - }); - this._sceneView = this.sceneTex.createView({ label: 'observatory-scene-hdr-view' }); - - const plan = planBloomMips(w, h); - this.plan = plan; - this.bloomTex = this.device.createTexture({ - label: 'observatory-bloom-mips', - size: [plan.baseW, plan.baseH], - format: SCENE_FORMAT, - mipLevelCount: plan.mipCount, - usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING - }); - - // SINGLE-subresource views for the blur chain (see header rule) … - const bloomTex = this.bloomTex; - this.mipViews = Array.from({ length: plan.mipCount }, (_, i) => - bloomTex.createView({ - label: `observatory-bloom-mip-${i}`, - baseMipLevel: i, - mipLevelCount: 1 - }) - ); - // … and the full-mip view for the composite ONLY (textureNumLevels). - this.bloomFullView = bloomTex.createView({ label: 'observatory-bloom-full' }); - - // Bind groups rebuild here only — zero per-frame allocations. - const sceneView = this._sceneView; - this.downBind = this.mipViews.map((_, i) => - this.device.createBindGroup({ - label: `observatory-bloom-down-bind-${i}`, - layout: this.blurLayout, - entries: [ - { binding: 1, resource: i === 0 ? sceneView : this.mipViews[i - 1] }, - { binding: 2, resource: this.samp } - ] - }) - ); - this.upBind = []; - for (let i = 0; i + 1 < plan.mipCount; i++) { - this.upBind.push( - this.device.createBindGroup({ - label: `observatory-bloom-up-bind-${i}`, - layout: this.blurLayout, - entries: [ - { binding: 1, resource: this.mipViews[i + 1] }, - { binding: 2, resource: this.samp } - ] - }) - ); - } - this.compositeBind = this.device.createBindGroup({ - label: 'observatory-post-composite-bind', - layout: this.compositeLayout, - entries: [ - { binding: 0, resource: { buffer: this.paramsBuffer } }, - { binding: 2, resource: this.samp }, - { binding: 3, resource: sceneView }, - { binding: 4, resource: this.bloomFullView } - ] - }); - } - - /** - * Encode the whole post stack: 2(N−1)+2 tiny fullscreen passes, each - * `setPipeline; setBindGroup; draw(3)`. Same encoder as the scene pass. - */ - encode(encoder: GPUCommandEncoder, swapchainView: GPUTextureView): void { - const plan = this.plan; - if (!plan || !this.compositeBind) return; - const n = plan.mipCount; - - // Downsample: scene (full res) → mip 0 (Karis), then mip i−1 → i. - for (let i = 0; i < n; i++) { - const pass = encoder.beginRenderPass({ - label: `observatory-bloom-down-${i}`, - colorAttachments: [{ view: this.mipViews[i], loadOp: 'clear', storeOp: 'store' }] - }); - pass.setPipeline(i === 0 ? this.pipeDownFirst : this.pipeDown); - pass.setBindGroup(0, this.downBind[i]); - pass.draw(3); - pass.end(); - } - - // Upsample: tent of mip i+1 accumulates ADDITIVELY onto the stored - // downsample at mip i (loadOp 'load' + one/one blend). DC gain becomes - // exactly n — normalized in the composite. Runs zero times when n = 1. - for (let i = n - 2; i >= 0; i--) { - const pass = encoder.beginRenderPass({ - label: `observatory-bloom-up-${i}`, - colorAttachments: [{ view: this.mipViews[i], loadOp: 'load', storeOp: 'store' }] - }); - pass.setPipeline(this.pipeUp); - pass.setBindGroup(0, this.upBind[i]); - pass.draw(3); - pass.end(); - } - - // Composite to the swapchain: bloom-add → tonemap → grain → vignette. - const pass = encoder.beginRenderPass({ - label: 'observatory-post-composite', - colorAttachments: [{ view: swapchainView, loadOp: 'clear', storeOp: 'store' }] - }); - pass.setPipeline(this.pipeComposite); - pass.setBindGroup(0, this.compositeBind); - pass.draw(3); - pass.end(); - } - - dispose(): void { - this.sceneTex?.destroy(); - this.bloomTex?.destroy(); - this.sceneTex = null; - this.bloomTex = null; - this._sceneView = null; - this.bloomFullView = null; - this.mipViews = []; - this.downBind = []; - this.upBind = []; - this.compositeBind = null; - this.plan = null; - this.width = 0; - this.height = 0; - } -} diff --git a/apps/dashboard/src/lib/observatory/post/shaders/post.wgsl.ts b/apps/dashboard/src/lib/observatory/post/shaders/post.wgsl.ts deleted file mode 100644 index d3ecd97..0000000 --- a/apps/dashboard/src/lib/observatory/post/shaders/post.wgsl.ts +++ /dev/null @@ -1,285 +0,0 @@ -/** - * Cognitive Observatory — post-processing shader module (S1–S4). - * - * ONE WGSL module, five entry points, globally unique bindings, consumed - * through EXPLICIT bind group layouts in post-chain.ts (WGSL trap #6 — - * auto-layout stripping unused bindings — is structurally dead here). - * - * Chain: scene (offscreen rgba16float HDR) → threshold-FREE mip bloom - * (13-tap Jimenez downsample, Karis average on the FIRST hop to kill - * fireflies; 9-tap tent upsample accumulated additively up the chain) → - * composite to the swapchain: - * - * hdr = scene + BLOOM_STRENGTH · bloom / mipCount - * → Khronos PBR Neutral tonemap (hue-preserving; NEVER ACES — it would - * skew the FSRS palette) - * → seeded TPDF film grain (720-frame periodic, capture-pinned) - * → cos⁴ vignette. - * - * Determinism: grain is keyed to the WRAPPED loop frame + integer pixel - * coords via a PCG hash — no wall clock, no Math.random — so identical - * URL+frame ⇒ identical pixels and the 720-frame loop stays seamless. - * - * Scene ALPHA is discarded by the composite: additive one/one blending - * accumulates it past 1 in the HDR target; the composite reads .rgb and - * writes a = 1 (canvas alphaMode is 'opaque'). - * - * Trap audit (the six WGSL traps previously hit in this codebase): - * (1) no `meta` identifier; (2) no arrays at all — bit-math fullscreen - * vertex + fully unrolled taps; (3) no per-instance varyings (instance-free - * fullscreen passes); (4) whole-vector writes only; (5) no arrayLength; - * (6) explicit layouts on all four pipelines. - */ - -// -- Tuning constants. TS is the single source of truth: the values are -// interpolated into the WGSL header below, so the shader can never drift -// from what post-chain.ts / tone-reference.ts compute with. -// (Re-exported through post-chain.ts as the public constant surface.) - -/** Bloom mix into the scene. Spec window 0.15–0.25 — the one tuning knob. */ -export const BLOOM_STRENGTH = 0.18; -/** Radial dispersion on the bloom term. LOCKED AT 0.0: chromatic aberration is - * INSANITY-PLAN §4 KILLED item #9 — it fringes the ignite/recall halos whose - * hue IS FSRS data (§7.1). Do not re-enable; re-litigate via the plan first. */ -export const BLOOM_CHROMATIC_TEXELS = 0.0; -/** Film grain amplitude. Spec window 1.5–2.5/255. */ -export const GRAIN_AMP = 2.0 / 255; -/** Vignette corner floor — "observatory, not tunnel". */ -export const VIGNETTE_LIFT = 0.85; -/** Vignette tan(θ) at the corner — attenuation ≈ 0.93 with lift 0.85. */ -export const VIGNETTE_TAN = 0.62; - -export const postWGSL = /* wgsl */ ` -// Tuning constants — interpolated from post.wgsl.ts (TS single source of truth). -const BLOOM_STRENGTH: f32 = ${BLOOM_STRENGTH}; -const BLOOM_CHROMATIC_TEXELS: f32 = ${BLOOM_CHROMATIC_TEXELS}; -const GRAIN_AMP: f32 = ${GRAIN_AMP}; -const VIGNETTE_LIFT: f32 = ${VIGNETTE_LIFT}; -const VIGNETTE_TAN: f32 = ${VIGNETTE_TAN}; - -// Params layout — VERBATIM from render-nodes.wgsl.ts (types.PARAMS_FLOATS). -struct Params { - frame: f32, - loop_phase: f32, - node_count: f32, - edge_count: f32, - path_count: f32, - pulse: f32, - viewport_w: f32, - viewport_h: f32, - brightness: f32, - demo_id: f32, - time: f32, - _pad: f32, -}; - -// Globally unique bindings — each entry point statically uses a subset; the -// explicit bind group layouts in post-chain.ts carry exactly what each -// pipeline needs (blur: 1+2, composite: 0+2+3+4). -@group(0) @binding(0) var params: Params; // composite only -@group(0) @binding(1) var src: texture_2d; // blur chain input -@group(0) @binding(2) var samp: sampler; // shared -@group(0) @binding(3) var scene_tex: texture_2d; // composite only -@group(0) @binding(4) var bloom_tex: texture_2d; // composite only (FULL-mip view) - -struct FSOut { - @builtin(position) pos: vec4f, - @location(0) uv: vec2f, -}; - -// Fullscreen triangle from bit math — no vertex buffer, no arrays. -// vi 0/1/2 → clip (-1,-1) (3,-1) (-1,3); uv y flipped so uv(0,0) = top-left. -@vertex -fn vs_fullscreen(@builtin(vertex_index) vi: u32) -> FSOut { - let xy = vec2f(f32((vi << 1u) & 2u), f32(vi & 2u)) * 2.0 - 1.0; - var out: FSOut; - out.pos = vec4f(xy, 0.0, 1.0); - out.uv = vec2f(xy.x, -xy.y) * 0.5 + 0.5; - return out; -} - -fn luma(c: vec3f) -> f32 { - return dot(c, vec3f(0.2126, 0.7152, 0.0722)); -} - -// --------------------------------------------------------------------------- -// Bloom downsample — 13-tap Jimenez (SIGGRAPH 2014 "Next Generation Post -// Processing in Call of Duty: Advanced Warfare"), taps fully unrolled. -// -// a b c outer ring at ±2 texels -// j k inner ring at ±1 texels -// d e f e = center -// l m -// g h i -// -// Grouped as 5 overlapping 4-tap boxes: center box (the four inner taps) -// weight 0.5, four corner boxes weight 0.125 each. A flat field reproduces -// itself EXACTLY (0.5 + 4·0.125 = 1) — that exactness is what the void -// preimage in tone-reference.ts depends on. -// --------------------------------------------------------------------------- - -@fragment -fn fs_downsample_karis(in: FSOut) -> @location(0) vec4f { - let ts = 1.0 / vec2f(textureDimensions(src)); - let a = textureSampleLevel(src, samp, in.uv + vec2f(-2.0, -2.0) * ts, 0.0).rgb; - let b = textureSampleLevel(src, samp, in.uv + vec2f( 0.0, -2.0) * ts, 0.0).rgb; - let c = textureSampleLevel(src, samp, in.uv + vec2f( 2.0, -2.0) * ts, 0.0).rgb; - let d = textureSampleLevel(src, samp, in.uv + vec2f(-2.0, 0.0) * ts, 0.0).rgb; - let e = textureSampleLevel(src, samp, in.uv, 0.0).rgb; - let f = textureSampleLevel(src, samp, in.uv + vec2f( 2.0, 0.0) * ts, 0.0).rgb; - let g = textureSampleLevel(src, samp, in.uv + vec2f(-2.0, 2.0) * ts, 0.0).rgb; - let h = textureSampleLevel(src, samp, in.uv + vec2f( 0.0, 2.0) * ts, 0.0).rgb; - let i = textureSampleLevel(src, samp, in.uv + vec2f( 2.0, 2.0) * ts, 0.0).rgb; - let j = textureSampleLevel(src, samp, in.uv + vec2f(-1.0, -1.0) * ts, 0.0).rgb; - let k = textureSampleLevel(src, samp, in.uv + vec2f( 1.0, -1.0) * ts, 0.0).rgb; - let l = textureSampleLevel(src, samp, in.uv + vec2f(-1.0, 1.0) * ts, 0.0).rgb; - let m = textureSampleLevel(src, samp, in.uv + vec2f( 1.0, 1.0) * ts, 0.0).rgb; - - let box_c = (j + k + l + m) * 0.25; - let box_tl = (a + b + d + e) * 0.25; - let box_tr = (b + c + e + f) * 0.25; - let box_bl = (d + e + g + h) * 0.25; - let box_br = (e + f + h + i) * 0.25; - - // Karis average (fireflies killer) — used ONLY on the full→mip0 hop. - // Each box is additionally weighted 1/(1 + luma) and the sum RENORMALIZED: - // on a flat field every Karis factor is equal, so the result is exact. - let w_c = 0.5 / (1.0 + luma(box_c)); - let w_tl = 0.125 / (1.0 + luma(box_tl)); - let w_tr = 0.125 / (1.0 + luma(box_tr)); - let w_bl = 0.125 / (1.0 + luma(box_bl)); - let w_br = 0.125 / (1.0 + luma(box_br)); - let sum = w_c * box_c + w_tl * box_tl + w_tr * box_tr + w_bl * box_bl + w_br * box_br; - return vec4f(sum / (w_c + w_tl + w_tr + w_bl + w_br), 1.0); -} - -@fragment -fn fs_downsample(in: FSOut) -> @location(0) vec4f { - let ts = 1.0 / vec2f(textureDimensions(src)); - let a = textureSampleLevel(src, samp, in.uv + vec2f(-2.0, -2.0) * ts, 0.0).rgb; - let b = textureSampleLevel(src, samp, in.uv + vec2f( 0.0, -2.0) * ts, 0.0).rgb; - let c = textureSampleLevel(src, samp, in.uv + vec2f( 2.0, -2.0) * ts, 0.0).rgb; - let d = textureSampleLevel(src, samp, in.uv + vec2f(-2.0, 0.0) * ts, 0.0).rgb; - let e = textureSampleLevel(src, samp, in.uv, 0.0).rgb; - let f = textureSampleLevel(src, samp, in.uv + vec2f( 2.0, 0.0) * ts, 0.0).rgb; - let g = textureSampleLevel(src, samp, in.uv + vec2f(-2.0, 2.0) * ts, 0.0).rgb; - let h = textureSampleLevel(src, samp, in.uv + vec2f( 0.0, 2.0) * ts, 0.0).rgb; - let i = textureSampleLevel(src, samp, in.uv + vec2f( 2.0, 2.0) * ts, 0.0).rgb; - let j = textureSampleLevel(src, samp, in.uv + vec2f(-1.0, -1.0) * ts, 0.0).rgb; - let k = textureSampleLevel(src, samp, in.uv + vec2f( 1.0, -1.0) * ts, 0.0).rgb; - let l = textureSampleLevel(src, samp, in.uv + vec2f(-1.0, 1.0) * ts, 0.0).rgb; - let m = textureSampleLevel(src, samp, in.uv + vec2f( 1.0, 1.0) * ts, 0.0).rgb; - - let box_c = (j + k + l + m) * 0.25; - let box_tl = (a + b + d + e) * 0.25; - let box_tr = (b + c + e + f) * 0.25; - let box_bl = (d + e + g + h) * 0.25; - let box_br = (e + f + h + i) * 0.25; - return vec4f(box_c * 0.5 + (box_tl + box_tr + box_bl + box_br) * 0.125, 1.0); -} - -// --------------------------------------------------------------------------- -// Bloom upsample — 9-tap 3×3 tent, 1/16·[1 2 1; 2 4 2; 1 2 1], radius = one -// SOURCE-mip texel. Rendered with additive one/one blending onto the stored -// downsample of the destination mip (accumulate-up-the-chain). The resulting -// DC gain of exactly mipCount is normalized in fs_composite. -// --------------------------------------------------------------------------- - -@fragment -fn fs_upsample_tent(in: FSOut) -> @location(0) vec4f { - let ts = 1.0 / vec2f(textureDimensions(src)); - let a = textureSampleLevel(src, samp, in.uv + vec2f(-1.0, -1.0) * ts, 0.0).rgb; - let b = textureSampleLevel(src, samp, in.uv + vec2f( 0.0, -1.0) * ts, 0.0).rgb; - let c = textureSampleLevel(src, samp, in.uv + vec2f( 1.0, -1.0) * ts, 0.0).rgb; - let d = textureSampleLevel(src, samp, in.uv + vec2f(-1.0, 0.0) * ts, 0.0).rgb; - let e = textureSampleLevel(src, samp, in.uv, 0.0).rgb; - let f = textureSampleLevel(src, samp, in.uv + vec2f( 1.0, 0.0) * ts, 0.0).rgb; - let g = textureSampleLevel(src, samp, in.uv + vec2f(-1.0, 1.0) * ts, 0.0).rgb; - let h = textureSampleLevel(src, samp, in.uv + vec2f( 0.0, 1.0) * ts, 0.0).rgb; - let i = textureSampleLevel(src, samp, in.uv + vec2f( 1.0, 1.0) * ts, 0.0).rgb; - let sum = (a + c + g + i) + (b + d + f + h) * 2.0 + e * 4.0; - return vec4f(sum * (1.0 / 16.0), 1.0); -} - -// --------------------------------------------------------------------------- -// Composite — bloom-add → PBR Neutral → grain → vignette (order is mandated). -// --------------------------------------------------------------------------- - -// Khronos PBR Neutral — EXACT port of the Khronos reference implementation. -// Hue-preserving; the FSRS palette keeps its channel ordering. Pinned to the -// CPU mirror in post/tone-reference.ts (pbrNeutralReference) — keep in -// lockstep, the void-preimage tests run against the mirror. -fn pbr_neutral(color_in: vec3f) -> vec3f { - let start_compression = 0.8 - 0.04; - let desaturation = 0.15; - var color = color_in; - let x = min(color.r, min(color.g, color.b)); - // WGSL select(false_value, true_value, condition) — argument order trap. - let offset = select(0.04, x - 6.25 * x * x, x < 0.08); - color = color - vec3f(offset); - let peak = max(color.r, max(color.g, color.b)); - if (peak < start_compression) { - return color; - } - let d = 1.0 - start_compression; - let new_peak = 1.0 - d * d / (peak + d - start_compression); - color = color * (new_peak / peak); - let g = 1.0 / (desaturation * (peak - new_peak) + 1.0); - // mix weight = 1 - g per the Khronos spec. - return mix(color, vec3f(new_peak), 1.0 - g); -} - -// PCG hash — integers only, 24-bit-exact output in [0, 1). Deterministic. -fn pcg(v: u32) -> u32 { - var s = v * 747796405u + 2891336453u; - let t = ((s >> ((s >> 28u) + 4u)) ^ s) * 277803737u; - return (t >> 22u) ^ t; -} - -fn hashf(p: vec2u, f: u32) -> f32 { - return f32(pcg(p.x ^ pcg(p.y ^ pcg(f))) >> 8u) / 16777216.0; -} - -@fragment -fn fs_composite(in: FSOut) -> @location(0) vec4f { - let pix = vec2u(in.pos.xy); - // Exact 1:1 fetch (alpha discarded — see module header). - let scene = textureLoad(scene_tex, pix, 0).rgb; - - // Bloom, normalized by the mip count: the additive up-chain has DC gain - // exactly mipCount, so /mips makes flat-field gain exactly 1 — the void - // preimage holds and brightness is viewport-stable. Chromatic dispersion - // rides the bloom term ONLY (BLOOM_CHROMATIC_TEXELS = 0.0 kills it). - let mips = f32(textureNumLevels(bloom_tex)); - let dims = vec2f(textureDimensions(bloom_tex)); - let dvec = in.uv - vec2f(0.5); - let off = dvec * (BLOOM_CHROMATIC_TEXELS * dot(dvec, dvec) * 4.0) / dims; - let bloom = vec3f( - textureSampleLevel(bloom_tex, samp, in.uv - off, 0.0).r, - textureSampleLevel(bloom_tex, samp, in.uv, 0.0).g, - textureSampleLevel(bloom_tex, samp, in.uv + off, 0.0).b - ) / mips; - - var c = pbr_neutral(scene + BLOOM_STRENGTH * bloom); - - // Seeded TPDF film grain (post-tonemap dither): keyed to the WRAPPED loop - // frame → 720-periodic and capture-pinned. Full strength in the shadows - // (kills #05060a banding), fades out of highlights. - let f = u32(params.frame + 0.5); - let n = hashf(pix, f) + hashf(pix ^ vec2u(0x9E3779B9u, 0x85EBCA6Bu), f) - 1.0; - let w = 1.0 - smoothstep(0.0, 0.8, luma(c)); - c += GRAIN_AMP * n * w; - - // cos⁴ vignette: cos⁴θ = (1 + r²·tan²)⁻², aspect-normalized so rn = 1.0 - // exactly at the corners regardless of viewport shape. Lifted floor keeps - // it an observatory, not a tunnel. - let ar = vec2f(params.viewport_w / max(params.viewport_h, 1.0), 1.0); - let rn = length((in.uv * 2.0 - 1.0) * ar) / length(ar); - let k = rn * rn * VIGNETTE_TAN * VIGNETTE_TAN; - c *= mix(VIGNETTE_LIFT, 1.0, 1.0 / ((1.0 + k) * (1.0 + k))); - - // NO gamma encode — display-referred pass-through, matching the pre-post - // look where shader outputs went straight to the swapchain. - return vec4f(c, 1.0); -} -`; diff --git a/apps/dashboard/src/lib/observatory/post/tone-reference.ts b/apps/dashboard/src/lib/observatory/post/tone-reference.ts deleted file mode 100644 index 02e7649..0000000 --- a/apps/dashboard/src/lib/observatory/post/tone-reference.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Cognitive Observatory — Khronos PBR Neutral tone mapping, CPU reference. - * - * Exact TS mirror of pbr_neutral() in post/shaders/post.wgsl.ts. vitest can't - * run WGSL on a GPU, so all determinism-critical tonemap math is verified - * against this mirror — keep the two in lockstep. - * - * IMPORTANT: PBR Neutral is NOT the identity below the compression knee. The - * black-offset branch subtracts `offset` from EVERY pixel (0.04 when the min - * channel is ≥ 0.08, else x − 6.25x²). A naive #05060a clear would therefore - * tonemap to ≈ #010206 (crushed void). VOID_CLEAR_HDR below is the analytic - * preimage that lands the post stack EXACTLY back on #05060a. - */ - -import { BLOOM_STRENGTH } from './post-chain'; - -/** Khronos PBR Neutral reference (https://github.com/KhronosGroup/ToneMapping). */ -export function pbrNeutralReference( - rgb: readonly [number, number, number] -): [number, number, number] { - const startCompression = 0.8 - 0.04; - const desaturation = 0.15; - - let [r, g, b] = rgb; - const x = Math.min(r, Math.min(g, b)); - const offset = x < 0.08 ? x - 6.25 * x * x : 0.04; - r -= offset; - g -= offset; - b -= offset; - - const peak = Math.max(r, Math.max(g, b)); - if (peak < startCompression) return [r, g, b]; - - const d = 1 - startCompression; - const newPeak = 1 - (d * d) / (peak + d - startCompression); - const scale = newPeak / peak; - r *= scale; - g *= scale; - b *= scale; - - const gMix = 1 / (desaturation * (peak - newPeak) + 1); - // mix(color, vec3(newPeak), 1 - g) per the Khronos spec. - const w = 1 - gMix; - return [r + w * (newPeak - r), g + w * (newPeak - g), b + w * (newPeak - b)]; -} - -// --------------------------------------------------------------------------- -// VOID_CLEAR_HDR — the HDR clear color whose post-stack output is #05060a. -// -// Derivation (verified by tone-reference.test.ts): -// - The normalized bloom chain has flat-field gain exactly 1 (renormalized -// Karis + exact box/tent weights + /mipCount in the composite), so a flat -// void field enters the tonemap as (1 + BLOOM_STRENGTH) · v = 1.18 · v. -// - Below the knee, out = in − offset with offset = x − 6.25x² (x = min -// channel < 0.08), hence out_min = 6.25x². Solving out_min = 5/255: -// x = sqrt((5/255)/6.25) = 0.05601120… -// offset = x − 5/255 = 0.03640336… -// g_in = 6/255 + offset; b_in = 10/255 + offset -// peak = b_in ≈ 0.0756 < 0.76 → the compression branch is NOT taken. -// - Divide by (1 + BLOOM_STRENGTH): pbrNeutral(1.18 · VOID_CLEAR_HDR) is -// EXACTLY (5/255, 6/255, 10/255) = #05060a. -// Literals: r ≈ 0.0474671, g ≈ 0.0507905, b ≈ 0.0640839. -// - f16 quantization of the clear (ulp ≈ 3e-5 near 0.05) keeps the -// tonemapped void well inside ±0.5/255 — verified safe. -// --------------------------------------------------------------------------- - -const VOID_R_IN = Math.sqrt(5 / 255 / 6.25); // 0.05601120… -const VOID_OFFSET = VOID_R_IN - 5 / 255; // 0.03640336… - -export const VOID_CLEAR_HDR: GPUColorDict = { - r: VOID_R_IN / (1 + BLOOM_STRENGTH), - g: (6 / 255 + VOID_OFFSET) / (1 + BLOOM_STRENGTH), - b: (10 / 255 + VOID_OFFSET) / (1 + BLOOM_STRENGTH), - a: 1 -}; diff --git a/apps/dashboard/src/lib/observatory/rescue-plan.ts b/apps/dashboard/src/lib/observatory/rescue-plan.ts deleted file mode 100644 index 22f06ac..0000000 --- a/apps/dashboard/src/lib/observatory/rescue-plan.ts +++ /dev/null @@ -1,634 +0,0 @@ -/** - * Cognitive Observatory — Retroactive Salience Backfill demo plan (salience-rescue). - * - * Pure CPU: given the API GraphResponse + the stable-indexed ObservatoryGraph + - * the demo seed, DETERMINISTICALLY pick the failure memory, the K=4 lookalikes - * (nearest in LAYOUT space — exactly what vector search would return), the true - * cause (graph-distance ≥ 3, low retention, old), the BFS hop-depth map for the - * backward wave, the PathSteps (probe beams, wave tree, causal arc), the spine - * beats, and the verdict copy. No Math.random(), no Date.now() — Date.parse is - * only applied to DATA timestamps. - * - * The 720-frame beat map (fixed 60Hz, seamless loop): - * 0-90 field at rest - * 90-120 DETONATION — failure flares crimson (demo.w) - * 120-260 SEARCHLIGHT — K lookalikes flare cold-white sequentially (demo.y) - * 260-520 BACKWARD WAVE — hop-by-hop interrogation away from the failure (demo.z) - * 520-600 CAUSE IGNITES — thin-film recall envelope (demo.x) + causal arc - * 600-660 VERDICT — DOM overlay - * 660-720 decay to rest — every envelope is exactly 0 at frames 0 and 719 - * - * `rescueEnvelopes` is the authoritative CPU mirror of shaders/rescue.wgsl.ts — - * the seam-zero unit test machine-checks the loop guarantee against it. - */ - -import type { GraphResponse } from '$types'; -import { DemoClock } from './demo-clock'; -import { buildNodeStateArray } from './graph-upload'; -import { FLOATS_PER_NODE, PATH_KIND, type ObservatoryGraph } from './types'; -import type { PathStepMeta } from './path-builder'; - -// --------------------------------------------------------------------------- -// Beat-map constants (shared with shaders/rescue.wgsl.ts via RescueShaderConsts) -// --------------------------------------------------------------------------- - -export const RESCUE_K = 4; -export const DETONATE_FRAME = 90; -/** Lookalike k flares at LOOKALIKE_BASE + k * LOOKALIKE_INTERVAL → 138,166,194,222. */ -export const LOOKALIKE_BASE = 138; -export const LOOKALIKE_INTERVAL = 28; -export const WAVE_START = 260; -export const WAVE_ARRIVAL_CAP = 514; -export const ARC_FRAME = 560; -export const VERDICT_START = 600; -export const VERDICT_END = 660; -export const UNREACHED = 0xffff; -export const MAX_WAVE_STEPS = 48; -export const LOOP_FRAMES = 720; - -/** BFS parent preference: causal chains first — RSB semantics. Unknown → 5. */ -export const EDGE_TYPE_RANK: Record = { - causal: 0, - temporal: 1, - shared_concepts: 2, - complementary: 3, - semantic: 4 -}; - -// --------------------------------------------------------------------------- -// Public types -// --------------------------------------------------------------------------- - -export interface RescueShaderConsts { - /** Frames per BFS hop for the backward wave: clamp(⌊252/D⌋, 14, 84). */ - hopSlot: number; - /** BFS depth of the cause (D). Wave lane fires only for 1 ≤ d ≤ D. */ - causeDepth: number; -} - -export interface RescueVerdictCopy { - headline: 'root cause found'; - causeLabel: string; - failureLabel: string; - causeDate: string; - hops: number; - k: number; - /** `${hops} hops back · ${causeDate} · vector search: 0 for ${k}` */ - receipt: string; -} - -export interface RescuePlan { - /** false ⇒ field renders, story suppressed (no fake cause on tiny graphs). */ - viable: boolean; - failureIndex: number; - causeIndex: number; - lookalikeIndices: number[]; - /** Per-node BFS hop depth from the failure; UNREACHED; failure = 0. */ - hopDepths: Uint16Array; - causeDepth: number; - hopSlot: number; - /** 1 u32/node: bits 0-15 hopDepth, 16 isFailure, 17 isCause, 18 isLookalike, 19-21 k. */ - waveData: Uint32Array; - /** UINTS_PER_PATHSTEP u32 per step; min Uint32Array(4). */ - pathData: Uint32Array; - /** - * MUST be 1:1 with pathData steps — setPathSteps sets params[4] and the - * ribbon draw count from THIS array's length. - */ - pathMetas: PathStepMeta[]; - /** Curated spine beats (route state only, NEVER sent to GPU). Unique beatFrames. */ - spineBeats: PathStepMeta[]; - verdict: RescueVerdictCopy; - consts: RescueShaderConsts; -} - -// --------------------------------------------------------------------------- -// Layout parity — the ONE correct way to know where nodes are on screen -// --------------------------------------------------------------------------- - -/** - * Byte-identical replica of NodeRenderer.upload's layout: same function - * (buildNodeStateArray), same fresh DemoClock, same rng consumption (incl. the - * center-node skip). Do NOT reimplement golden-angle placement here. - */ -export function layoutPositions(graph: ObservatoryGraph, seed: string): Float32Array { - const layoutClock = new DemoClock({ seed }); - return buildNodeStateArray(graph, layoutClock.state.rng).data; -} - -// --------------------------------------------------------------------------- -// Selection algorithms (all exported for tests; ties → ascending node index) -// --------------------------------------------------------------------------- - -function nodeDegrees(graph: ObservatoryGraph): Uint32Array { - const degree = new Uint32Array(graph.nodes.length); - for (const e of graph.edges) { - degree[e.sourceIndex]++; - degree[e.targetIndex]++; - } - return degree; -} - -/** - * Pick the failure memory: non-center, unsuppressed, well-connected, prefer - * failure/guardrail (then confusion/weak-spot) tags, prefer nodes away from - * the field center in layout (≥ 0.45 · fieldRadius 120 = 54). - * Relaxation ladder: degree ≥ 2 → any unsuppressed non-center → any - * non-center → center (degenerate single-node field). Empty graph → -1. - */ -export function pickFailureIndex(graph: ObservatoryGraph, positions: Float32Array): number { - const n = graph.nodes.length; - if (n === 0) return -1; - const degree = nodeDegrees(graph); - - const score = (i: number): number => { - const node = graph.nodes[i]; - const tags = new Set(node.tags.map((t) => t.toLowerCase())); - let s = 0; - if (tags.has('failure') || tags.has('guardrail')) s += 3; - if (tags.has('confusion') || tags.has('weak-spot')) s += 2; - s += Math.min(degree[i], 8) / 8; - const x = positions[i * FLOATS_PER_NODE + 0]; - const y = positions[i * FLOATS_PER_NODE + 1]; - const z = positions[i * FLOATS_PER_NODE + 2]; - if (Math.sqrt(x * x + y * y + z * z) >= 54) s += 0.5; - return s; - }; - - const tiers: Array<(i: number) => boolean> = [ - (i) => i !== graph.centerIndex && !graph.nodes[i].suppressed && degree[i] >= 2, - (i) => i !== graph.centerIndex && !graph.nodes[i].suppressed, - (i) => i !== graph.centerIndex, - () => true - ]; - for (const accept of tiers) { - let best = -1; - let bestScore = -Infinity; - for (let i = 0; i < n; i++) { - if (!accept(i)) continue; - const s = score(i); - // strict > keeps the lowest index on ties (ascending scan) - if (s > bestScore) { - bestScore = s; - best = i; - } - } - if (best >= 0) return best; - } - return -1; -} - -/** - * Undirected BFS from the failure. Hop depths are TRUE BFS distances - * (layer order is edge-rank independent); parent pointers are chosen in a - * second pass preferring causal/temporal edges (RSB semantics), then lowest - * neighbor index — fully deterministic. - */ -export function bfsFromFailure( - graph: ObservatoryGraph, - failureIndex: number -): { depths: Uint16Array; parents: Int32Array } { - const n = graph.nodes.length; - const depths = new Uint16Array(n).fill(UNREACHED); - const parents = new Int32Array(n).fill(-1); - if (failureIndex < 0 || failureIndex >= n) return { depths, parents }; - - const adj: Array> = Array.from({ length: n }, () => []); - for (const e of graph.edges) { - const rank = EDGE_TYPE_RANK[e.type] ?? 5; - adj[e.sourceIndex].push({ nbr: e.targetIndex, rank }); - adj[e.targetIndex].push({ nbr: e.sourceIndex, rank }); - } - for (const list of adj) list.sort((a, b) => a.rank - b.rank || a.nbr - b.nbr); - - depths[failureIndex] = 0; - const queue = [failureIndex]; - for (let qi = 0; qi < queue.length; qi++) { - const u = queue[qi]; - for (const { nbr } of adj[u]) { - if (depths[nbr] === UNREACHED) { - depths[nbr] = depths[u] + 1; - queue.push(nbr); - } - } - } - - // Parent pass: first depth-(d-1) neighbor in (rank, index) order wins. - for (let v = 0; v < n; v++) { - if (depths[v] === UNREACHED || depths[v] === 0) continue; - for (const { nbr } of adj[v]) { - if (depths[nbr] === depths[v] - 1) { - parents[v] = nbr; - break; - } - } - } - - return { depths, parents }; -} - -/** - * Pick the true cause: graph-distant (depth ≥ 3, relaxed 2 → 1), low - * retention (hard ≤ 0.45, relaxed away if empty — visually slate/dim), old. - * score = 2·(1−retention) + 0.5·min(depth,6)/6 + 0.5·ageRank, ageRank ∈ [0,1] - * with the oldest createdAt → 1 (invalid/missing dates → 0). - * Sort (score desc, depth desc, index asc). No candidate at any depth ⇒ -1. - */ -export function pickCauseIndex( - response: GraphResponse, - graph: ObservatoryGraph, - depths: Uint16Array, - failureIndex: number -): { index: number; depth: number } { - const createdById = new Map(); - for (const nd of response.nodes) createdById.set(nd.id, nd.createdAt); - - for (const minDepth of [3, 2, 1]) { - const cand: number[] = []; - for (let i = 0; i < graph.nodes.length; i++) { - if (i === graph.centerIndex || i === failureIndex) continue; - const d = depths[i]; - if (d === UNREACHED || d < minDepth) continue; - cand.push(i); - } - if (cand.length === 0) continue; - - let pool = cand.filter((i) => graph.nodes[i].retention <= 0.45); - if (pool.length === 0) pool = cand; - - // ageRank across the pool: oldest Date.parse(createdAt) → 1. - const times = new Map(); - let minT = Infinity; - let maxT = -Infinity; - for (const i of pool) { - const raw = createdById.get(graph.nodes[i].id); - const t = raw ? Date.parse(raw) : NaN; - if (Number.isFinite(t)) { - times.set(i, t); - if (t < minT) minT = t; - if (t > maxT) maxT = t; - } - } - const ageRank = (i: number): number => { - const t = times.get(i); - if (t === undefined) return 0; - if (maxT === minT) return 1; - return (maxT - t) / (maxT - minT); - }; - const score = (i: number): number => - 2 * (1 - graph.nodes[i].retention) + (0.5 * Math.min(depths[i], 6)) / 6 + 0.5 * ageRank(i); - - pool.sort((a, b) => { - const sa = score(a); - const sb = score(b); - if (sb !== sa) return sb - sa; - if (depths[b] !== depths[a]) return depths[b] - depths[a]; - return a - b; - }); - return { index: pool[0], depth: depths[pool[0]] }; - } - return { index: -1, depth: 0 }; -} - -/** - * K = min(4, eligible) nearest neighbors of the failure in LAYOUT space, - * excluding failure/cause/center — "looks similar" is literal: these are the - * nodes vector search would return. Flare order = nearest first. - */ -export function pickLookalikes( - positions: Float32Array, - nodeCount: number, - failureIndex: number, - causeIndex: number, - centerIndex: number -): number[] { - const fx = positions[failureIndex * FLOATS_PER_NODE + 0]; - const fy = positions[failureIndex * FLOATS_PER_NODE + 1]; - const fz = positions[failureIndex * FLOATS_PER_NODE + 2]; - const cand: Array<{ i: number; d2: number }> = []; - for (let i = 0; i < nodeCount; i++) { - if (i === failureIndex || i === causeIndex || i === centerIndex) continue; - const dx = positions[i * FLOATS_PER_NODE + 0] - fx; - const dy = positions[i * FLOATS_PER_NODE + 1] - fy; - const dz = positions[i * FLOATS_PER_NODE + 2] - fz; - cand.push({ i, d2: dx * dx + dy * dy + dz * dz }); - } - cand.sort((a, b) => a.d2 - b.d2 || a.i - b.i); - return cand.slice(0, RESCUE_K).map((c) => c.i); -} - -// --------------------------------------------------------------------------- -// Wave timing -// --------------------------------------------------------------------------- - -/** σ = clamp(⌊252/D⌋, 14, 84): the wave reaches the cause 6-10 frames before ignition. */ -export function hopSlotFor(causeDepth: number): number { - const d = Math.max(1, causeDepth); - return Math.min(84, Math.max(14, Math.floor(252 / d))); -} - -/** W(d) = min(WAVE_START + σ·d, WAVE_ARRIVAL_CAP). */ -export function waveArrivalFrame(depth: number, hopSlot: number): number { - return Math.min(WAVE_START + hopSlot * depth, WAVE_ARRIVAL_CAP); -} - -export function lookalikeFrame(k: number): number { - return LOOKALIKE_BASE + LOOKALIKE_INTERVAL * k; -} - -export function truncateLabel(label: string): string { - return label.length > 64 ? label.slice(0, 64) + '…' : label; -} - -// --------------------------------------------------------------------------- -// Envelope math — the authoritative CPU mirror of shaders/rescue.wgsl.ts -// --------------------------------------------------------------------------- - -function smooth(a: number, b: number, f: number): number { - const t = Math.min(1, Math.max(0, (f - a) / (b - a))); - return t * t * (3 - 2 * t); -} - -function env(f: number, a0: number, a1: number, r0: number, r1: number): number { - return smooth(a0, a1, f) * (1 - smooth(r0, r1, f)); -} - -/** - * Pure function of (frame, packed wave word, shader consts) → the four demo - * lanes (x recall/ignition, y searchlight, z wave, w shock). Every term is - * A·S(a0,a1,f)·(1−S(r0,r1,f)) with all a0 ≥ 88 and all r1 ≤ 700, so the value - * is EXACTLY zero at frames 0 and 719 — the machine-checked seam guarantee. - * Keep in lockstep with rescue_choreo in shaders/rescue.wgsl.ts. - */ -export function rescueEnvelopes( - frame: number, - packed: number, - c: RescueShaderConsts -): { x: number; y: number; z: number; w: number } { - const depth = packed & 0xffff; - const isFailure = (packed & 0x10000) !== 0; - const isCause = (packed & 0x20000) !== 0; - const isLook = (packed & 0x40000) !== 0; - const lookK = (packed >>> 19) & 0x7; - const loopPhase = frame / LOOP_FRAMES; - - let x = 0; - let y = 0; - let z = 0; - let w = 0; - - if (isFailure) { - // Detonation spike + wound simmer (burns through the investigation) - // + recognition flare as the causal arc lands at 560. - w += env(frame, 90, 96, 120, 168); - w += 0.35 * env(frame, 100, 130, 600, 656); - w += 0.35 * env(frame, 552, 562, 580, 640); - // Symptom backlight while the cause burns. - x += 0.4 * env(frame, 556, 566, 620, 668); - } - if (!isFailure && depth >= 1 && depth <= 12) { - // Shockwave blink: crimson concussion at 3 frames/hop along REAL graph distance. - w += - 0.75 * - Math.exp(-0.3 * depth) * - env(frame, 92 + 3 * depth, 96 + 3 * depth, 96 + 3 * depth, 122 + 3 * depth); - } - if (isLook) { - const fk = lookalikeFrame(lookK); - // Searchlight flare: cold pop, sequential, on camera. - y += env(frame, fk - 6, fk, fk + 10, fk + 26); - // Ash residue: the struck-through lookalike stays in frame until the verdict. - y += 0.15 * smooth(fk + 10, fk + 26, frame) * (1 - smooth(600, 656, frame)); - } - if (!isFailure && depth >= 1 && depth <= c.causeDepth) { - const wd = waveArrivalFrame(depth, c.hopSlot); - // Interrogation flicker: 24 integer sine cycles per loop, per-depth phase. - const flicker = 0.75 + 0.25 * Math.sin(2 * Math.PI * 24 * loopPhase + 1.7 * depth); - z += env(frame, wd - 10, wd, wd + 28, wd + 64) * flicker; - // Scanned ember. - z += 0.08 * smooth(wd + 28, wd + 64, frame) * (1 - smooth(580, 640, frame)); - } - if (isCause) { - // Cause ignition rides the EXISTING recall response in render-nodes.wgsl: - // spectral() thin-film band + white-hot core + sprite swell, for free. - x += env(frame, 520, 546, 640, 700); - } - - return { x, y, z, w }; -} - -// --------------------------------------------------------------------------- -// Plan builder -// --------------------------------------------------------------------------- - -const UINTS_PER_STEP = 4; - -function emptyPlan(nodeCount: number): RescuePlan { - const waveData = new Uint32Array(nodeCount); - waveData.fill(UNREACHED); - return { - viable: false, - failureIndex: -1, - causeIndex: -1, - lookalikeIndices: [], - hopDepths: new Uint16Array(nodeCount).fill(UNREACHED), - causeDepth: 0, - hopSlot: hopSlotFor(3), - waveData, - pathData: new Uint32Array(4), - pathMetas: [], - spineBeats: [], - verdict: { - headline: 'root cause found', - causeLabel: '', - failureLabel: '', - causeDate: '', - hops: 0, - k: 0, - receipt: '' - }, - consts: { hopSlot: hopSlotFor(3), causeDepth: 3 } - }; -} - -/** - * Build the full deterministic salience-rescue plan. Same graph + seed → - * identical plan (byte-identical typed arrays). Empty/tiny/edgeless graphs - * survive with viable:false — the field breathes, no fake cause. - */ -export function buildRescuePlan( - response: GraphResponse, - graph: ObservatoryGraph, - seed: string -): RescuePlan { - const n = graph.nodes.length; - if (n === 0) return emptyPlan(0); - - const positions = layoutPositions(graph, seed); - const failureIndex = pickFailureIndex(graph, positions); - if (failureIndex < 0) return emptyPlan(n); - - const { depths, parents } = bfsFromFailure(graph, failureIndex); - const cause = pickCauseIndex(response, graph, depths, failureIndex); - if (cause.index < 0) { - const plan = emptyPlan(n); - plan.failureIndex = failureIndex; - plan.hopDepths = depths; - return plan; - } - - const causeIndex = cause.index; - const causeDepth = Math.max(1, cause.depth); - const hopSlot = hopSlotFor(causeDepth); - const W = (d: number) => waveArrivalFrame(d, hopSlot); - - const lookalikeIndices = pickLookalikes(positions, n, failureIndex, causeIndex, graph.centerIndex); - const K = lookalikeIndices.length; - - // --- waveData packing (1 u32/node) --- - const waveData = new Uint32Array(n); - for (let i = 0; i < n; i++) { - let word = depths[i] & 0xffff; - if (i === failureIndex) word |= 1 << 16; - if (i === causeIndex) word |= 1 << 17; - waveData[i] = word; - } - lookalikeIndices.forEach((li, k) => { - waveData[li] |= (1 << 18) | (k << 19); - }); - - // --- PathStep emission: probes, wave tree, arc --- - interface Step { - src: number; - dst: number; - bf: number; - kind: number; - beatKind: string; - } - const steps: Step[] = []; - - // Probe beams (vector search visibly probing and failing ON CAMERA). - lookalikeIndices.forEach((li, k) => { - steps.push({ - src: failureIndex, - dst: li, - bf: lookalikeFrame(k), - kind: PATH_KIND.probe, - beatKind: 'probe' - }); - }); - - // Wave tree, capped at MAX_WAVE_STEPS with the failure→cause parent chain - // guaranteed, then remaining by (depth asc, index asc). - const chainNodes: number[] = []; - { - let v = causeIndex; - while (v !== failureIndex && v >= 0 && parents[v] >= 0) { - chainNodes.push(v); - v = parents[v]; - } - } - const chainSet = new Set(chainNodes); - const rest: number[] = []; - for (let v = 0; v < n; v++) { - if (v === failureIndex || chainSet.has(v)) continue; - const d = depths[v]; - if (d === UNREACHED || d < 1 || d > causeDepth) continue; - if (parents[v] < 0) continue; - rest.push(v); - } - rest.sort((a, b) => depths[a] - depths[b] || a - b); - const waveNodes = [...chainNodes.slice().reverse(), ...rest].slice(0, MAX_WAVE_STEPS); - waveNodes.sort((a, b) => depths[a] - depths[b] || a - b); - for (const v of waveNodes) { - steps.push({ - src: parents[v], - dst: v, - bf: W(depths[v]), - kind: PATH_KIND.backwardCause, - beatKind: 'wave' - }); - } - - // The causal arc: cause → failure, lands at 560 (kind 1 = crimson-magenta tint). - steps.push({ - src: causeIndex, - dst: failureIndex, - bf: ARC_FRAME, - kind: PATH_KIND.backwardCause, - beatKind: 'arc' - }); - - const pathData = new Uint32Array(Math.max(1, steps.length) * UINTS_PER_STEP); - const pathMetas: PathStepMeta[] = []; - steps.forEach((s, i) => { - pathData[i * UINTS_PER_STEP + 0] = s.src; - pathData[i * UINTS_PER_STEP + 1] = s.dst; - pathData[i * UINTS_PER_STEP + 2] = s.bf; - pathData[i * UINTS_PER_STEP + 3] = s.kind; - pathMetas.push({ - sourceIndex: s.src, - targetIndex: s.dst, - beatFrame: s.bf, - kind: s.kind, - beatKind: s.beatKind, - nodeId: graph.nodes[s.dst].id, - label: truncateLabel(graph.nodes[s.dst].label) - }); - }); - - // --- Curated spine beats (unique, strictly increasing beatFrames) --- - const failureLabel = truncateLabel(graph.nodes[failureIndex].label); - const causeLabel = truncateLabel(graph.nodes[causeIndex].label); - const spineBeats: PathStepMeta[] = []; - const spine = (beatFrame: number, kind: number, label: string, nodeId: string) => { - spineBeats.push({ - sourceIndex: failureIndex, - targetIndex: failureIndex, - beatFrame, - kind, - beatKind: 'rescue', - nodeId, - label - }); - }; - spine(DETONATE_FRAME, 1, `failure: ${failureLabel}`, graph.nodes[failureIndex].id); - lookalikeIndices.forEach((li, k) => { - spine(lookalikeFrame(k), 0, `lookalike ✗ · ${truncateLabel(graph.nodes[li].label)}`, graph.nodes[li].id); - }); - spine(W(1), 1, 'reaching backward through time', 'rescue-wave-start'); - if (causeDepth >= 2 && W(causeDepth) !== W(1)) { - spine(W(causeDepth), 1, `scrubbing past · ${causeDepth} hops`, 'rescue-wave-deep'); - } - spine(ARC_FRAME, 1, `causal arc · ${causeLabel}`, graph.nodes[causeIndex].id); - spine(VERDICT_START, 1, 'root cause found', 'rescue-verdict'); - - // --- Verdict copy (REAL memory labels + real date) --- - const createdAt = response.nodes.find((nd) => nd.id === graph.nodes[causeIndex].id)?.createdAt ?? ''; - const causeDate = createdAt ? createdAt.slice(0, 10) : ''; - const verdict: RescueVerdictCopy = { - headline: 'root cause found', - causeLabel, - failureLabel, - causeDate, - hops: causeDepth, - k: K, - receipt: `${causeDepth} hops back · ${causeDate} · vector search: 0 for ${K}` - }; - - return { - viable: true, - failureIndex, - causeIndex, - lookalikeIndices, - hopDepths: depths, - causeDepth, - hopSlot, - waveData, - pathData, - pathMetas, - spineBeats, - verdict, - consts: { hopSlot, causeDepth } - }; -} diff --git a/apps/dashboard/src/lib/observatory/rescue-renderer.ts b/apps/dashboard/src/lib/observatory/rescue-renderer.ts deleted file mode 100644 index f1fa818..0000000 --- a/apps/dashboard/src/lib/observatory/rescue-renderer.ts +++ /dev/null @@ -1,116 +0,0 @@ -/** - * Cognitive Observatory — salience-rescue choreography pass. - * - * Compute-only FramePass (no render(): nodes and ribbons draw via the - * NodeRenderer's existing pipelines). Uploads the per-node packed wave word - * once (static), then each frame extends the choreography INTO the NodeState - * demo lanes as a pure function of (frame, role, hopDepth) — no stateful - * integration, so capture mode (?frame=N) works with zero special-casing. - * - * PASS ORDER IS LOAD-BEARING: this pass MUST be constructed AFTER the - * NodeRenderer (the route guarantees it: handleReady creates NodeRenderer, - * the upload $effect creates RescueRenderer) so rescue_choreo encodes AFTER - * recall_sim in the same encoder and its demo-lane overwrite wins. recall_sim - * rewrites demo.x every frame with an afterglow window of bf+40..bf+200 — the - * causal arc at bf=560 would otherwise carry visible residual across the - * 719→0 loop seam. - * - * Three independent walls keep OTHER demos pixel-identical: - * (a) the route constructs this renderer only in the rescue branch, - * (b) compute() gates on params[9] === 2 ('salience-rescue' demo index), - * (c) demo.y/.z/.w have no other writer, so the new render-nodes terms - * multiply/add exact 0.0 elsewhere. - */ - -import type { ObservatoryEngine, FramePass } from './engine'; -import type { NodeRenderer } from './node-renderer'; -import type { RescuePlan } from './rescue-plan'; -import { rescueWGSL } from './shaders/rescue.wgsl'; - -/** DEMO_MODES.indexOf('salience-rescue') — types.ts, verified index 2. */ -const RESCUE_DEMO_ID = 2; - -export interface RescueRendererOptions { - engine: ObservatoryEngine; - nodeRenderer: NodeRenderer; - plan: RescuePlan; -} - -export class RescueRenderer implements FramePass { - private engine: ObservatoryEngine; - private nodeRenderer: NodeRenderer; - private plan: RescuePlan; - - private pipeline: GPUComputePipeline | null = null; - private bindGroup: GPUBindGroup | null = null; - private waveBuffer: GPUBuffer | null = null; - - constructor(opts: RescueRendererOptions) { - this.engine = opts.engine; - this.nodeRenderer = opts.nodeRenderer; - this.plan = opts.plan; - this.engine.addPass(this); - } - - /** Create the wave buffer + compute pipeline. Call after NodeRenderer.upload(). */ - upload(): void { - const device = this.engine.gpuDevice; - if (!device || !this.engine.paramsBuffer) return; - if (!this.plan.viable) return; - if (!this.nodeRenderer.nodeStateBuffer || this.nodeRenderer.nodeCountValue === 0) return; - - this.waveBuffer?.destroy(); - this.waveBuffer = device.createBuffer({ - label: 'observatory-rescue-wave', - size: Math.max(4, this.plan.waveData.byteLength), - usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST - }); - device.queue.writeBuffer(this.waveBuffer, 0, this.plan.waveData.buffer as ArrayBuffer); - - // hopSlot/causeDepth are baked into the shader as f32 literals — no - // uniform buffer, so the auto layout has nothing to strip. - const module = device.createShaderModule({ - label: 'observatory-rescue-choreo', - code: rescueWGSL(this.plan.consts) - }); - this.pipeline = device.createComputePipeline({ - label: 'observatory-rescue-choreo', - layout: 'auto', - compute: { module, entryPoint: 'rescue_choreo' } - }); - - // EXACTLY the 3 declared bindings — auto layout strips unused bindings - // and binding anything extra invalidates the group (the BirthRenderer - // lesson, birth-renderer.ts createComputePipeline). - this.bindGroup = device.createBindGroup({ - label: 'observatory-rescue-bind', - layout: this.pipeline.getBindGroupLayout(0), - entries: [ - { binding: 0, resource: { buffer: this.engine.paramsBuffer } }, - { binding: 1, resource: { buffer: this.nodeRenderer.nodeStateBuffer } }, - { binding: 2, resource: { buffer: this.waveBuffer } } - ] - }); - } - - /** FramePass — overwrite the four demo lanes for this frame (pure of frame). */ - compute(encoder: GPUCommandEncoder): void { - if (this.engine.params[9] !== RESCUE_DEMO_ID) return; - if (!this.pipeline || !this.bindGroup) return; - const n = this.nodeRenderer.nodeCountValue; - if (n === 0) return; - - const pass = encoder.beginComputePass({ label: 'observatory-rescue-choreo' }); - pass.setPipeline(this.pipeline); - pass.setBindGroup(0, this.bindGroup); - pass.dispatchWorkgroups(Math.ceil(n / 64)); - pass.end(); - } - - dispose(): void { - this.waveBuffer?.destroy(); - this.waveBuffer = null; - this.pipeline = null; - this.bindGroup = null; - } -} diff --git a/apps/dashboard/src/lib/observatory/shaders/birth-particles.wgsl.ts b/apps/dashboard/src/lib/observatory/shaders/birth-particles.wgsl.ts deleted file mode 100644 index 1bd405b..0000000 --- a/apps/dashboard/src/lib/observatory/shaders/birth-particles.wgsl.ts +++ /dev/null @@ -1,124 +0,0 @@ -/** - * Cognitive Observatory — birth particle compute pass (Moment B, Task B3). - * - * One invocation per particle (workgroup 64, dispatch ceil(N/64)). Each - * particle converges from its start position toward the target over the - * 720-frame loop: - * - * frames 0–239 : latent trace condensing (slow drift) - * frames 240–329: engram coalescence (accelerated convergence) - * frames 330–359: memory ignition (flash — handled in render) - * frames 360–509: associations engrave (hold at target) - * frames 510–719: stabilization (hold, then reset) - * - * All time terms are integer-cycles per 720 frames so the loop seam is - * invisible. Capture mode (params._pad == 1.0) skips integration. - * - * Particle layout (16 floats / 64 bytes per particle): - * start_life : xyz start position, w phase offset (stagger) - * target_size : xyz target position, w base size (1.0 + rng * 1.8) - * color_phase : rgb base color, w phase offset - * state : xyz current position (shader writes), w alpha - */ -export const birthParticlesWGSL = /* wgsl */ ` -struct Params { - frame: f32, - loop_phase: f32, - node_count: f32, - edge_count: f32, - path_count: f32, - pulse: f32, - viewport_w: f32, - viewport_h: f32, - brightness: f32, - demo_id: f32, - time: f32, - _pad: f32, -}; - -// 16 floats / 64 bytes per particle (matches birth-plan.ts layout). -struct BirthParticle { - start_life: vec4, - target_size: vec4, - color_phase: vec4, - state: vec4, -}; - -@group(0) @binding(0) var params: Params; -@group(0) @binding(1) var particles: array; - -@compute @workgroup_size(64) -fn birth_compute(@builtin(global_invocation_id) id: vec3) { - let i = id.x; - if (i >= arrayLength(&particles)) { - return; - } - - // Capture mode (params._pad == 1.0): skip physics integration. - // The storage-buffer state stays frozen at initial upload values. - if (params._pad == 1.0) { - return; - } - - var particle = particles[i]; - let frame = params.frame; - let phase = params.loop_phase; - - // --- Convergence choreography (integer cycles per 720-frame loop) --- - - // Phase offset (stagger) from start_life.w: 0..1 → delays convergence. - let stagger = particle.start_life.w; - - // Effective frame: staggered loop frame (wraps at 720). - let effFrame = fract(phase + stagger * 0.15) * 720.0; - - // --- Phase 1: latent trace condensing (frames 0–239) --- - // Slow drift toward target. - var t: f32; - if (effFrame < 240.0) { - // Smooth ease-in: 0 → 1 over 240 frames. - t = effFrame / 240.0; - t = t * t * (3.0 - 2.0 * t); // smoothstep - } - // --- Phase 2: engram coalescence (frames 240–329) --- - // Accelerated convergence to target. - else if (effFrame < 330.0) { - let localFrame = effFrame - 240.0; - // 0 → 1 over 90 frames, with slight overshoot then settle. - t = localFrame / 90.0; - t = t * t * (3.0 - 2.0 * t); - // Add a small overshoot (1.05) then settle back to 1.0. - t = 1.0 - 0.05 * (1.0 - t); - } - // --- Phase 3: memory ignition (frames 330–359) --- - // Hold at target (flash handled in render). - else if (effFrame < 360.0) { - t = 1.0; - } - // --- Phase 4: associations engrave (frames 360–509) --- - // Hold at target. - else if (effFrame < 510.0) { - t = 1.0; - } - // --- Phase 5: stabilization (frames 510–719) --- - // Hold at target, then fade alpha for reset. - else { - let localFrame = effFrame - 510.0; - // Fade alpha to 0 for seamless reset at frame 0. - t = 1.0; - particle.state.w = 1.0 - smoothstep(0.0, 150.0, localFrame); - } - - // Interpolate from start to target. - let startPos = particle.start_life.xyz; - let targetPos = particle.target_size.xyz; - // (WGSL forbids swizzle stores - reconstruct, preserving alpha in .w) - particle.state = vec4(mix(startPos, targetPos, t), particle.state.w); - - // Alpha: particles fade in during convergence, fade out during reset. - let fadeIn = smoothstep(0.0, 60.0, effFrame); - particle.state.w = max(particle.state.w, fadeIn * 0.8); - - particles[i] = particle; -} -`; diff --git a/apps/dashboard/src/lib/observatory/shaders/firewall.wgsl.ts b/apps/dashboard/src/lib/observatory/shaders/firewall.wgsl.ts deleted file mode 100644 index 18aae15..0000000 --- a/apps/dashboard/src/lib/observatory/shaders/firewall.wgsl.ts +++ /dev/null @@ -1,125 +0,0 @@ -/** - * Cognitive Observatory — firewall choreography compute pass (WGSL). - * - * One invocation per node. Decodes the packed fire word (firewall-plan.ts: - * bits 0-7 shockDelay, bit 8 isIntruder, bit 9 isSeverNeighbor, bits 10-13 - * sever slot k) and writes ALL FOUR NodeState demo lanes as PURE functions of - * (params.frame, params.loop_phase, packed word): - * - * demo.x ALWAYS 0.0 — the recall/thin-film grammar can never fire here - * demo.y intruder only: intrusion flare band (0..1], 36 integer sine - * cycles/loop, then the sustained MEMBRANE band [2.60..2.90], 12 - * integer cycles — one lane, two value ranges (render-nodes.wgsl - * separates them with min(fy,1) vs smoothstep(1.5, 2.2, fy)) - * demo.z ALWAYS 0.0 — the forgetting-horizon grammar can never fire here - * demo.w crimson shock: source detonation on the intruder, per-node rim as - * the radial front passes (arrival A = 150 + delay, amplitude fades - * with distance), sever-blink receipts at 345 + 21k - * - * This pass MUST encode AFTER recall_sim (simulate.wgsl) in the same encoder: - * recall_sim rewrites demo.x every frame from the path buffer (afterglow - * decays bf+40..bf+200 — the k=5 sever beam at bf=450 would leave residual - * demo.x near the seam). Overwriting all four lanes here is simultaneously - * the choreography, the loop-seam guarantee, and free ?frame=N capture - * support (stateless: same frame in → same lanes out). - * - * Every envelope has attack a0 ≥ 90 and release r1 ≤ 680 ⇒ exact 0.0 at - * frames 0 and 719. Sines are factors on zero-at-seam envelopes with INTEGER - * cycles per loop. The CPU mirror (firewall-plan.ts firewallEnvelopes) is - * machine-checked by the seam-zero test — keep both in lockstep. - * - * Bind group = EXACTLY the 3 declared bindings (params, nodes, fire). - */ - -export const firewallWGSL = /* wgsl */ ` -struct Params { - frame: f32, - loop_phase: f32, - node_count: f32, - edge_count: f32, - path_count: f32, - pulse: f32, - viewport_w: f32, - viewport_h: f32, - brightness: f32, - demo_id: f32, - time: f32, - _pad: f32, -}; - -struct Node { - pos_radius: vec4, - vel_retention: vec4, - color_flags: vec4, - demo: vec4, -}; - -@group(0) @binding(0) var params: Params; -@group(0) @binding(1) var nodes: array; -// 1 u32/node: bits 0-7 shockDelay, 8 isIntruder, 9 isSeverNeighbor, -// 10-13 sever slot k (firewall-plan.ts packing). Every node carries a delay. -@group(0) @binding(2) var fire: array; - -const TAU: f32 = 6.28318530717958647; - -fn env(f: f32, a0: f32, a1: f32, r0: f32, r1: f32) -> f32 { - return smoothstep(a0, a1, f) * (1.0 - smoothstep(r0, r1, f)); -} - -@compute @workgroup_size(64) -fn firewall_choreo(@builtin(global_invocation_id) id: vec3) { - let i = id.x; - if (i >= u32(params.node_count)) { - return; - } - if (i >= arrayLength(&fire)) { - return; - } - // Belt-and-braces atop the TS gate: firewall is demo index 4. - if (params.demo_id != 4.0) { - return; - } - - let packed = fire[i]; - let delay = f32(packed & 0xffu); - let is_intruder = (packed & 0x100u) != 0u; - let is_sever = (packed & 0x200u) != 0u; - let k = f32((packed >> 10u) & 0xfu); - - let f = params.frame; - - var fy = 0.0; - var fw = 0.0; - - if (is_intruder) { - // Intrusion flare: sickly strobe, band (0..1], 36 integer cycles/loop. - // C¹ handoff into the membrane over 330-332 (the rise sweeps the flare - // band exactly once — the condensation read is intentional). - fy = env(f, 90.0, 96.0, 310.0, 332.0) - * (0.55 + 0.45 * sin(TAU * 36.0 * params.loop_phase)); - // Membrane: sustained ring band [2.60..2.90], 12 integer cycles/loop. - fy = fy + env(f, 330.0, 352.0, 620.0, 680.0) - * (2.75 + 0.15 * sin(TAU * 12.0 * params.loop_phase)); - // Source detonation as the front leaves. - fw = env(f, 148.0, 153.0, 162.0, 196.0); - } else { - // Crimson rim as the radial front passes: arrival A = 150 + delay, - // amplitude fades with distance; A ∈ [150, 294] ⇒ all rims dead by 320. - let a = 150.0 + delay; - let amp = 0.9 - 0.45 * (delay / 144.0); - fw = amp * env(f, a - 2.0, a + 3.0, a + 8.0, a + 26.0); - if (is_sever) { - // Node-side receipt of the severed edge; last release 474. - let sk = 345.0 + 21.0 * k; - fw = fw + 0.6 * env(f, sk - 4.0, sk, sk + 6.0, sk + 24.0); - } - } - - // WGSL forbids swizzle stores — reconstruct the FULL vec4; pos/vel/color - // lanes pass through untouched (the force sim owns them). demo.x and - // demo.z are hard 0.0: the recall and horizon grammars can never fire here. - var node = nodes[i]; - node.demo = vec4(0.0, fy, 0.0, fw); - nodes[i] = node; -} -`; diff --git a/apps/dashboard/src/lib/observatory/shaders/forgetting.wgsl.ts b/apps/dashboard/src/lib/observatory/shaders/forgetting.wgsl.ts deleted file mode 100644 index 32bd356..0000000 --- a/apps/dashboard/src/lib/observatory/shaders/forgetting.wgsl.ts +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Cognitive Observatory — forgetting-horizon choreography compute pass (WGSL). - * - * One invocation per node. Decodes the packed horizon word (forgetting-plan.ts: - * bits 0-7 rank, bit 8 isDrifting, bit 9 isRescued, bits 10-11 rescue slot k) - * and writes ALL FOUR NodeState demo lanes as PURE functions of - * (params.frame, packed word): - * - * demo.x rescue ignition (existing thin-film recall response) on the 3 rescued - * demo.y ALWAYS 0.0 — the rescue searchlight grammar can never fire here - * demo.z horizon fade-and-fall (vertex drift + shrink, fragment dim to ~6%) - * demo.w ALWAYS 0.0 — the shock grammar can never fire here - * - * This pass MUST encode AFTER recall_sim (simulate.wgsl) in the same encoder: - * recall_sim rewrites demo.x every frame from the path buffer (afterglow decays - * bf+40..bf+200 — the k=2 rescue ribbon at bf=438 would leave residual demo.x - * near the seam). Overwriting all four lanes here is simultaneously the - * choreography, the loop-seam guarantee, and free ?frame=N capture support - * (stateless: same frame in → same lanes out). - * - * Every term has attack a0 ≥ 90 and is multiplied by the master release - * 1−smoothstep(660, 712, f) ⇒ exact 0.0 at frames 0 and 719. NO sines in this - * moment. The CPU mirror (forgetting-plan.ts forgettingEnvelopes) is - * machine-checked by the seam-zero test — keep both in lockstep. - * - * Bind group = EXACTLY the 3 declared bindings (params, nodes, horizon). - */ - -export const forgettingWGSL = /* wgsl */ ` -struct Params { - frame: f32, - loop_phase: f32, - node_count: f32, - edge_count: f32, - path_count: f32, - pulse: f32, - viewport_w: f32, - viewport_h: f32, - brightness: f32, - demo_id: f32, - time: f32, - _pad: f32, -}; - -struct Node { - pos_radius: vec4, - vel_retention: vec4, - color_flags: vec4, - demo: vec4, -}; - -@group(0) @binding(0) var params: Params; -@group(0) @binding(1) var nodes: array; -// 1 u32/node: bits 0-7 rank, 8 isDrifting, 9 isRescued, 10-11 rescue slot k -// (forgetting-plan.ts packing). Non-drifting nodes are exactly 0. -@group(0) @binding(2) var horizon: array; - -fn env(f: f32, a0: f32, a1: f32, r0: f32, r1: f32) -> f32 { - return smoothstep(a0, a1, f) * (1.0 - smoothstep(r0, r1, f)); -} - -@compute @workgroup_size(64) -fn forgetting_choreo(@builtin(global_invocation_id) id: vec3) { - let i = id.x; - if (i >= u32(params.node_count)) { - return; - } - if (i >= arrayLength(&horizon)) { - return; - } - // Belt-and-braces atop the TS gate: forgetting-horizon is demo index 3. - if (params.demo_id != 3.0) { - return; - } - - let packed = horizon[i]; - let is_drifting = (packed & 0x100u) != 0u; - let is_rescued = (packed & 0x200u) != 0u; - let rank01 = f32(packed & 0xffu) / 255.0; - let k = f32((packed >> 10u) & 0x3u); - - let f = params.frame; - // Master release: every lane is exactly 0.0 by frame 712 — the seam wall. - let master = 1.0 - smoothstep(660.0, 712.0, f); - - var dx = 0.0; - var dz = 0.0; - - if (is_drifting) { - let onset = 90.0 + 42.0 * rank01; - // Phase 1 — the drift: dim + fall to the 0.55 plateau, retention-staggered. - let phase1 = 0.55 * smoothstep(onset, onset + 210.0, f); - if (is_rescued) { - let rk = 318.0 + 60.0 * k; - // Snap-back begins 22 frames before the recall ribbon lands at rk. - dz = master * phase1 * (1.0 - smoothstep(rk - 22.0, rk + 6.0, f)); - // Ignition rides the EXISTING recall response (render-nodes.wgsl): - // spectral() thin-film band + white-hot core + sprite swell for free. - dx = master * env(f, rk - 26.0, rk, rk + 60.0, rk + 130.0); - } else { - // Phase 2 — the sink: to exactly 1.0 over 640..660 (the ~6% floor era). - let phase2 = 0.45 * smoothstep(480.0 + 24.0 * rank01, 640.0, f); - dz = master * (phase1 + phase2); - } - } - - // WGSL forbids swizzle stores — reconstruct the FULL vec4; pos/vel/color - // lanes pass through untouched (the force sim owns them). demo.y and - // demo.w are hard 0.0: the rescue/firewall grammars can never fire here. - var node = nodes[i]; - node.demo = vec4(dx, 0.0, dz, 0.0); - nodes[i] = node; -} -`; diff --git a/apps/dashboard/src/lib/observatory/shaders/render-edges.wgsl.ts b/apps/dashboard/src/lib/observatory/shaders/render-edges.wgsl.ts deleted file mode 100644 index ab3bc32..0000000 --- a/apps/dashboard/src/lib/observatory/shaders/render-edges.wgsl.ts +++ /dev/null @@ -1,166 +0,0 @@ -/** - * Cognitive Observatory — edge wavefront render shader (WGSL). - * - * Draws additive edges between nodes, with a traveling wavefront that - * travels from source → target at the beat frame. The wavefront is a - * glowing pulse that rides along the edge, brightening as it approaches - * the target node (spec §7.2: additive bloom, thin-film spectral glow). - * - * Layout contracts: Params = types.PARAMS_FLOATS, Edge = 2×vec2 - * (types.ts UINTS_PER_EDGE), PathStep = 4×vec4 (types.ts - * UINTS_PER_PATHSTEP). - */ -export const renderEdgesWGSL = /* wgsl */ ` -struct Params { - frame: f32, - loop_phase: f32, - node_count: f32, - edge_count: f32, - path_count: f32, - pulse: f32, - viewport_w: f32, - viewport_h: f32, - brightness: f32, - demo_id: f32, - time: f32, - _pad: f32, -}; - -struct Camera { - view_proj: mat4x4, - right: vec4, - up: vec4, -}; - -struct Node { - pos_radius: vec4, - vel_retention: vec4, - color_flags: vec4, - demo: vec4, -}; - -// x source index, y target index, z beat frame, w kind (0 recall, 1 backward) -@group(0) @binding(0) var params: Params; -@group(0) @binding(1) var camera: Camera; -// Source/target node indices (2 u32 per edge). -@group(0) @binding(2) var edges: array>; -// PathStep buffer for wavefront timing. -@group(0) @binding(3) var path: array>; -// NodeState storage buffer (positions for edge endpoints). -@group(0) @binding(4) var nodes: array; - -// Iridescent thin-film band — ported EXACTLY from causal-brain-demo.html -// spectral(w) (visual DNA §7.1): indigo → cyan-teal → mint → magenta rim. -fn spectral(w_in: f32) -> vec3 { - let w = fract(w_in); - let stops = array, 4>( - vec3(0.20, 0.28, 0.95), // indigo - vec3(0.20, 0.85, 0.90), // cyan-teal - vec3(0.45, 1.00, 0.72), // mint - vec3(0.85, 0.45, 1.00) // magenta rim - ); - let f = w * 4.0; - let i = u32(floor(f)) % 4u; - let frac = f - floor(f); - let a = stops[i]; - let b = stops[(i + 1u) % 4u]; - return mix(a, b, frac); -} - -struct VSOut { - @builtin(position) clip: vec4, - @location(0) color: vec3, - @location(1) width: f32, -}; - -@vertex -fn vs_main( - @builtin(vertex_index) vi: u32, - @builtin(instance_index) ii: u32 -) -> VSOut { - var out: VSOut; - - let edgeCount = u32(params.edge_count); - if (ii >= edgeCount) { - out.clip = vec4(0.0, 0.0, 2.0, 1.0); - return out; - } - - let edge = edges[ii]; - let srcIdx = edge.x; - let tgtIdx = edge.y; - - if (srcIdx >= u32(params.node_count) || tgtIdx >= u32(params.node_count)) { - out.clip = vec4(0.0, 0.0, 2.0, 1.0); - return out; - } - - let src = nodes[srcIdx]; - let tgt = nodes[tgtIdx]; - - // Two vertices per edge: source (vi=0) and target (vi=1). - let pos = select(src.pos_radius.xyz, tgt.pos_radius.xyz, vi == 1u); - - // World-space position. - let world = pos; - out.clip = camera.view_proj * vec4(world, 1.0); - - // Wavefront computation: find the nearest path beat for this edge. - let pathCount = u32(params.path_count); - var waveIntensity = 0.0; - var waveT = 1.0; // 0 = source, 1 = target - - for (var s = 0u; s < pathCount; s = s + 1u) { - let step = path[s]; - let srcIdxS = step.x; - let tgtIdxS = step.y; - let bf = f32(step.z); - - // Check if this path step uses the same source→target. - if (srcIdxS == srcIdx && tgtIdxS == tgtIdx) { - let frame = params.frame; - // Wavefront: sharp pulse traveling from source to target. - let attack = smoothstep(bf - 10.0, bf + 2.0, frame); - let decay = 1.0 - smoothstep(bf + 30.0, bf + 180.0, frame); - waveIntensity = max(waveIntensity, attack * decay); - - // Wave position along edge (0 = source, 1 = target). - let arrival = bf - 10.0; - let end = bf + 30.0; - if (frame >= arrival && frame <= end) { - waveT = (frame - arrival) / (end - arrival); - } else if (frame > end) { - waveT = 1.0; - } - } - } - - // Edge base color: blend of source and target node base colors. - let srcColor = src.color_flags.rgb; - let tgtColor = tgt.color_flags.rgb; - let baseColor = mix(srcColor, tgtColor, 0.5); - - // Wavefront color: thin-film spectral band, modulated by wave position. - let waveColor = spectral(waveT + params.loop_phase); - - // Combine: base edge (dim) + wavefront pulse (bright, additive). - let edgeAlpha = 0.08 * params.brightness; // dim connecting line - let waveAlpha = waveIntensity * 0.9 * params.brightness; // bright pulse - - // Spectral hue rides the wavefront. - out.color = baseColor * edgeAlpha + waveColor * waveAlpha; - - // Line width: thicker at the wavefront for visibility. - out.width = 1.0 + waveIntensity * 3.0; - - return out; -} - -@fragment -fn fs_main(in: VSOut) -> @location(0) vec4 { - // Soft edge: feather the line edges. - let alpha = smoothstep(0.0, 0.5, in.width) * 0.6; - // Additive: alpha is ignored, light accumulates. - return vec4(in.color, 1.0); -} -`; diff --git a/apps/dashboard/src/lib/observatory/shaders/render-nodes.wgsl.ts b/apps/dashboard/src/lib/observatory/shaders/render-nodes.wgsl.ts deleted file mode 100644 index 070e387..0000000 --- a/apps/dashboard/src/lib/observatory/shaders/render-nodes.wgsl.ts +++ /dev/null @@ -1,269 +0,0 @@ -/** - * Cognitive Observatory — node billboard shader (WGSL). - * - * Instanced soft-glow sprites read straight from the NodeState storage buffer - * (compute-boids render pattern, spec §1). Additive blending onto the void so - * overlapping memories build light instead of z-fighting. - * - * Visual DNA §7: base hue = FSRS state color (meaning at rest); the global - * breath `pulse` modulates halo energy so the field is alive even when idle. - * Layout contracts: Params = types.PARAMS_FLOATS, Node = 4×vec4f (types.ts). - */ -export const renderNodesWGSL = /* wgsl */ ` -struct Params { - frame: f32, - loop_phase: f32, - node_count: f32, - edge_count: f32, - path_count: f32, - pulse: f32, - viewport_w: f32, - viewport_h: f32, - brightness: f32, - demo_id: f32, - time: f32, - _pad: f32, -}; - -struct Camera { - view_proj: mat4x4, - right: vec4, - up: vec4, -}; - -struct Node { - pos_radius: vec4, - vel_retention: vec4, - color_flags: vec4, - demo: vec4, -}; - -@group(0) @binding(0) var params: Params; -@group(0) @binding(1) var camera: Camera; -@group(0) @binding(2) var nodes: array; - -// Iridescent thin-film band — ported EXACTLY from causal-brain-demo.html -// spectral(w) (visual DNA §7.1): indigo → cyan-teal → mint → magenta rim, -// wrapping. Activation glow rides this band; base color stays FSRS state. -fn spectral(w_in: f32) -> vec3 { - let w = fract(w_in); - // var, not let: WGSL only allows dynamic indexing through a reference. - var stops = array, 4>( - vec3(0.20, 0.28, 0.95), // indigo - vec3(0.20, 0.85, 0.90), // cyan-teal - vec3(0.45, 1.00, 0.72), // mint - vec3(0.85, 0.45, 1.00) // magenta rim - ); - let f = w * 4.0; - let i = u32(floor(f)) % 4u; - let frac = f - floor(f); - let a = stops[i]; - let b = stops[(i + 1u) % 4u]; - return mix(a, b, frac); -} - -struct VSOut { - @builtin(position) clip: vec4, - @location(0) uv: vec2, - // Per-instance constants: flat interpolation guarantees the flag bit - // field survives the raster stage bit-exact (no barycentric rounding). - @location(1) @interpolate(flat) color: vec3, - // x retention, y flags (bit field as f32), z recall intensity, w radius - @location(2) @interpolate(flat) misc: vec4, - // Per-demo choreography lanes (demo.y, demo.z, demo.w), gated by demo_id: - // rescue (2) searchlight/wave/shock, forgetting-horizon (3) fade-and-fall, - // firewall (4) flare-membrane/shock. Each demo's choreography pass is the - // ONLY writer of its lanes, and every gated term below is an exact no-op - // when its lane is 0.0 — other demos stay pixel-identical. - @location(3) @interpolate(flat) demo_yzw: vec3, -}; - -// Quad corners for two triangles (vertex_index 0..5). -const CORNERS = array, 6>( - vec2(-1.0, -1.0), - vec2( 1.0, -1.0), - vec2( 1.0, 1.0), - vec2(-1.0, -1.0), - vec2( 1.0, 1.0), - vec2(-1.0, 1.0) -); - -@vertex -fn vs_main( - @builtin(vertex_index) vi: u32, - @builtin(instance_index) ii: u32 -) -> VSOut { - var out: VSOut; - if (ii >= u32(params.node_count)) { - // degenerate — clipped away - out.clip = vec4(0.0, 0.0, 2.0, 1.0); - return out; - } - - let node = nodes[ii]; - let corner = CORNERS[vi]; - - // Breath: halo geometry swells ~6% on the global pulse (§7.2), and the - // center memory breathes a touch deeper — a heartbeat, not a strobe. - let flags = u32(node.color_flags.w); - let is_center = (flags & 1u) != 0u; - var breath = 1.0 + 0.06 * params.pulse; - if (is_center) { - breath = 1.0 + 0.12 * params.pulse; - } - - // Sprite spans ~3.2× the core radius so the halo has room to feather out. - // Recall activation swells the sprite — the wavefront physically blooms. - // Per-demo choreography lanes swell it too, gated by demo_id so each - // demo's grammar can never leak into another (lanes are 0.0 elsewhere, - // and the gate makes the no-op structural, not just numerical). - let recall = node.demo.x; - let dy = node.demo.y; - let dz = node.demo.z; - let dw = node.demo.w; - var lane_swell = 0.0; - if (params.demo_id == 2.0) { - // salience-rescue: searchlight pop, wave shiver, shock bloom. - lane_swell = 0.5 * dy + 0.25 * dz + 0.9 * dw; - } else if (params.demo_id == 4.0) { - // firewall: intrusion flare pop (band (0..1]), membrane presence - // (band [2.6..2.9] via the range gate), crimson shock bloom. - lane_swell = 0.35 * min(dy, 1.0) + 0.3 * smoothstep(1.5, 2.2, dy) + 0.55 * dw; - } - // forgetting-horizon (demo 3): VISUAL displacement toward the horizon — - // down and away from the field axis, ~40.5 units at dz = 1 — plus a - // shrink. pos_radius is NEVER written (the force sim owns positions); - // drift is pure of demo.z, so ?frame=N capture stays exact. CPU mirror: - // forgetting-plan.ts horizonDrift(). - var horizon_scale = 1.0; - var drift = vec3(0.0); - if (params.demo_id == 3.0) { - let dzc = clamp(dz, 0.0, 1.0); - horizon_scale = 1.0 - 0.35 * dzc; - if (dz > 0.0) { - let p = node.pos_radius.xyz; - let r_xz = max(length(p.xz), 0.001); - let away = vec3(p.x / r_xz, 0.0, p.z / r_xz); - drift = dzc * (vec3(0.0, -34.0, 0.0) + away * 22.0); - } - } - let half_size = node.pos_radius.w * 3.2 * breath * (1.0 + 0.9 * recall) - * (1.0 + lane_swell) * horizon_scale; - let world = node.pos_radius.xyz + drift - + camera.right.xyz * corner.x * half_size - + camera.up.xyz * corner.y * half_size; - - out.clip = camera.view_proj * vec4(world, 1.0); - out.uv = corner; - out.color = node.color_flags.rgb; - out.misc = vec4(node.vel_retention.w, node.color_flags.w, node.demo.x, node.pos_radius.w); - out.demo_yzw = vec3(dy, dz, dw); - return out; -} - -@fragment -fn fs_main(in: VSOut) -> @location(0) vec4 { - let d = length(in.uv); - if (d > 1.0) { - discard; - } - - let retention = in.misc.x; - let flags = u32(in.misc.y); - let suppressed = (flags & 2u) != 0u; - let is_center = (flags & 1u) != 0u; - - // Soft sprite: hot core + feathered halo. The halo rides the breath pulse. - let core = smoothstep(0.22, 0.0, d); - let halo = pow(max(1.0 - d, 0.0), 2.4); - var intensity = core * 1.35 + halo * (0.42 + 0.18 * params.pulse); - - // Meaning layer: low-retention memories glow dimmer (drifting toward the - // horizon), the center anchor reads brightest. - intensity = intensity * (0.45 + 0.55 * retention); - if (is_center) { - intensity = intensity * 1.6; - } - if (suppressed) { - intensity = intensity * 0.28; - } - - var color = in.color * intensity; - - // Forgetting-horizon (demo 3): multiplicative dim toward near-black as - // demo.z rises. Floor 0.06 — never fully gone, always retrievable. Sits - // BEFORE the recall block so a rescued memory's ignition burns through - // the fade. demo_yzw.y carries demo.z (vec3 = y/z/w lanes). - if (params.demo_id == 3.0) { - color = color * mix(1.0, 0.06, clamp(in.demo_yzw.y, 0.0, 1.0)); - } - - // Recall activation (§7.1): the thin-film band takes over as the wave - // lands. Hue drifts exactly ONE full spectral cycle per 720-frame loop - // (loop_phase wraps 0→1 and spectral() fract-wraps) — oil-slick shimmer - // with a mathematically invisible loop seam. - let recall = in.misc.z; - if (recall > 0.001) { - let band = spectral(0.1 + params.loop_phase + d * 0.35); - let activation = band * recall * (core * 1.7 + halo * 0.9); - // white-hot pinpoint at full ignition - let flash = vec3(1.0, 1.0, 1.0) * core * recall * 0.55; - color = color + activation + flash; - } - - // Per-demo choreography lanes — gated by demo_id AND on nonzero values so - // every other demo is pixel-unchanged (each demo's pass is the only - // writer of its lanes, and lanes are exactly 0.0 everywhere else). - if (params.demo_id == 2.0) { - if (in.demo_yzw.x > 0.001) { - // Searchlight: cold clinical white — unmistakably NOT the spectral grammar. - color = color + vec3(0.82, 0.90, 1.00) * in.demo_yzw.x * (core * 1.8 + halo * 0.7); - } - if (in.demo_yzw.y > 0.001) { - // Interrogation shimmer: icy spectral strobe as the wave scrubs the past. - color = color + spectral(0.55 + params.loop_phase) * in.demo_yzw.y * (core * 0.9 + halo * 0.5) - + vec3(1.0) * core * in.demo_yzw.y * 0.2; - } - if (in.demo_yzw.z > 0.001) { - // Detonation: crimson blaze + warm-white pinpoint. - color = color + vec3(1.00, 0.16, 0.10) * in.demo_yzw.z * (core * 1.9 + halo * 1.1) - + vec3(1.0, 0.85, 0.8) * core * in.demo_yzw.z * 0.4; - } - } else if (params.demo_id == 4.0) { - // firewall: demo.y carries TWO value bands — intrusion flare (0..1] - // and membrane [2.6..2.9] — separated by range, one lane. demo.w is - // the crimson shock rim / sever blink. (demo_yzw = y/z/w lanes.) - let fy = in.demo_yzw.x; - let fw = in.demo_yzw.z; - // Intrusion flare: sickly green-white — a hue deliberately OUTSIDE - // both the FSRS palette and the thin-film band. Continuous across the - // band boundary (fades out as fy climbs toward the membrane band). - let flare = min(fy, 1.0) * (1.0 - smoothstep(1.0, 1.8, fy)); - if (flare > 0.001) { - color = color + vec3(0.62, 1.00, 0.55) * flare * (core * 1.7 + halo * 0.9) - + vec3(0.90, 1.00, 0.85) * core * flare * 0.5; - } - // Membrane: quarantine ring at d ≈ 0.75 with fresnel-ish falloff — - // green body, crimson edge. exp(-q·q) squares by multiplication and - // the pow base is clamped ≥ 0 (no pow(neg) anywhere). - let mw = smoothstep(1.5, 2.2, fy); - if (mw > 0.001) { - let q = (d - 0.75) * 9.0; - let ring = exp(-q * q); - let fresnel = pow(clamp(d / 0.75, 0.0, 1.0), 3.0); - let ring_col = mix(vec3(0.55, 1.00, 0.60), vec3(1.00, 0.20, 0.16), - smoothstep(0.72, 0.92, d)); - color = color + ring_col * ring * fresnel * mw * 1.4; - } - // Shockwave: crimson RIM as the front passes (a rim, not a blaze). - if (fw > 0.001) { - let rim = smoothstep(0.45, 0.8, d) * (1.0 - smoothstep(0.85, 1.0, d)); - color = color + vec3(1.00, 0.14, 0.10) * rim * fw * 1.5 - + vec3(1.00, 0.60, 0.50) * core * fw * 0.15; - } - } - - // Additive target (src=one, dst=one): alpha is ignored, light accumulates. - return vec4(color * params.brightness, 1.0); -} -`; diff --git a/apps/dashboard/src/lib/observatory/shaders/render-path.wgsl.ts b/apps/dashboard/src/lib/observatory/shaders/render-path.wgsl.ts deleted file mode 100644 index 6bff3cd..0000000 --- a/apps/dashboard/src/lib/observatory/shaders/render-path.wgsl.ts +++ /dev/null @@ -1,181 +0,0 @@ -/** - * Cognitive Observatory — recall path edge wavefront (Increment 6). - * - * One instanced screen-aligned quad per path step. The vertex shader fetches - * both endpoint nodes from the NodeState storage buffer (GraphWaGu edge_vert - * pattern, spec §1) and extrudes a constant-screen-width ribbon between them. - * The fragment draws a light packet traveling source → target, timed to land - * exactly on the beat frame, with a fading trail behind it. - * - * Deterministic: wave position is a pure function of (frame, beatFrame). - */ -export const renderPathWGSL = /* wgsl */ ` -struct Params { - frame: f32, - loop_phase: f32, - node_count: f32, - edge_count: f32, - path_count: f32, - pulse: f32, - viewport_w: f32, - viewport_h: f32, - brightness: f32, - demo_id: f32, - time: f32, - _pad: f32, -}; - -struct Camera { - view_proj: mat4x4, - right: vec4, - up: vec4, -}; - -struct Node { - pos_radius: vec4, - vel_retention: vec4, - color_flags: vec4, - demo: vec4, -}; - -@group(0) @binding(0) var params: Params; -@group(0) @binding(1) var camera: Camera; -@group(0) @binding(2) var nodes: array; -// x source index, y target index, z beat frame, w kind (0 recall, 1 backward) -@group(0) @binding(3) var path: array>; - -// Same thin-film band as the node shader (§7.1). -fn spectral(w_in: f32) -> vec3 { - let w = fract(w_in); - // var, not let: WGSL only allows dynamic indexing through a reference. - var stops = array, 4>( - vec3(0.20, 0.28, 0.95), - vec3(0.20, 0.85, 0.90), - vec3(0.45, 1.00, 0.72), - vec3(0.85, 0.45, 1.00) - ); - let f = w * 4.0; - let i = u32(floor(f)) % 4u; - let frac = f - floor(f); - let a = stops[i]; - let b = stops[(i + 1u) % 4u]; - return mix(a, b, frac); -} - -struct VSOut { - @builtin(position) clip: vec4, - // x: t along segment (0 source → 1 target), y: side (-1..1) - @location(0) uv: vec2, - // x: beat frame, y: kind, z: segment visible (0 skips degenerate steps) - // Per-instance constant — flat keeps it bit-exact through the raster. - @location(1) @interpolate(flat) beat: vec3, -}; - -// (t, side) corners for two triangles of the ribbon. -const RIBBON = array, 6>( - vec2(0.0, -1.0), - vec2(1.0, -1.0), - vec2(1.0, 1.0), - vec2(0.0, -1.0), - vec2(1.0, 1.0), - vec2(0.0, 1.0) -); - -@vertex -fn vs_main( - @builtin(vertex_index) vi: u32, - @builtin(instance_index) ii: u32 -) -> VSOut { - var out: VSOut; - if (ii >= u32(params.path_count)) { - out.clip = vec4(0.0, 0.0, 2.0, 1.0); - out.beat = vec3(0.0); - return out; - } - - let step = path[ii]; - let src = nodes[step.x]; - let dst = nodes[step.y]; - let corner = RIBBON[vi]; - - // Degenerate (origin beat: source == target) — emit nothing visible. - if (step.x == step.y) { - out.clip = vec4(0.0, 0.0, 2.0, 1.0); - out.beat = vec3(0.0); - return out; - } - - let a = camera.view_proj * vec4(src.pos_radius.xyz, 1.0); - let b = camera.view_proj * vec4(dst.pos_radius.xyz, 1.0); - - // NDC-space perpendicular for constant screen width. - let ndc_a = a.xy / max(a.w, 0.0001); - let ndc_b = b.xy / max(b.w, 0.0001); - var dir = ndc_b - ndc_a; - let dlen = max(length(dir), 0.0001); - dir = dir / dlen; - let perp = vec2(-dir.y, dir.x); - - // Ribbon half-width in NDC (aspect-corrected), ~2.5 px on a 900px-tall view. - let px = 2.5 / max(params.viewport_h, 1.0) * 2.0; - let width = vec2(px * (params.viewport_h / max(params.viewport_w, 1.0)), px); - - let base = mix(a, b, corner.x); - let offset = perp * width * corner.y * base.w; - out.clip = vec4(base.xy + offset, base.zw); - out.uv = vec2(corner.x, corner.y); - out.beat = vec3(f32(step.z), f32(step.w), 1.0); - return out; -} - -@fragment -fn fs_main(in: VSOut) -> @location(0) vec4 { - if (in.beat.z < 0.5) { - discard; - } - - let frame = params.frame; - let bf = in.beat.x; - let t = in.uv.x; - - // Wave departs 45 frames before the beat and lands exactly on it. - let progress = clamp((frame - (bf - 45.0)) / 45.0, 0.0, 1.0); - // Nothing before departure; trail lingers ~90 frames after arrival. - let live = smoothstep(bf - 46.0, bf - 44.0, frame) - * (1.0 - smoothstep(bf + 40.0, bf + 90.0, frame)); - if (live <= 0.001) { - discard; - } - - // The light packet: gaussian around the wavefront position. - let dwave = (t - progress) * 14.0; - let packet = exp(-dwave * dwave); - - // Fading trail behind the packet — provenance stays visible a beat. - var trail = 0.0; - if (t < progress) { - trail = (1.0 - (progress - t)) * 0.22; - } - - // Feather across the ribbon width. - let across = 1.0 - abs(in.uv.y); - let profile = across * across; - - // Backward/contradiction hops burn hotter into the magenta rim (§7.4). - // Hue drifts one full spectral cycle per loop (seamless at the wrap). - // Kind 2 (salience-rescue probe): a gray failing beam — vector search - // visibly probing lookalikes and coming back empty. Kinds 0/1 unchanged. - var band = spectral(0.15 + t * 0.35 + params.loop_phase); - var packet_white = 0.35; - if (in.beat.y > 1.5) { - band = vec3(0.62, 0.66, 0.72); - packet_white = 0.18; - } else if (in.beat.y > 0.5) { - band = mix(band, vec3(1.0, 0.25, 0.45), 0.55); - } - - let energy = (packet * 1.6 + trail) * profile * live; - let color = band * energy + vec3(1.0) * packet * profile * live * packet_white; - return vec4(color * params.brightness, 1.0); -} -`; diff --git a/apps/dashboard/src/lib/observatory/shaders/rescue.wgsl.ts b/apps/dashboard/src/lib/observatory/shaders/rescue.wgsl.ts deleted file mode 100644 index ee2689d..0000000 --- a/apps/dashboard/src/lib/observatory/shaders/rescue.wgsl.ts +++ /dev/null @@ -1,148 +0,0 @@ -/** - * Cognitive Observatory — salience-rescue choreography compute pass (WGSL). - * - * One invocation per node. Decodes the packed wave word (rescue-plan.ts: - * bits 0-15 hopDepth, 16 isFailure, 17 isCause, 18 isLookalike, 19-21 k) and - * writes ALL FOUR NodeState demo lanes as PURE functions of - * (params.frame, params.loop_phase, packed word): - * - * demo.x cause ignition (existing thin-film recall response) + symptom backlight - * demo.y searchlight cold-white flare on the K lookalikes + ash residue - * demo.z backward-wave interrogation flicker + scanned ember - * demo.w detonation spike + wound simmer + shockwave blinks + recognition flare - * - * This pass MUST encode AFTER recall_sim (simulate.wgsl) in the same encoder: - * recall_sim rewrites demo.x every frame from the path buffer (afterglow decays - * bf+40..bf+200 — the causal arc at bf=560 would leave a visible residual at - * frame 719). Overwriting all four lanes here is simultaneously the - * choreography, the loop-seam guarantee, and free ?frame=N capture support - * (stateless: same frame in → same lanes out). - * - * Every envelope term is A·smoothstep(a0,a1,f)·(1−smoothstep(r0,r1,f)) with - * attacks a0 ≥ 88 and releases r1 ≤ 700 ⇒ exact 0.0 at frames 0 and 719. - * The flicker sine runs 24 INTEGER cycles per loop. The CPU mirror - * (rescue-plan.ts rescueEnvelopes) is machine-checked by the seam-zero test — - * keep both in lockstep. - * - * `hopSlot`/`causeDepth` are template-substituted f32 literals (no uniform - * buffer → no strippable binding). Bind group = EXACTLY the 3 declared - * bindings (params, nodes, wave). - */ - -import type { RescueShaderConsts } from '../rescue-plan'; - -export function rescueWGSL(c: RescueShaderConsts): string { - const hopSlot = c.hopSlot.toFixed(1); - const causeDepth = c.causeDepth.toFixed(1); - return /* wgsl */ ` -struct Params { - frame: f32, - loop_phase: f32, - node_count: f32, - edge_count: f32, - path_count: f32, - pulse: f32, - viewport_w: f32, - viewport_h: f32, - brightness: f32, - demo_id: f32, - time: f32, - _pad: f32, -}; - -struct Node { - pos_radius: vec4, - vel_retention: vec4, - color_flags: vec4, - demo: vec4, -}; - -@group(0) @binding(0) var params: Params; -@group(0) @binding(1) var nodes: array; -// 1 u32/node: bits 0-15 hopDepth (0xffff unreached), 16 failure, 17 cause, -// 18 lookalike, 19-21 lookalike k (rescue-plan.ts packing). -@group(0) @binding(2) var wave: array; - -const HOP_SLOT: f32 = ${hopSlot}; -const CAUSE_DEPTH: f32 = ${causeDepth}; -const TAU: f32 = 6.28318530717958647; - -fn env(f: f32, a0: f32, a1: f32, r0: f32, r1: f32) -> f32 { - return smoothstep(a0, a1, f) * (1.0 - smoothstep(r0, r1, f)); -} - -fn arrival(d: f32) -> f32 { - return min(260.0 + HOP_SLOT * d, 514.0); -} - -@compute @workgroup_size(64) -fn rescue_choreo(@builtin(global_invocation_id) id: vec3) { - let i = id.x; - if (i >= u32(params.node_count)) { - return; - } - if (i >= arrayLength(&wave)) { - return; - } - // Belt-and-braces atop the TS gate: salience-rescue is demo index 2. - if (params.demo_id != 2.0) { - return; - } - - let packed = wave[i]; - let depth_u = packed & 0xffffu; - let d = f32(depth_u); - let is_failure = (packed & 0x10000u) != 0u; - let is_cause = (packed & 0x20000u) != 0u; - let is_look = (packed & 0x40000u) != 0u; - let look_k = f32((packed >> 19u) & 0x7u); - - let f = params.frame; - - var dx = 0.0; - var dy = 0.0; - var dz = 0.0; - var dw = 0.0; - - if (is_failure) { - // Detonation spike, wound simmer, recognition flare as the arc lands. - dw = dw + env(f, 90.0, 96.0, 120.0, 168.0); - dw = dw + 0.35 * env(f, 100.0, 130.0, 600.0, 656.0); - dw = dw + 0.35 * env(f, 552.0, 562.0, 580.0, 640.0); - // Symptom backlight while the cause burns. - dx = dx + 0.4 * env(f, 556.0, 566.0, 620.0, 668.0); - } - if (!is_failure && depth_u >= 1u && depth_u <= 12u) { - // Shockwave blink: crimson concussion, 3 frames/hop of REAL graph distance. - dw = dw + 0.75 * exp(-0.3 * d) - * env(f, 92.0 + 3.0 * d, 96.0 + 3.0 * d, 96.0 + 3.0 * d, 122.0 + 3.0 * d); - } - if (is_look) { - let fk = 138.0 + 28.0 * look_k; - // Searchlight flare — cold pop, sequential, on camera. - dy = dy + env(f, fk - 6.0, fk, fk + 10.0, fk + 26.0); - // Ash residue — the struck-through lookalike stays in frame until the verdict. - dy = dy + 0.15 * smoothstep(fk + 10.0, fk + 26.0, f) * (1.0 - smoothstep(600.0, 656.0, f)); - } - if (!is_failure && depth_u >= 1u && d <= CAUSE_DEPTH) { - let wd = arrival(d); - // Interrogation flicker: 24 integer sine cycles per loop, per-depth phase. - let flicker = 0.75 + 0.25 * sin(TAU * 24.0 * params.loop_phase + 1.7 * d); - dz = dz + env(f, wd - 10.0, wd, wd + 28.0, wd + 64.0) * flicker; - // Scanned ember. - dz = dz + 0.08 * smoothstep(wd + 28.0, wd + 64.0, f) * (1.0 - smoothstep(580.0, 640.0, f)); - } - if (is_cause) { - // Cause ignition rides the EXISTING recall response (render-nodes.wgsl): - // spectral() thin-film band + white-hot core + sprite swell at full intensity. - dx = dx + env(f, 520.0, 546.0, 640.0, 700.0); - } - - // WGSL forbids swizzle stores — reconstruct the FULL vec4; pos/vel/color - // lanes pass through untouched (the force sim owns them). - var node = nodes[i]; - node.demo = vec4(dx, dy, dz, dw); - nodes[i] = node; -} -`; -} diff --git a/apps/dashboard/src/lib/observatory/shaders/simulate.wgsl.ts b/apps/dashboard/src/lib/observatory/shaders/simulate.wgsl.ts deleted file mode 100644 index f4e08a3..0000000 --- a/apps/dashboard/src/lib/observatory/shaders/simulate.wgsl.ts +++ /dev/null @@ -1,163 +0,0 @@ -/** - * Cognitive Observatory — recall-path + force simulation compute pass (Increment 7). - * - * One invocation per node (workgroup 64, dispatch ceil(N/64) — the canonical - * compute-boids pattern, spec §1). Each node: - * - * 1. Scans the small PathStep buffer (≤ 8 beats) and computes its recall - * activation envelope for this loop frame (arrival / departure). - * 2. Computes deterministic GPU force settle: edge springs, O(N²) - * repulsion (≤ 500 nodes), gentle centering, damping, velocity cap, - * and position integration. - * - * Writes NodeState.demo.x (recall intensity). Deterministic: everything is a - * pure function of (frame, path buffer, node state) — no randomness, no wall - * clock. Center node (isCenter flag) never moves. - * - * PASS ORDER (salience-rescue): rescue_choreo (shaders/rescue.wgsl.ts) MUST - * encode AFTER this pass in the same encoder — it overwrites all four demo - * lanes so the arc-afterglow demo.x written here (decays bf+40..bf+200) never - * crosses the 719→0 loop seam. The route guarantees construction order: - * NodeRenderer first, RescueRenderer second. - */ -export const simulateWGSL = /* wgsl */ ` -struct Params { - frame: f32, - loop_phase: f32, - node_count: f32, - edge_count: f32, - path_count: f32, - pulse: f32, - viewport_w: f32, - viewport_h: f32, - brightness: f32, - demo_id: f32, - time: f32, - _pad: f32, -}; - -struct Node { - pos_radius: vec4, - vel_retention: vec4, - color_flags: vec4, - demo: vec4, -}; - -@group(0) @binding(0) var params: Params; -@group(0) @binding(1) var nodes: array; -// x source index, y target index, z beat frame, w kind (0 recall, 1 backward) -@group(0) @binding(2) var path: array>; -// x source index, y target node index (Increment 7: force simulation edges) -@group(0) @binding(3) var edges: array>; - -// --- Force-simulation helpers (Increment 7) --- - -fn safe_normalize(v: vec3) -> vec3 { - let l = length(v); - if (l < 0.0001) { return vec3(0.0); } - return v / l; -} - -fn clamp_len(v: vec3, hi: f32) -> vec3 { - let l = length(v); - if (l > hi && l > 0.0001) { return v * (hi / l); } - return v; -} - -@compute @workgroup_size(64) -fn recall_sim(@builtin(global_invocation_id) id: vec3) { - let i = id.x; - if (i >= u32(params.node_count)) { - return; - } - - let frame = params.frame; - var intensity = 0.0; - - let steps = u32(params.path_count); - for (var s = 0u; s < steps; s = s + 1u) { - let step = path[s]; - let bf = f32(step.z); - - if (step.y == i) { - // Arrival: sharp attack as the wavefront lands, slow afterglow. - let attack = smoothstep(bf - 14.0, bf + 4.0, frame); - let decay = 1.0 - smoothstep(bf + 40.0, bf + 200.0, frame); - intensity = max(intensity, attack * decay); - } - if (step.x == i && step.x != step.y) { - // Departure: the source shimmers as the wave leaves it. - let rise = smoothstep(bf - 55.0, bf - 30.0, frame); - let fall = 1.0 - smoothstep(bf + 10.0, bf + 70.0, frame); - intensity = max(intensity, rise * fall * 0.45); - } - } - - var node = nodes[i]; - let flags = u32(node.color_flags.w); - let is_center = (flags & 1u) != 0u; - - // Write recall intensity (existing behavior preserved). - node.demo.x = intensity; - - // --- Increment 7: force simulation --- - - // Capture mode (params._pad == 1.0): skip physics integration entirely. - // The storage-buffer state stays frozen at initial upload values, - // making same URL + frame → identical pixels (spec §4 Inc 9). - if (params._pad == 0.0) { - // 7B: center anchor — center node never moves. - // (WGSL forbids swizzle stores — reconstruct the vec4, preserving .w.) - if (is_center) { - node.pos_radius = vec4(0.0, 0.0, 0.0, node.pos_radius.w); - node.vel_retention = vec4(0.0, 0.0, 0.0, node.vel_retention.w); - nodes[i] = node; - return; - } - - let pos = node.pos_radius.xyz; - var force = vec3(0.0); - - // 7C: edge springs — scan existing edgeBuffer, no atomics. - for (var e = 0u; e < u32(params.edge_count); e = e + 1u) { - let edge = edges[e]; - var other_idx = 0xffffffffu; - if (edge.x == i) { other_idx = edge.y; } - if (edge.y == i) { other_idx = edge.x; } - if (other_idx != 0xffffffffu && other_idx < u32(params.node_count)) { - let other = nodes[other_idx].pos_radius.xyz; - let delta = other - pos; - let dist = max(length(delta), 0.001); - let dir = delta / dist; - let stretch = dist - 34.0; - force = force + dir * stretch * 0.00055; - } - } - - // 7D: soft repulsion (only ≤ 500 nodes for performance). - if (u32(params.node_count) <= 500u) { - for (var j = 0u; j < u32(params.node_count); j = j + 1u) { - if (j == i) { continue; } - let other = nodes[j].pos_radius.xyz; - let delta = pos - other; - let d2 = max(dot(delta, delta), 9.0); - force = force + safe_normalize(delta) * (7.5 / d2); - } - } - - // Gentle centering: keeps the field in frame without crushing it. - force = force + (-pos) * 0.0008; - - // 7B: velocity damping + cap, then position integration. - var vel = node.vel_retention.xyz; - vel = (vel + force) * 0.88; - vel = clamp_len(vel, 0.42); - node.vel_retention = vec4(vel, node.vel_retention.w); - node.pos_radius = vec4(pos + vel, node.pos_radius.w); - } - // When capture_mode (params._pad == 1.0), node is NOT written back — - // the storage buffer retains its initial upload values, guaranteeing - // deterministic pixels for the same frame index. - nodes[i] = node; -} -`; diff --git a/apps/dashboard/src/lib/observatory/types.ts b/apps/dashboard/src/lib/observatory/types.ts deleted file mode 100644 index b7cc119..0000000 --- a/apps/dashboard/src/lib/observatory/types.ts +++ /dev/null @@ -1,159 +0,0 @@ -/** - * Cognitive Observatory — shared CPU-side types + GPU buffer layout constants. - * - * The Observatory is a full-bleed WebGPU "living memory field". Node/particle - * state lives in GPU storage buffers (boids ping-pong pattern, spec §1); this - * module defines the exact byte layout so the TS uploader and the WGSL shaders - * agree lane-for-lane. - * - * Visual DNA (spec §7): a node's BASE hue = its FSRS state color (meaning), - * its ACTIVATION glow rides the thin-film spectral band (transcendence). - */ - -import type { GraphNode, GraphEdge } from '$types'; - -/** Demo modes reachable as deterministic loops: ?demo=&loop=1 */ -export type DemoMode = - | 'recall-path' - | 'engram-birth' - | 'salience-rescue' - | 'forgetting-horizon' - | 'firewall'; - -export const DEMO_MODES: readonly DemoMode[] = [ - 'recall-path', - 'engram-birth', - 'salience-rescue', - 'forgetting-horizon', - 'firewall' -] as const; - -export function isDemoMode(v: string): v is DemoMode { - return (DEMO_MODES as readonly string[]).includes(v); -} - -// --------------------------------------------------------------------------- -// GPU buffer layout — NodeState -// --------------------------------------------------------------------------- -// Each node occupies 4 × vec4 = 64 bytes, 16-byte aligned lanes so the -// WGSL struct maps 1:1 with no padding surprises. -// -// lane 0 pos_radius : xyz world position + w visual radius -// lane 1 vel_retention : xyz velocity + w FSRS retention (0..1) -// lane 2 color_flags : rgb base color + w packed flags (as f32) -// lane 3 demo : x recall intensity, y birth phase, -// z ripple phase, w shock phase -// -// FLOATS_PER_NODE is what the uploader writes; keep it in lockstep with the -// WGSL `struct NodeState`. -export const FLOATS_PER_NODE = 16; -export const BYTES_PER_NODE = FLOATS_PER_NODE * 4; // 64 - -// Lane offsets (in floats) for the uploader. -export const NODE_LANE = { - posRadius: 0, // +0..+3 - velRetention: 4, // +4..+7 - colorFlags: 8, // +8..+11 - demo: 12 // +12..+15 -} as const; - -// Packed visual flags (stored in color_flags.w as an f32 bit field via bitcast -// on the GPU; on the CPU side we assemble the integer then Math.fround it). -export const NODE_FLAG = { - isCenter: 1 << 0, - suppressed: 1 << 1, - isAha: 1 << 2, - isFailure: 1 << 3, - isConfusion: 1 << 4 -} as const; - -// --------------------------------------------------------------------------- -// GPU buffer layout — EdgeIndex (static) and PathStep (demo path) -// --------------------------------------------------------------------------- -/** array> source/target node indices. 2 u32 per edge. */ -export const UINTS_PER_EDGE = 2; - -/** - * array> per path beat: - * x source node index, y target node index, z beat frame, w kind - * kind: 0 normal recall hop, 1 backward-cause hop (salience rescue), - * 2 probe beam (salience rescue: vector search failing on camera). - */ -export const UINTS_PER_PATHSTEP = 4; - -export const PATH_KIND = { - recall: 0, - backwardCause: 1, - probe: 2 -} as const; - -// --------------------------------------------------------------------------- -// Per-frame uniforms — must match the WGSL `struct Params` exactly. -// --------------------------------------------------------------------------- -// Laid out as a flat Float32Array; sizes chosen so the whole block is a -// multiple of 16 bytes (WebGPU uniform alignment requirement). -// -// [0] frame (loop frame, wraps at loopFrames) -// [1] loopPhase (0..1) -// [2] nodeCount -// [3] edgeCount -// [4] pathCount -// [5] pulse (0.5 + 0.5*sin — global breath, spec §7.2) -// [6] viewportW -// [7] viewportH -// [8] brightness (from graphState) -// [9] demoId (DemoMode index) -// [10] time (fixed sim seconds = frame / fps; NOT wall clock) -// [11] _pad -export const PARAMS_FLOATS = 12; -export const PARAMS_BYTES = PARAMS_FLOATS * 4; // 48 (multiple of 16) - -export function demoModeId(mode: DemoMode): number { - const i = DEMO_MODES.indexOf(mode); - return i < 0 ? 0 : i; -} - -// --------------------------------------------------------------------------- -// CPU-side observatory graph (stable-indexed view of the API GraphResponse). -// --------------------------------------------------------------------------- -export interface ObservatoryNode { - id: string; - index: number; // stable position in the NodeState buffer - label: string; - type: string; - retention: number; - tags: string[]; - isCenter: boolean; - suppressed: boolean; -} - -export interface ObservatoryEdge { - sourceIndex: number; - targetIndex: number; - weight: number; - type: string; -} - -export interface ObservatoryGraph { - nodes: ObservatoryNode[]; - edges: ObservatoryEdge[]; - /** id -> stable buffer index */ - indexById: Map; - centerIndex: number; -} - -/** Narrow the loose API GraphNode into what the Observatory needs. */ -export function toObservatoryNode(n: GraphNode, index: number): ObservatoryNode { - return { - id: n.id, - index, - label: n.label, - type: n.type, - retention: typeof n.retention === 'number' ? n.retention : 0, - tags: Array.isArray(n.tags) ? n.tags : [], - isCenter: !!n.isCenter, - suppressed: (n.suppression_count ?? 0) > 0 - }; -} - -export type { GraphNode, GraphEdge }; diff --git a/apps/dashboard/src/lib/stores/api.ts b/apps/dashboard/src/lib/stores/api.ts index dd5f095..f4b77e0 100644 --- a/apps/dashboard/src/lib/stores/api.ts +++ b/apps/dashboard/src/lib/stores/api.ts @@ -6,21 +6,13 @@ import type { HealthCheck, TimelineResponse, GraphResponse, - DuplicatesResponse, - ContradictionsResponse, - CrossProjectPatternsResponse, - MemoryAuditResponse, DreamResult, ImportanceScore, RetentionDistribution, ConsolidationResult, IntentionItem, SuppressResult, - UnsuppressResult, - SanhedrinAppealReason, - SanhedrinAppealResponse, - SanhedrinLatestResponse, - SanhedrinTelemetryResponse + UnsuppressResult } from '$types'; const BASE = '/api'; @@ -115,28 +107,6 @@ export const api = { retentionDistribution: () => fetcher('/retention-distribution'), - // Memory hygiene & provenance: duplicate clusters, contradiction pairs, - // cross-project pattern transfer, per-memory audit trail. - duplicates: (threshold = 0.8, limit = 20) => - fetcher(`/duplicates?threshold=${threshold}&limit=${limit}`), - - contradictions: (params?: { topic?: string; min_trust?: number; limit?: number }) => { - const qs = params - ? '?' + - new URLSearchParams( - Object.entries(params) - .filter(([, v]) => v !== undefined) - .map(([k, v]) => [k, String(v)]) - ).toString() - : ''; - return fetcher(`/contradictions${qs}`); - }, - - crossProjectPatterns: () => fetcher('/patterns/cross-project'), - - memoryAudit: (id: string, limit = 100) => - fetcher(`/memories/${encodeURIComponent(id)}/audit?limit=${limit}`), - // Intentions intentions: (status = 'active') => fetcher<{ intentions: IntentionItem[]; total: number; filter: string }>(`/intentions?status=${status}`), @@ -149,132 +119,5 @@ export const api = { fetcher>('/deep_reference', { method: 'POST', body: JSON.stringify({ query, depth }) - }), - - sanhedrin: { - latest: () => fetcher('/sanhedrin/latest'), - telemetry: (days = 7) => fetcher(`/sanhedrin/telemetry?days=${days}`), - appeal: (reason: SanhedrinAppealReason, note?: string, claimId?: string, receiptId?: string) => - fetcher('/sanhedrin/appeal', { - method: 'POST', - body: JSON.stringify({ reason, note, claimId, receiptId }) - }) - }, - - // Agent Black Box (v2.2): replayable agent-run traces. The runId in a tool - // result threads through here unchanged — one id, end to end. - traces: { - list: (limit = 50) => fetcher(`/traces?limit=${limit}`), - get: (runId: string) => fetcher(`/traces/${encodeURIComponent(runId)}`), - exportUrl: (runId: string) => `${BASE}/traces/${encodeURIComponent(runId)}/export` - }, - - // Memory Receipts (v2.2): the nutrition label for a retrieval. - receipts: { - list: (limit = 50) => fetcher(`/receipts?limit=${limit}`), - // B5: scope to one run so the Black Box panel shows that run's receipts. - listForRun: (runId: string, limit = 50) => - fetcher( - `/receipts?run=${encodeURIComponent(runId)}&limit=${limit}` - ), - get: (receiptId: string) => fetcher(`/receipts/${encodeURIComponent(receiptId)}`) - }, - - // Memory PRs (v2.2): the risk-gated brain-change review queue. - memoryPrs: { - list: (status?: string, limit = 100) => { - const qs = new URLSearchParams(); - if (status) qs.set('status', status); - qs.set('limit', String(limit)); - return fetcher(`/memory-prs?${qs.toString()}`); - }, - get: (id: string) => fetcher(`/memory-prs/${encodeURIComponent(id)}`), - act: (id: string, action: MemoryPrAction) => - fetcher>(`/memory-prs/${encodeURIComponent(id)}/${action}`, { - method: 'POST' - }), - getMode: () => fetcher<{ mode: ReviewMode; pendingCount: number }>('/memory-prs/mode'), - setMode: (mode: ReviewMode) => - fetcher<{ mode: ReviewMode }>('/memory-prs/mode', { - method: 'POST', - body: JSON.stringify({ mode }) - }) - } -}; - -// --------------------------------------------------------------------------- -// Agent Black Box / Receipts / Memory PR types -// --------------------------------------------------------------------------- - -export type TraceRunSummary = { - runId: string; - firstTool: string | null; - eventCount: number; - retrievedCount: number; - suppressedCount: number; - writeCount: number; - vetoCount: number; - startedAt: number; - lastAt: number; -}; - -export type TraceRunListResponse = { total: number; runs: TraceRunSummary[] }; - -/** One trace event — discriminated on `type`, matching the Rust schema. */ -export type TraceEvent = - | { type: 'mcp.call'; runId: string; tool: string; argsHash: string; at: number } - | { type: 'memory.retrieve'; runId: string; ids: string[]; activation: Record; at: number } - | { type: 'memory.suppress'; runId: string; id: string; reason: string; at: number } - | { type: 'memory.write'; runId: string; id: string; diff: unknown; source: string; at: number } - | { type: 'contradiction.detected'; runId: string; ids: string[]; winnerId?: string; detail: string; at: number } - | { type: 'sanhedrin.veto'; runId: string; claim: string; evidenceIds: string[]; confidence: number; at: number } - | { type: 'dream.patch'; runId: string; proposalIds: string[]; at: number }; - -export type TraceDetail = { - runId: string; - summary: Omit | null; - events: TraceEvent[]; -}; - -export type Receipt = { - receipt_id: string; - retrieved: string[]; - suppressed: { id: string; reason: string }[]; - activation_path: string[]; - trust_floor: number; - decay_risk: 'low' | 'medium' | 'high'; - mutations: { id: string; kind: string; note?: string }[]; -}; - -export type ReceiptListResponse = { total: number; receipts: Receipt[] }; - -export type MemoryPrAction = - | 'promote' - | 'merge' - | 'supersede' - | 'quarantine' - | 'forget' - | 'ask_agent_why'; - -export type ReviewMode = 'fast' | 'risk_gated' | 'paranoid'; - -export type MemoryPr = { - id: string; - kind: string; - status: string; - title: string; - diff: Record; - signals: { code: string; detail: string }[]; - subject_id?: string; - run_id?: string; - created_at: string; - decided_at?: string; - decision?: string; -}; - -export type MemoryPrListResponse = { - total: number; - pendingCount: number; - mode: ReviewMode; - prs: MemoryPr[]; + }) }; diff --git a/apps/dashboard/src/lib/stores/toast.ts b/apps/dashboard/src/lib/stores/toast.ts index 6bc195c..6daef38 100644 --- a/apps/dashboard/src/lib/stores/toast.ts +++ b/apps/dashboard/src/lib/stores/toast.ts @@ -61,13 +61,6 @@ function createToastStore() { update(list => { const next = [entry, ...list]; if (next.length > MAX_VISIBLE) { - for (const dropped of next.slice(MAX_VISIBLE)) { - const handle = dwellTimers.get(dropped.id); - if (handle) clearTimeout(handle); - dwellTimers.delete(dropped.id); - dwellPaused.delete(dropped.id); - dwellStart.delete(dropped.id); - } return next.slice(0, MAX_VISIBLE); } return next; @@ -236,18 +229,6 @@ function createToastStore() { }; } - case 'HookVerdictRecorded': { - const verdict = String(d.verdict ?? 'NOTE'); - const reason = String(d.reason ?? 'Sanhedrin receipt updated'); - return { - type: event.type, - title: `Sanhedrin ${verdict}`, - body: reason, - color, - dwellMs: verdict === 'VETO' ? 8000 : DEFAULT_DWELL_MS, - }; - } - // Noise — never toast case 'Heartbeat': case 'SearchPerformed': diff --git a/apps/dashboard/src/lib/stores/websocket.ts b/apps/dashboard/src/lib/stores/websocket.ts index 578c43b..12f0a5d 100644 --- a/apps/dashboard/src/lib/stores/websocket.ts +++ b/apps/dashboard/src/lib/stores/websocket.ts @@ -6,13 +6,11 @@ const MAX_EVENTS = 200; function createWebSocketStore() { const { subscribe, set, update } = writable<{ connected: boolean; - reconnecting: boolean; events: VestigeEvent[]; lastHeartbeat: VestigeEvent | null; error: string | null; }>({ connected: false, - reconnecting: false, events: [], lastHeartbeat: null, error: null @@ -34,7 +32,7 @@ function createWebSocketStore() { ws.onopen = () => { reconnectAttempts = 0; - update(s => ({ ...s, connected: true, reconnecting: false, error: null })); + update(s => ({ ...s, connected: true, error: null })); }; ws.onmessage = (event) => { @@ -67,28 +65,16 @@ function createWebSocketStore() { function scheduleReconnect(url: string) { if (reconnectTimer) clearTimeout(reconnectTimer); - update(s => ({ ...s, reconnecting: true })); const delay = Math.min(1000 * 2 ** reconnectAttempts, 30000); reconnectAttempts++; reconnectTimer = setTimeout(() => connect(url), delay); } function disconnect() { - if (reconnectTimer) { - clearTimeout(reconnectTimer); - reconnectTimer = null; - } - if (ws) { - // Detach onclose BEFORE closing: otherwise close() fires the handler, - // which calls scheduleReconnect and resurrects the socket we just - // asked to tear down. - ws.onclose = null; - ws.onerror = null; - ws.onmessage = null; - ws.close(); - } + if (reconnectTimer) clearTimeout(reconnectTimer); + ws?.close(); ws = null; - set({ connected: false, reconnecting: false, events: [], lastHeartbeat: null, error: null }); + set({ connected: false, events: [], lastHeartbeat: null, error: null }); } function clearEvents() { @@ -98,7 +84,7 @@ function createWebSocketStore() { /** * Inject a synthetic event into the feed as if it had arrived over the * WebSocket. Used by the dev-mode "Preview Birth Ritual" button on the - * Settings page to let developers trigger a demo of the v2.3 Memory Birth + * Settings page to let Sam trigger a demo of the v2.3 Memory Birth * Ritual without ingesting a real memory. Downstream consumers — * InsightToast, Graph3D — cannot distinguish synthetic from real. */ @@ -122,7 +108,6 @@ export const websocket = createWebSocketStore(); // Derived stores for specific event types export const isConnected = derived(websocket, $ws => $ws.connected); -export const isReconnecting = derived(websocket, $ws => $ws.reconnecting); export const eventFeed = derived(websocket, $ws => $ws.events); export const heartbeat = derived(websocket, $ws => $ws.lastHeartbeat); export const memoryCount = derived(websocket, $ws => @@ -143,30 +128,6 @@ export const uptimeSeconds = derived(websocket, $ws => ($ws.lastHeartbeat?.data?.uptime_secs as number) ?? 0 ); -// Agent Black Box (v2.2): the live stream of trace events, newest first. Each -// is a real `VestigeEvent::TraceEvent` backed by a persisted `agent_traces` -// row — the dashboard pulse is only ever driven by these, never by fakes. -export const traceEvents = derived(websocket, $ws => - $ws.events.filter((e) => e.type === 'TraceEvent') -); - -// The most recent runId seen on the live feed — the "current run" indicator in -// Proof Mode / the Black Box live header. -export const liveRunId = derived(websocket, $ws => { - const latest = $ws.events.find((e) => e.type === 'TraceEvent'); - return (latest?.data?.run_id as string) ?? null; -}); - -// The single most recent trace event (for the "last event" readout). -export const lastTraceEvent = derived(websocket, $ws => - $ws.events.find((e) => e.type === 'TraceEvent') ?? null -); - -// Live Memory PR notifications (opened / decided) for the queue badge + toasts. -export const memoryPrEvents = derived(websocket, $ws => - $ws.events.filter((e) => e.type === 'MemoryPrOpened' || e.type === 'MemoryPrDecided') -); - export function formatUptime(secs: number): string { if (!Number.isFinite(secs) || secs < 0) return '—'; const d = Math.floor(secs / 86_400); diff --git a/apps/dashboard/src/lib/types/index.ts b/apps/dashboard/src/lib/types/index.ts index 1673501..a76b092 100644 --- a/apps/dashboard/src/lib/types/index.ts +++ b/apps/dashboard/src/lib/types/index.ts @@ -167,10 +167,6 @@ export type VestigeEventType = | 'ActivationSpread' | 'ImportanceScored' | 'DeepReferenceCompleted' - | 'HookVerdictRecorded' - | 'TraceEvent' - | 'MemoryPrOpened' - | 'MemoryPrDecided' | 'Heartbeat'; export interface VestigeEvent { @@ -206,89 +202,6 @@ export interface UnsuppressResult { stability: number; } -export type VerdictLevel = 'PASS' | 'NOTE' | 'CAUTION' | 'VETO' | 'APPEALED'; -export type SanhedrinAppealReason = 'stale' | 'wrong' | 'too_strict'; - -export interface SanhedrinAppealState { - status: 'open' | 'appealed'; - actions?: SanhedrinAppealReason[]; - lastReason?: SanhedrinAppealReason; - note?: string; -} - -export interface SanhedrinPrecedent { - type?: string; - summary?: string; - command?: string; - exitCode?: number | null; - evidence?: string; -} - -export interface SanhedrinClaim { - id: string; - text: string; - fingerprint: string; - class: string; - subject: string; - risk: string; - evidence_state: string; - decision: string; - precedent: SanhedrinPrecedent[]; - fix: string; - appeal: SanhedrinAppealState; -} - -export interface SanhedrinReceipt { - schema: string; - id: string; - draftId: string; - createdAt: string; - overall: string; - verdictBar: VerdictLevel; - summary: string; - draftPreview: string; - claims: SanhedrinClaim[]; - receipts: Array>; - source?: Record; -} - -export interface SanhedrinLatestResponse { - receipt: SanhedrinReceipt | null; - stateDir: string; - receiptPath?: string; - htmlPath?: string; - schemaWarning?: string | null; -} - -export interface SanhedrinAppealResponse { - appeal: Record; - receipt: SanhedrinReceipt; -} - -export interface SanhedrinDailyTelemetry { - date: string; - total: number; - pass: number; - note: number; - caution: number; - veto: number; - appealed: number; - failOpen: number; -} - -export interface SanhedrinTelemetryResponse { - days: number; - stateDir: string; - totalRuns: number; - byVerdict: Partial>; - byClass: Record; - appeals: number; - failOpen: number; - truncated?: boolean; - lastRunAt?: string | null; - daily: SanhedrinDailyTelemetry[]; -} - // Intentions (prospective memory) export interface IntentionItem { id: string; @@ -302,107 +215,6 @@ export interface IntentionItem { snoozed_until?: string | null; } -// Memory Hygiene — GET /api/duplicates (cosine-similarity clusters) -export interface DuplicateClusterMemory { - id: string; - content: string; - nodeType: string; - tags: string[]; - retention: number; - createdAt: string; -} - -export interface DuplicateClusterGroup { - similarity: number; - suggestedAction: 'merge' | 'review'; - memories: DuplicateClusterMemory[]; -} - -export interface DuplicatesResponse { - threshold: number; - total: number; - clusters: DuplicateClusterGroup[]; -} - -// Contradiction pairs — GET /api/contradictions. Field names mirror the -// Contradiction interface in $components/ContradictionArcs.svelte; a = older -// memory, b = newer. Sorted by similarity desc. -export interface ContradictionPair { - memory_a_id: string; - memory_b_id: string; - memory_a_preview: string; - memory_b_preview: string; - memory_a_type?: string; - memory_b_type?: string; - memory_a_created?: string; - memory_b_created?: string; - memory_a_tags?: string[]; - memory_b_tags?: string[]; - trust_a: number; - trust_b: number; - similarity: number; - date_diff_days: number; - topic: string; -} - -export interface ContradictionsResponse { - memoriesAnalyzed: number; - total: number; - contradictions: ContradictionPair[]; -} - -// Cross-project pattern transfer — GET /api/patterns/cross-project. -// Only the six tracked categories ever cross the wire (backend drops others). -export type CrossProjectCategory = - | 'ErrorHandling' - | 'AsyncConcurrency' - | 'Testing' - | 'Architecture' - | 'Performance' - | 'Security'; - -export interface CrossProjectPattern { - name: string; - category: CrossProjectCategory; - origin_project: string; - transferred_to: string[]; - transfer_count: number; - last_used: string; - confidence: number; -} - -export interface CrossProjectPatternsResponse { - projects: string[]; - patterns: CrossProjectPattern[]; -} - -// Per-memory audit trail — GET /api/memories/{id}/audit. Events arrive -// newest-first; the action union matches AuditAction in -// $components/audit-trail-helpers.ts. -export type MemoryAuditAction = - | 'created' - | 'accessed' - | 'promoted' - | 'demoted' - | 'edited' - | 'suppressed' - | 'dreamed' - | 'reconsolidated'; - -export interface MemoryAuditEvent { - action: MemoryAuditAction; - timestamp: string; // RFC3339 - old_value?: number; - new_value?: number; - reason?: string; - triggered_by?: string; -} - -export interface MemoryAuditResponse { - memoryId: string; - events: MemoryAuditEvent[]; -} - // Node type colors for visualization — bioluminescent palette export const NODE_TYPE_COLORS: Record = { fact: '#00A8FF', // electric blue @@ -426,7 +238,6 @@ export const EVENT_TYPE_COLORS: Record = { Rac1CascadeSwept: '#6E3FFF', SearchPerformed: '#818CF8', DeepReferenceCompleted: '#C4B5FD', - HookVerdictRecorded: '#F59E0B', DreamStarted: '#9D00FF', DreamProgress: '#B44AFF', DreamCompleted: '#C084FC', diff --git a/apps/dashboard/src/routes/(app)/activation/+page.svelte b/apps/dashboard/src/routes/(app)/activation/+page.svelte index ed4bcc0..2433e43 100644 --- a/apps/dashboard/src/routes/(app)/activation/+page.svelte +++ b/apps/dashboard/src/routes/(app)/activation/+page.svelte @@ -24,11 +24,6 @@ } from '$components/ActivationNetwork.svelte'; import { filterNewSpreadEvents } from '$components/activation-helpers'; import type { Memory, VestigeEvent } from '$types'; - import PageHeader from '$lib/components/PageHeader.svelte'; - import Icon from '$lib/components/Icon.svelte'; - import AnimatedNumber from '$lib/components/AnimatedNumber.svelte'; - import { reveal } from '$lib/actions/reveal'; - import { spotlight, magnetic } from '$lib/actions/interactions'; let searchQuery = $state(''); let loading = $state(false); @@ -185,30 +180,19 @@ }); -
                    - -
                    - - {liveEnabled ? 'Live' : 'Paused'} - · - - bursts -
                    -
                    +
                    +
                    +

                    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
                    @@ -254,8 +236,8 @@ {#if loading}
                    -
                    -

                    Computing activation…

                    +
                    +

                    Computing activation...

                    {:else if errorMessage} @@ -268,8 +250,8 @@
                    {:else if !focusedSource && searched}
                    -
                    -
                    +
                    +

                    No matching memory

                    Nothing in the graph matches @@ -281,8 +263,8 @@

                    {:else if !focusedSource}
                    -
                    -
                    +
                    +

                    Waiting for activation

                    Seed a burst with the search bar above, or enable live mode to diff --git a/apps/dashboard/src/routes/(app)/blackbox/+page.svelte b/apps/dashboard/src/routes/(app)/blackbox/+page.svelte deleted file mode 100644 index 3187bf8..0000000 --- a/apps/dashboard/src/routes/(app)/blackbox/+page.svelte +++ /dev/null @@ -1,954 +0,0 @@ - - -

                    - - - - - - -
                    -
                    - WebSocket - - - {$isConnected ? 'Connected' : 'Offline'} - -
                    -
                    - Live runId - {$liveRunId ?? '—'} -
                    -
                    - Last event - - {#if $lastTraceEvent} - - {eventLabel(($lastTraceEvent.data?.event as TraceEvent)?.type)} - - {:else} - awaiting… - {/if} - -
                    -
                    - Events seen - - - -
                    -
                    - - {#if !proofMode} -
                    - - - - -
                    - {#if loading} -
                    Loading trace…
                    - {:else if error} -
                    {error}
                    - {:else if !detail} -
                    Select a run to replay.
                    - {:else} - -
                    -
                    - - Step {scrubIndex + 1} / {detail.events.length} - - {#if currentEvent} - +{relativeMs(currentEvent.at, startAt)}ms - {/if} -
                    - - -
                    - {#each detail.events as ev, i (i)} - - {/each} -
                    -
                    - - - {#if currentEvent} -
                    -
                    - {eventGlyph(currentEvent.type)} - {eventLabel(currentEvent.type)} - {formatAt(currentEvent.at)} -
                    -

                    {eventSummary(currentEvent)}

                    - - {#if currentEvent.type === 'memory.retrieve'} -
                    - {#each currentEvent.ids as id (id)} - - {id.slice(0, 8)} - {#if currentEvent.activation[id] != null} - {(currentEvent.activation[id] * 100).toFixed(0)}% - {/if} - - {/each} -
                    - {:else if currentEvent.type === 'contradiction.detected'} -
                    - kept {currentEvent.winnerId?.slice(0, 8)} - vs - {#each currentEvent.ids.filter((i) => i !== currentEvent.winnerId) as id (id)} - {id.slice(0, 8)} - {/each} -
                    - {:else if currentEvent.type === 'sanhedrin.veto'} -
                    - {#each currentEvent.evidenceIds as id (id)} - {id.slice(0, 8)} - {/each} -
                    - {/if} -
                    - {/if} - - -
                    -

                    - Memory pulse — touched this run -

                    - {#if pulsedIds.length === 0} -

                    No memories touched yet.

                    - {:else} -
                    - {#each pulsedIds as id (id)} - {id.slice(0, 8)} - {/each} -
                    - {/if} -
                    - - -
                    -

                    Event producers — this run

                    -
                      -
                    • - mcp.call · memory.write · memory.retrieve · memory.suppress - live -
                    • -
                    • - contradiction.detected - - {hasContradiction ? 'fired this run' : 'no contradiction in this run'} - -
                    • -
                    • - dream.patch - - {hasDream ? 'fired this run' : 'No dream run in this trace'} - -
                    • -
                    • - sanhedrin.veto - - {hasVeto ? 'fired this run' : 'No veto producer connected (optional Sanhedrin hook, off by default)'} - -
                    • -
                    -
                    - - - {#if receipts.length} -
                    -

                    - Receipts — proof behind retrievals -

                    -
                    - {#each receipts.slice(0, 2) as r (r.receipt_id)} - - {/each} -
                    -
                    - {/if} - - -
                    -

                    Event log

                    -
                      - {#each detail.events as ev, i (i)} -
                    1. scrubIndex} - style:--c={eventColor(ev.type)} - > - -
                    2. - {/each} -
                    -
                    - {/if} -
                    -
                    - {:else} - -
                    -
                    - - {$liveRunId ?? 'awaiting run…'} -
                    - {#if $lastTraceEvent} - {@const ev = $lastTraceEvent.data?.event as TraceEvent} -
                    - {eventGlyph(ev?.type)} -
                    -
                    {eventLabel(ev?.type)}
                    -
                    {eventSummary(ev)}
                    -
                    -
                    - {/if} -
                    - - trace events -
                    -

                    Watch the agent think. Watch memory change. Watch the receipt prove why.

                    -
                    - {/if} -
                    - - diff --git a/apps/dashboard/src/routes/(app)/contradictions/+page.svelte b/apps/dashboard/src/routes/(app)/contradictions/+page.svelte index 5373045..df7aa18 100644 --- a/apps/dashboard/src/routes/(app)/contradictions/+page.svelte +++ b/apps/dashboard/src/routes/(app)/contradictions/+page.svelte @@ -1,12 +1,5 @@
                    - +

                    Explore Connections

                    {#each (['associations', 'chains', 'bridges'] as const) as m} @@ -154,51 +144,29 @@ {#if sourceMemory} {#if loading} -
                    -
                    - - Exploring {mode}… -
                    -
                    - {#each Array(4) as _, i} -
                    -
                    -
                    -
                    -
                    -
                    -
                    - {/each} -
                    +
                    +
                    +

                    Exploring {mode}...

                    {:else if associations.length > 0}
                    -

                    - - Connections Found -

                    +

                    {associations.length} Connections Found

                    - {#each associations as assoc, i (i)} -
                    -
                    -
                    - {i + 1} -
                    -
                    -

                    {assoc.content}

                    -
                    - {#if assoc.nodeType}{assoc.nodeType}{/if} - {#if assoc.score}Score: {Number(assoc.score).toFixed(3)}{/if} - {#if assoc.similarity}Similarity: {Number(assoc.similarity).toFixed(3)}{/if} - {#if assoc.retention}{(Number(assoc.retention) * 100).toFixed(0)}% retention{/if} - {#if assoc.connectionType}{assoc.connectionType}{/if} -
                    + {#each associations as assoc, i} +
                    +
                    + {i + 1} +
                    +
                    +

                    {assoc.content}

                    +
                    + {#if assoc.nodeType}{assoc.nodeType}{/if} + {#if assoc.score}Score: {Number(assoc.score).toFixed(3)}{/if} + {#if assoc.similarity}Similarity: {Number(assoc.similarity).toFixed(3)}{/if} + {#if assoc.retention}{(Number(assoc.retention) * 100).toFixed(0)}% retention{/if} + {#if assoc.connectionType}{assoc.connectionType}{/if}
                    @@ -206,26 +174,16 @@
                    {:else} -
                    - -

                    No connections surfaced yet

                    -

                    - {#if mode === 'associations'} - This memory hasn't formed strong links here. Try a broader source query — the graph rewards more general seeds. - {:else} - No {mode} found between these two memories. Pick a different source or target and the path may light up. - {/if} -

                    +
                    +
                    +

                    No connections found for this query.

                    {/if} {/if}
                    -

                    - - Importance Scorer -

                    +

                    Importance Scorer

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

                    - - - - - - - {#if submitMessage} -

                    - {submitMessage} -

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

                    Why Pro exists

                    -

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

                    -
                    -
                    - {#each proTracks as track} -
                    -
                    -

                    {track.name}

                    -

                    {track.copy}

                    -
                    - {/each} -
                    -
                    - -
                    -
                    -

                    Always-on answers

                    -

                    The support bot handles the first wave.

                    -

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

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

                    {line}

                    - {/each} -
                    - {/each} - {#if botBusy} -
                    -

                    Checking the onboarding notes...

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

                    May to June

                    -

                    The plan is simple.

                    -
                    -
                      -
                    1. - May - Get Vestige into every MCP, Claude Code, Cursor, local AI, Rust, and self-hosted channel that cares about agent memory. -
                    2. -
                    3. - June - Invite the first Solo Pro and Team Pro users into sync, backups, shared memory, PostgreSQL-backed deployments, and bot-assisted support. -
                    4. -
                    5. - After - Use paid feedback to turn Vestige from a beloved local tool into durable agent-memory infrastructure. -
                    6. -
                    -
                    - -
                    - - diff --git a/apps/dashboard/svelte.config.js b/apps/dashboard/svelte.config.js index 7b4101d..4298fa7 100644 --- a/apps/dashboard/svelte.config.js +++ b/apps/dashboard/svelte.config.js @@ -1,13 +1,6 @@ import adapter from '@sveltejs/adapter-static'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; -const appVersion = process.env.VESTIGE_DASHBOARD_VERSION ?? process.env.npm_package_version ?? 'dev'; - -// Base path the app is served from. Defaults to '/dashboard' for local dev and -// the embedded release binary. CI overrides it (e.g. '/vestige') so assets -// resolve correctly when published to a GitHub Pages project subpath. -const basePath = process.env.VESTIGE_BASE_PATH ?? '/dashboard'; - /** @type {import('@sveltejs/kit').Config} */ const config = { preprocess: vitePreprocess(), @@ -20,10 +13,7 @@ const config = { strict: false }), paths: { - base: basePath - }, - version: { - name: appVersion + base: '/dashboard' }, alias: { $lib: 'src/lib', diff --git a/apps/dashboard/vite.config.ts b/apps/dashboard/vite.config.ts index c85be98..363f4be 100644 --- a/apps/dashboard/vite.config.ts +++ b/apps/dashboard/vite.config.ts @@ -9,14 +9,11 @@ export default defineConfig({ port: 5173, proxy: { '/api': { - target: process.env.VESTIGE_API_TARGET ?? 'http://127.0.0.1:3927', + target: 'http://127.0.0.1:3927', changeOrigin: true }, '/ws': { - target: (process.env.VESTIGE_API_TARGET ?? 'http://127.0.0.1:3927').replace( - 'http', - 'ws' - ), + target: 'ws://127.0.0.1:3927', ws: true } } diff --git a/assets/causebench-recall-at-1.svg b/assets/causebench-recall-at-1.svg deleted file mode 100644 index bc55be9..0000000 --- a/assets/causebench-recall-at-1.svg +++ /dev/null @@ -1,46 +0,0 @@ - - CauseBench root-cause recall@1 — Vestige 60% / 50%, every resemblance retriever 0% - - Root-cause recall@1 — can the retriever rank the true cause #1? - CauseBench · a cause that shares an entity with the failure but none of its words - - - - - - - 0% - 20% - 40% - 60% - - Vestige - - - 60% synthetic - 50% real - - Dense vector (nomic) - - 0% - - Hybrid-RRF (BM25 + dense) - - 0% - - BM25 - - 0% - - TF-IDF - - 0% - - Recency - - 0% - - - Every resemblance retriever ranks the true cause #1 exactly 0% of the time. - Vestige also lands the cause in the top 5 in 100% of cases. Source: benchmarks/causebench/RESULTS.md · reproduce with 3 commands. - diff --git a/assets/vestige-icon.png b/assets/vestige-icon.png deleted file mode 100644 index 2f7deaa..0000000 Binary files a/assets/vestige-icon.png and /dev/null differ diff --git a/blackbox-proof-2026-06-22/PROOF.md b/blackbox-proof-2026-06-22/PROOF.md deleted file mode 100644 index 7d37994..0000000 --- a/blackbox-proof-2026-06-22/PROOF.md +++ /dev/null @@ -1,87 +0,0 @@ -# Vestige Agent Black Box — Proof Pack (2026-06-22) - -> **Public claim (frozen):** Vestige records real MCP memory activity into a -> replayable local trace, with receipts and reviewable risky writes. -> -> We do **not** claim Sanhedrin vetoes or dream patches are live by default. -> Those producers are optional and off by default — the UI says so explicitly. - -This pack is captured from a **live** Vestige build on branch -`feat/agent-black-box` — a real `vestige-mcp` process with the dashboard -enabled, driven by real MCP `tools/call` traffic. Nothing here is mocked. - -## The receipt chain — one runId, every hop - -The money guarantee: a single `runId` (`run_proof`) crosses every layer, -byte-identical. Verified two ways — by the files in this folder, and by the -deterministic regression test `test_full_spine_one_runid_crosses_every_hop` -(crates/vestige-mcp/src/server.rs). - -| Hop | Layer | Evidence in this pack | -|----|-------|------| -| 1 | MCP tool output (`runId` + `traceUri`) | every tool result; see test HOP 1 | -| 2 | SQLite `agent_traces` rows | `trace.json` (`runId: run_proof`, 10 events) | -| 3 | WebSocket broadcast | `websocket-events.jsonl` (6 `TraceEvent` lines, each with `run_id`) | -| 4 | `/api/traces/:runId` response | `trace.json` is the export of that endpoint | -| 5 | dashboard render | screenshots (Black Box timeline = the 10 events) | -| 6 | `vestige://trace/{runId}` MCP resource | test HOP 5 resolves the same id | - -## Files - -| File | What it proves | -|------|----------------| -| `status.json` | the live server health at capture time | -| `trace.json` | the full `.vestige-trace.json` export — 10 real events in order | -| `receipt.json` | a real retrieval receipt (`r_2026_06_22_runproof`, 5 retrieved, decay medium) | -| `memory_pr.json` | the risky auth write → Memory PR, **promoted** through UI→API→SQLite, signal `sensitive_topic` | -| `websocket-events.jsonl` | the live WS stream: `TraceEvent`×6, `MemoryPrOpened`, `MemoryPrDecided`, `MemoryCreated`, `MemoryUpdated` | -| `screenshots/` | Graph, Black Box, Receipts (in PR), Memory PRs — see `screenshots/README.md` | - -## Per-feature honesty: real / caveat / stub - -| Feature | Status | Notes | -|---------|--------|-------| -| `mcp.call` trace | **REAL** | every tools/call records one; args **hashed**, never stored raw | -| `memory.write` trace | **REAL** | fires on smart_ingest/ingest, memory promote/demote/edit, codebase remember_*, AND destructive purge/delete | -| `memory.retrieve` trace | **REAL** | fires on deep_reference/search, with per-id activation | -| `memory.suppress` trace | **REAL** | recorded path; fires when retrieval suppresses | -| `contradiction.detected` trace | **REAL** | fires when deep_reference surfaces a contradiction pair; UI says "no contradiction in this run" when none | -| Memory Receipts | **REAL** | built from real scored memories + trust, persisted, attached to output | -| Risk-gated Memory PRs | **REAL** | quarantine review: commit-then-suppress, audit preserved, influence suspended. Promote verified end-to-end (releases the memory, even past the 24h window). Destructive purge/delete also open a PR. PR content is **redacted** for sensitive writes (preview + hash, never the raw secret) | -| Fast / Risk-Gated / Paranoid modes | **REAL** | persisted to `/review_mode.json`; Risk-Gated is the default | -| WebSocket broadcast | **REAL** | proven by `websocket-events.jsonl` + a unit test | -| `vestige://trace/{runId}` resource | **REAL** | proven by the full-spine test | -| `sanhedrin.veto` trace | **CAVEAT** | extraction code is real + unit-tested, but the Sanhedrin verifier is an optional hook, **off by default** — no producer is connected, and the UI says exactly that | -| `dream.patch` trace | **REAL** (proven 2026-06-22) | a real `dream` run over 6 memories produced one `dream.patch` event under `run_dream_proof` — see `dream-trace.json` (last event), `dream-websocket-events.jsonl`, and `screenshots/dream-producers.png` where the row flips to "fired this run". The UI still shows "No dream run in this trace" for runs where no dream executed. | -| Graph-pulse "Open receipt in Cinema" | **REAL (deep-link)** | navigates the graph centered on the receipt's primary memory; MemoryCinema itself is unchanged | - -No feature is stubbed. The two CAVEATs are real plumbing whose upstream -producer is intentionally off by default — surfaced as explicit UI states, not -empty mystery. - -## dream.patch — proven with a real dream run (2026-06-22) - -Bounded follow-up: a single real `dream` consolidation flipped the `dream.patch` -producer from "quiet" to a recorded live event, same runId, every hop. - -- 6 related memories seeded under `run_dream_proof`, then one `dream` call. -- The dream produced one consolidation insight → one `dream.patch` event: - `dream:RecurringPattern:5d941c7f+a41aca72+b029fe53+6167f2c3+1117dd4e+e0782442` - (the real insight type + the six source memories it bridged). -- SQLite: `dream-trace.json` (14 events, last is `dream.patch`). -- API: `/api/traces/run_dream_proof/export` → `dream-trace.json`. -- WebSocket: `dream-websocket-events.jsonl` (the `dream.patch` TraceEvent). -- Dashboard: `screenshots/black-box-dream.png` + `screenshots/dream-producers.png` - (the producers row shows **dream.patch · fired this run**). - -`dream.patch` is real but not live-by-default: it fires only when a dream -actually runs. The UI says so for runs where it didn't. - -## Reproduce - -1. `VESTIGE_DATA_DIR= VESTIGE_DASHBOARD_ENABLED=true vestige-mcp` (stdio). -2. `initialize`, then drive `smart_ingest` / `deep_reference` calls with a - `runId` argument. -3. A sensitive-topic write (auth/security/money/identity/…) opens a Memory PR. -4. `curl /api/traces//export` → the `.vestige-trace.json`. -5. `cargo test -p vestige-mcp test_full_spine_one_runid_crosses_every_hop`. diff --git a/blackbox-proof-2026-06-22/REVIEW.md b/blackbox-proof-2026-06-22/REVIEW.md deleted file mode 100644 index 95b1a2f..0000000 --- a/blackbox-proof-2026-06-22/REVIEW.md +++ /dev/null @@ -1,198 +0,0 @@ -# Agent Black Box — Review Bundle - -**Branch:** `feat/agent-black-box` (head = branch tip) -**Base (review against):** `9e92a5999ada37bed9b4820bb25b7748b417411c` (the -`feat/dashboard-bleeding-edge` tip this branched from) -**Status:** feature work **frozen**. No quarantine-constellation work has -started. - -Start here, then read `PROOF.md` (the per-feature real/caveat/stub ledger) and -open the screenshots. - -> **Update — review findings addressed.** A full multi-agent review found 7 -> real issues (4 blockers). All are fixed and tested; this bundle was -> **re-captured from a single run (`run_proof`)** so trace.json, -> websocket-events.jsonl, and memory_pr.json now all carry the same runId. See -> "Review findings addressed" below. - ---- - -## Frozen public claim - -> Vestige records real MCP memory activity into a replayable local trace, with -> receipts and reviewable risky writes. - -We do **not** claim Sanhedrin vetoes are live by default. `dream.patch` is real -but fires only when a dream actually runs (proven below; the UI says so when no -dream ran). - ---- - -## What's in this bundle - -| File | What it is | -|------|------------| -| `REVIEW.md` | this file — the entry point | -| `PROOF.md` | per-feature real/caveat/stub ledger + reproduce steps | -| `status.json` | live server `/api/health` at capture time | -| `trace.json` | `.vestige-trace.json` export of `run_proof` (10 real events) | -| `receipt.json` | a real retrieval receipt (`r_2026_06_22_runproof`) | -| `memory_pr.json` | the risky auth write → Memory PR, **promoted** UI→API→SQLite | -| `websocket-events.jsonl` | live WS stream: `TraceEvent`×6, `MemoryPrOpened`, `MemoryPrDecided`, … | -| `dream-trace.json` | `run_dream_proof` export — 14 events, last is `dream.patch` | -| `dream-websocket-events.jsonl` | live WS stream containing the `dream.patch` `TraceEvent` | -| `screenshots/black-box.png` | Black Box tab (spine header, scrubber, producers, log) | -| `screenshots/receipts.png` | the `ReceiptCard` with real data + "Open receipt in Cinema" | -| `screenshots/memory-prs.png` | Memory PRs: diff, "Why this opened", `Decided: promote` | -| `screenshots/graph.png` | live graph constellation + Memory Cinema (unchanged) | -| `screenshots/black-box-dream.png` | Black Box on the dream run | -| `screenshots/dream-producers.png` | producers panel — `dream.patch · fired this run` | - ---- - -## Caveat ledger (the honest part) - -| Producer | Status | Why | -|----------|--------|-----| -| `mcp.call`, `memory.write`, `memory.retrieve`, `memory.suppress` | **REAL** | fire on real tool traffic; args hashed, never stored raw | -| `contradiction.detected` | **REAL** | fires when deep_reference surfaces a contradiction; UI shows "no contradiction in this run" otherwise | -| Memory Receipts | **REAL** | built from real scored memories + trust, persisted, attached to output | -| Risk-gated Memory PRs (quarantine review) | **REAL** | commit-then-suppress; audit preserved, influence suspended; Promote verified end-to-end | -| WebSocket broadcast | **REAL** | `websocket-events.jsonl` + unit test | -| `vestige://trace/{runId}` resource | **REAL** | full-spine test hop 5 | -| `dream.patch` | **REAL** (not live-by-default) | proven by `run_dream_proof`; fires only when a dream runs; UI says so otherwise | -| `sanhedrin.veto` | **CAVEAT** | extraction is real + unit-tested, but the Sanhedrin verifier is an **optional hook, off by default** — no producer connected; UI says exactly that | - -No feature is stubbed. The one remaining caveat is surfaced as an explicit UI -state, not an empty space. - ---- - -## Command receipts (run live at 2026-06-22T22:57:59Z) - -Toolchain: `rustc 1.95.0` · `cargo 1.95.0` · `node v24.12.0` · `pnpm 10.33.0`. - -``` -$ cargo test --workspace --lib -test result: ok. 529 passed; 0 failed; 0 ignored; 0 measured (vestige-core) -test result: ok. 33 passed; 0 failed; 0 ignored; 0 measured (tests/e2e unit) -test result: ok. 428 passed; 0 failed; 0 ignored; 0 measured (vestige-mcp) -# 990 lib tests, 0 failures - -$ cargo clippy --workspace -- -D warnings -Finished `dev` profile ... (EXIT 0, zero warnings) - -$ pnpm --filter @vestige/dashboard check -COMPLETED 905 FILES 0 ERRORS 0 WARNINGS 0 FILES_WITH_PROBLEMS - -$ pnpm --filter @vestige/dashboard build -✓ built in 4.15s ... ✔ done - -$ cargo test -p vestige-mcp test_full_spine_one_runid_crosses_every_hop -test server::tests::test_full_spine_one_runid_crosses_every_hop ... ok - -$ cargo test -p vestige-mcp trace_recorder::tests::extract_dream -test ...extract_dream_proposals_empty_when_not_dream_tool ... ok -test ...extract_dream_proposals_from_real_insights_shape ... ok - -$ cargo test -p vestige-core trace -test result: ok. 27 passed; 0 failed (trace schema, receipt, review) -``` - -Only statuses with a receipt above are credited. Nothing is claimed from memory. - ---- - -## Review surface (what changed) - -The Black Box work sits in a series of commits on top of the base. To see the -exact, current diff (build artifacts + this proof bundle excluded): - -``` -$ git diff --stat 9e92a59..HEAD -- ':!apps/dashboard/build' ':!blackbox-proof-2026-06-22' -# ~27 source files (Rust + SvelteKit). Run this against the branch tip for the -# live count — it grows as review fixes land. -``` - -Commits (oldest first) — run `git log --oneline 9e92a59..HEAD` for the live, -authoritative list; the series so far: -- `80c823a` feat: Agent Black Box + Receipts + risk-gated Memory PRs -- `b89beee` proof: Proof Lock — full-spine test, honest UI states, proof pack -- `140b15f` proof: dream.patch proven live with a real dream run -- `cadffb4` docs: package the review bundle — REVIEW.md entry point -- `8f7bed0` fix: review blockers B1–B7 + re-capture proof bundle -- `6a0173d` fix: C1 unconditional quarantine release + C2 trace destructive writes -- `…` fix: C2-deep (gate destructive writes post-delete) + PRIV (redact PR content) - -The hashes above are point-in-time; the branch tip is the source of truth. - -Key files to review: -- **Core (pure logic):** `crates/vestige-core/src/trace/{mod,receipt,review}.rs` -- **Persistence:** `crates/vestige-core/src/storage/trace_store.rs` + `migrations.rs` (V18) -- **MCP wiring:** `crates/vestige-mcp/src/trace_recorder.rs`, `server.rs` (dispatch), - `resources/trace.rs` -- **Dashboard API:** `crates/vestige-mcp/src/dashboard/{handlers,events,mod}.rs` -- **UI:** `apps/dashboard/src/routes/(app)/{blackbox,memory-prs}/+page.svelte`, - `lib/components/{ReceiptCard.svelte,blackbox-helpers.ts}` - ---- - -## Suggested review checklist - -- [ ] **Spine integrity:** read `test_full_spine_one_runid_crosses_every_hop` - (crates/vestige-mcp/src/server.rs) — does it actually assert the runId is - byte-identical at all five hops? -- [ ] **Privacy:** confirm `mcp.call` stores only a hash of args - (`trace_recorder::hash_args`), never raw args. -- [ ] **Risk taxonomy:** review `classify_write` + `WriteContext` - (crates/vestige-core/src/trace/review.rs) — is the sensitive-topic / - contradiction / supersede gating correct and not over-broad? -- [ ] **Quarantine semantics:** confirm risky writes are committed-then-suppressed - (audit preserved), not silently dropped, and the copy says so. -- [ ] **Migration safety:** V18 is additive; `MIGRATIONS.last().version` is used - by the migration tests (no hard-coded version literals left). -- [ ] **Local-first defaults:** Risk-Gated is default; Sanhedrin/dream producers - stay optional and off by default; nothing forces heavy models. -- [ ] **No protected code touched:** `MemoryCinema.svelte` and `graph/cinema/*` - are unchanged; the graph page only gained an additive `?center=` param. - ---- - -## Review findings addressed (2026-06-22) - -A full read-only review (multiple parallel agents, both Rust and dashboard) -found 7 real issues — 4 blockers. All fixed and tested: - -| # | Severity | Finding | Fix | Proof | -|---|----------|---------|-----|-------| -| B1 | blocker | Promoting a Memory PR didn't unsuppress the quarantined memory — UI said "promoted" while the memory stayed out of retrieval | `act_on_memory_pr` now calls `reverse_suppression(subject_id)` on accept actions (promote/merge/supersede); `MemoryPrAction::releases_memory()` encodes the rule | live: PR response `subjectReleased: true`, SQLite `suppression_count: 0`; tests `promote_releases_a_quarantined_memory_end_to_end`, `only_accept_actions_release_the_memory` | -| B2 | blocker | memory promote/demote (returns `action`, not `decision`) and `codebase` writes bypassed the write-trace + gate | `extract_writes` reads `action` too, filtered by `is_write_decision`; `is_write_tool` includes `codebase` | tests `extract_writes_recognizes_action_shape_b2`, `extract_writes_ignores_read_actions_b2`, `write_tool_set_includes_codebase_b2` | -| B3 | blocker | Receipt ids collided within a run (`r__` + `INSERT OR REPLACE`) — later receipt overwrote earlier | id is now `r___` | live: two receipts in `run_proof` have distinct ids; test `receipt_ids_unique_within_a_run_b3` | -| B4 | blocker | Proof bundle mis-assembled: `trace.json`=`run_proof` but `websocket-events.jsonl`=`run_proof2` | re-captured the whole bundle from one run | all artifacts now carry `run_proof` (verified) | -| B5 | P2 | Black Box receipts panel showed global latest, not the selected run's | `list_receipts_for_run` + `/api/receipts?run=` + page uses `listForRun(runId)` | live: `?run=run_proof` returns only that run; test `receipts_are_listable_per_run_b5` | -| B6 | P2 | `SENSITIVE_TOPICS` substring match false-fired ("tokenizer"→token, "author"→auth) | word-boundary matching | tests `sensitive_topic_word_boundary_no_false_positives_b6`, `..._still_catches_real_b6` | -| B7 | P3 | `set_review_mode` non-atomic write; export filename used raw `run_id` | `write_atomic` (temp+rename); filename sanitized; static routes declared before dynamic | covered by build + the atomic-write helper's existing use | -| C1 | blocker | B1's release used `reverse_suppression`, which **refuses past the 24h labile window** — a PR promoted late stayed suppressed | new `release_quarantine(id)`: unconditional release (no time limit), used by the PR handler instead | test `release_quarantine_works_past_the_labile_window_c1` (proves reverse_suppression refuses but release_quarantine succeeds at +100h) | -| C2 | blocker | `memory` `purge`/`delete` (destructive removal) bypassed the write-trace + gate | added purge/purged/delete/deleted/forget/forgotten to `is_write_decision` | test `extract_writes_recognizes_destructive_actions_c2` | -| C2-deep | blocker | C2 made purge *trace*, but `gate_writes` did `get_node→skip` on the (already-deleted) row, so a destructive write still **never opened a PR** | gate now treats a missing node as gateable for destructive decisions (builds the context from the decision, marks `forgets`); the PR records the removal with `deleted:true` | test `gate_opens_pr_for_destructive_write_after_node_deleted_c2`; **live:** purging a node opened a PR (`kind: node_decayed`, `deleted: true`) | -| PRIV | blocker | `gate_writes` copied **full `node.content`** into the PR `diff` + `title` — a real secret would leak into the `memory_prs` table and any exported proof bundle | PR now stores a truncated **preview** + a **content hash**; sensitive-topic-gated writes are fully **redacted** (`[redacted — sensitive content…]`); the committed `memory_pr.json` was re-captured and contains no secret | tests `gate_redacts_sensitive_content_in_pr_priv`, `content_preview_redacts_sensitive_and_truncates`; **live + bundle scan:** no secret string anywhere | - -One earlier (self-)review claim was **withdrawn**: the `/api/memory-prs/mode` -vs `/{id}` route order is *not* a functional bug — axum 0.8 / matchit gives -static segments priority. Reordered for clarity only. - -Net after fixes (B1–B7 + C1/C2 + C2-deep + PRIV): **1007 lib tests pass, clippy `-D warnings` clean, dashboard -check + build clean.** - -## Reproduce (any reviewer, locally) - -```sh -# 1. run a real server with the dashboard on -VESTIGE_DATA_DIR=$(mktemp -d) VESTIGE_DASHBOARD_ENABLED=true vestige-mcp # stdio JSON-RPC -# 2. initialize, then drive tools/call with a runId arg (smart_ingest, deep_reference) -# 3. a sensitive-topic write opens a Memory PR; promote it via the dashboard -# 4. export the trace: -curl -s http://127.0.0.1:3927/api/traces//export -# 5. for dream.patch: seed >=5 memories, then call the `dream` tool with the same runId -# 6. run the regression: cargo test -p vestige-mcp test_full_spine_one_runid_crosses_every_hop -``` diff --git a/blackbox-proof-2026-06-22/dream-trace.json b/blackbox-proof-2026-06-22/dream-trace.json deleted file mode 100644 index d96f918..0000000 --- a/blackbox-proof-2026-06-22/dream-trace.json +++ /dev/null @@ -1 +0,0 @@ -{"events":[{"argsHash":"ec88652d34b9d5b6","at":1782168489775,"runId":"run_dream_proof","tool":"smart_ingest","type":"mcp.call"},{"at":1782168489889,"diff":{"decision":"create"},"id":"b029fe53-5f78-4c49-8100-62e6243a19ae","runId":"run_dream_proof","source":"agent","type":"memory.write"},{"argsHash":"10134f9d053e1139","at":1782168490987,"runId":"run_dream_proof","tool":"smart_ingest","type":"mcp.call"},{"at":1782168491091,"diff":{"decision":"create"},"id":"6167f2c3-c567-4ec4-b63d-d0b6660173fc","runId":"run_dream_proof","source":"agent","type":"memory.write"},{"argsHash":"a687fb99ed887e10","at":1782168492200,"runId":"run_dream_proof","tool":"smart_ingest","type":"mcp.call"},{"at":1782168492297,"diff":{"decision":"create"},"id":"e0782442-0ce0-4a54-94db-4814ae392bbd","runId":"run_dream_proof","source":"agent","type":"memory.write"},{"argsHash":"a672de9e54d138f9","at":1782168493413,"runId":"run_dream_proof","tool":"smart_ingest","type":"mcp.call"},{"at":1782168493496,"diff":{"decision":"create"},"id":"a41aca72-7758-4f07-8da0-2e16469efc81","runId":"run_dream_proof","source":"agent","type":"memory.write"},{"argsHash":"d5878d7adad2cfe1","at":1782168494628,"runId":"run_dream_proof","tool":"smart_ingest","type":"mcp.call"},{"at":1782168494724,"diff":{"decision":"create"},"id":"5d941c7f-c75c-4c89-acc1-af1b95d3a4de","runId":"run_dream_proof","source":"agent","type":"memory.write"},{"argsHash":"14c334181d37393d","at":1782168495841,"runId":"run_dream_proof","tool":"smart_ingest","type":"mcp.call"},{"at":1782168495922,"diff":{"decision":"create"},"id":"1117dd4e-11f7-434d-b0b7-2b5bcbd841d4","runId":"run_dream_proof","source":"agent","type":"memory.write"},{"argsHash":"1071da7bf3583db3","at":1782168511374,"runId":"run_dream_proof","tool":"dream","type":"mcp.call"},{"at":1782168511375,"proposalIds":["dream:RecurringPattern:5d941c7f+a41aca72+b029fe53+6167f2c3+1117dd4e+e0782442"],"runId":"run_dream_proof","type":"dream.patch"}],"exportedAt":"2026-06-22T22:50:09.588334+00:00","format":"vestige-trace","runId":"run_dream_proof","summary":{"eventCount":14,"firstTool":"smart_ingest","lastAt":1782168511375,"retrievedCount":0,"startedAt":1782168489775,"suppressedCount":0,"vetoCount":0,"writeCount":6},"version":1} \ No newline at end of file diff --git a/blackbox-proof-2026-06-22/dream-websocket-events.jsonl b/blackbox-proof-2026-06-22/dream-websocket-events.jsonl deleted file mode 100644 index 9bdc8f3..0000000 --- a/blackbox-proof-2026-06-22/dream-websocket-events.jsonl +++ /dev/null @@ -1,5 +0,0 @@ -{"data": {"timestamp": "2026-06-22T22:48:29.454773+00:00", "version": "2.1.27"}, "type": "Connected"} -{"type": "TraceEvent", "data": {"run_id": "run_dream_proof", "seq": 12, "event": {"type": "mcp.call", "runId": "run_dream_proof", "tool": "dream", "argsHash": "1071da7bf3583db3", "at": 1782168511374}, "timestamp": "2026-06-22T22:48:31.374680Z"}} -{"type": "DreamStarted", "data": {"memory_count": 6, "timestamp": "2026-06-22T22:48:31.374852Z"}} -{"type": "DreamCompleted", "data": {"memories_replayed": 6, "connections_found": 0, "insights_generated": 1, "duration_ms": 0, "timestamp": "2026-06-22T22:48:31.375855Z"}} -{"type": "TraceEvent", "data": {"run_id": "run_dream_proof", "seq": 13, "event": {"type": "dream.patch", "runId": "run_dream_proof", "proposalIds": ["dream:RecurringPattern:5d941c7f+a41aca72+b029fe53+6167f2c3+1117dd4e+e0782442"], "at": 1782168511375}, "timestamp": "2026-06-22T22:48:31.375973Z"}} diff --git a/blackbox-proof-2026-06-22/memory_pr.json b/blackbox-proof-2026-06-22/memory_pr.json deleted file mode 100644 index e69de29..0000000 diff --git a/blackbox-proof-2026-06-22/receipt.json b/blackbox-proof-2026-06-22/receipt.json deleted file mode 100644 index e69de29..0000000 diff --git a/blackbox-proof-2026-06-22/screenshots/README.md b/blackbox-proof-2026-06-22/screenshots/README.md deleted file mode 100644 index 0ed5f2a..0000000 --- a/blackbox-proof-2026-06-22/screenshots/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# Proof Pack Screenshots - -Captured with Playwright (`@playwright/test`, headless Chromium, 1440×1700 @2x) -from the **live** Vestige dashboard at `http://localhost:5173/dashboard`, -proxying to a real `vestige-mcp` server with real trace data. - -| File | Tab | Shows | -|------|-----|-------| -| `black-box.png` | Black Box | spine header (WebSocket Connected), run picker (`proof`/`proof2`), timeline scrubber + colored ticks, current event detail, memory pulse, **event producers** (with honest `dream.patch`/`sanhedrin.veto` off-by-default states), receipts panel, full event log | -| `receipts.png` | Black Box → Receipts | a real `ReceiptCard`: receipt id, retrieved/suppressed/trust-floor, activation path, retrieved ids, "Open receipt in Cinema" | -| `memory-prs.png` | Memory PRs | killer line + quarantine-review note, Fast/Risk-Gated/Paranoid modes, status filters, PR rows, cognition diff, "Why this opened" signal (`sensitive_topic`), `Decided: promote` | -| `graph.png` | Graph | the live WebGL memory constellation + Memory Cinema button (unchanged) | - -Re-capture: start the dev server (`pnpm --filter @vestige/dashboard dev`), -point its `/api` proxy at a running `vestige-mcp` with trace data, then run the -capture script (see PROOF.md "Reproduce"). diff --git a/blackbox-proof-2026-06-22/screenshots/black-box-dream.png b/blackbox-proof-2026-06-22/screenshots/black-box-dream.png deleted file mode 100644 index 20ac8d0..0000000 Binary files a/blackbox-proof-2026-06-22/screenshots/black-box-dream.png and /dev/null differ diff --git a/blackbox-proof-2026-06-22/screenshots/black-box.png b/blackbox-proof-2026-06-22/screenshots/black-box.png deleted file mode 100644 index 25d524e..0000000 Binary files a/blackbox-proof-2026-06-22/screenshots/black-box.png and /dev/null differ diff --git a/blackbox-proof-2026-06-22/screenshots/dream-producers.png b/blackbox-proof-2026-06-22/screenshots/dream-producers.png deleted file mode 100644 index 9e410ed..0000000 Binary files a/blackbox-proof-2026-06-22/screenshots/dream-producers.png and /dev/null differ diff --git a/blackbox-proof-2026-06-22/screenshots/graph.png b/blackbox-proof-2026-06-22/screenshots/graph.png deleted file mode 100644 index 90db4eb..0000000 Binary files a/blackbox-proof-2026-06-22/screenshots/graph.png and /dev/null differ diff --git a/blackbox-proof-2026-06-22/screenshots/memory-prs.png b/blackbox-proof-2026-06-22/screenshots/memory-prs.png deleted file mode 100644 index e73a146..0000000 Binary files a/blackbox-proof-2026-06-22/screenshots/memory-prs.png and /dev/null differ diff --git a/blackbox-proof-2026-06-22/screenshots/receipts.png b/blackbox-proof-2026-06-22/screenshots/receipts.png deleted file mode 100644 index e443946..0000000 Binary files a/blackbox-proof-2026-06-22/screenshots/receipts.png and /dev/null differ diff --git a/blackbox-proof-2026-06-22/status.json b/blackbox-proof-2026-06-22/status.json deleted file mode 100644 index e69de29..0000000 diff --git a/blackbox-proof-2026-06-22/trace.json b/blackbox-proof-2026-06-22/trace.json deleted file mode 100644 index e69de29..0000000 diff --git a/blackbox-proof-2026-06-22/websocket-events.jsonl b/blackbox-proof-2026-06-22/websocket-events.jsonl deleted file mode 100644 index acfbda6..0000000 --- a/blackbox-proof-2026-06-22/websocket-events.jsonl +++ /dev/null @@ -1,8 +0,0 @@ -{"data": {"timestamp": "2026-06-23T00:43:28.238141+00:00", "version": "2.1.27"}, "type": "Connected"} -{"type": "TraceEvent", "data": {"run_id": "run_proof", "seq": 14, "event": {"type": "mcp.call", "runId": "run_proof", "tool": "deep_reference", "argsHash": "13a31297fe007a2e", "at": 1782175410153}, "timestamp": "2026-06-23T00:43:30.154710Z"}} -{"type": "TraceEvent", "data": {"run_id": "run_proof", "seq": 15, "event": {"type": "memory.retrieve", "runId": "run_proof", "ids": ["591c638e-1fc7-4b6d-bcb3-b7fcb6c0c7b3", "6aa12b99-270e-4fb6-b523-9f01b0bee16b", "26a3c976-043b-4915-accf-ae098c8dc66b", "76c13cba-7b88-4ce7-b7de-0a906d372806"], "activation": {"26a3c976-043b-4915-accf-ae098c8dc66b": 0.62, "591c638e-1fc7-4b6d-bcb3-b7fcb6c0c7b3": 0.62, "6aa12b99-270e-4fb6-b523-9f01b0bee16b": 0.53, "76c13cba-7b88-4ce7-b7de-0a906d372806": 0.62}, "at": 1782175410209}, "timestamp": "2026-06-23T00:43:30.209554Z"}} -{"type": "TraceEvent", "data": {"run_id": "run_proof", "seq": 16, "event": {"type": "mcp.call", "runId": "run_proof", "tool": "search", "argsHash": "ac19c646baf0673d", "at": 1782175411167}, "timestamp": "2026-06-23T00:43:31.167561Z"}} -{"type": "SearchPerformed", "data": {"query": "dashboard", "result_count": 2, "result_ids": ["26a3c976-043b-4915-accf-ae098c8dc66b", "76c13cba-7b88-4ce7-b7de-0a906d372806"], "duration_ms": 0, "timestamp": "2026-06-23T00:43:31.182829Z"}} -{"type": "TraceEvent", "data": {"run_id": "run_proof", "seq": 17, "event": {"type": "memory.retrieve", "runId": "run_proof", "ids": ["26a3c976-043b-4915-accf-ae098c8dc66b", "76c13cba-7b88-4ce7-b7de-0a906d372806"], "activation": {}, "at": 1782175411182}, "timestamp": "2026-06-23T00:43:31.182933Z"}} -{"type": "MemoryUnsuppressed", "data": {"id": "6aa12b99-270e-4fb6-b523-9f01b0bee16b", "remaining_count": 0, "timestamp": "2026-06-23T00:46:18.338387Z"}} -{"type": "MemoryPrDecided", "data": {"id": "pr_31ab4c15f1694504bf33be82715bee03", "decision": "promote", "status": "promoted", "timestamp": "2026-06-23T00:46:18.338407Z"}} diff --git a/crates/vestige-core/Cargo.toml b/crates/vestige-core/Cargo.toml index e794089..0171472 100644 --- a/crates/vestige-core/Cargo.toml +++ b/crates/vestige-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vestige-core" -version = "2.2.1" +version = "2.1.0" edition = "2024" rust-version = "1.91" authors = ["Vestige Team"] @@ -51,40 +51,12 @@ ort-dynamic = ["embeddings", "fastembed/ort-load-dynamic"] # Requires: fastembed with nomic-v2-moe feature nomic-v2 = ["embeddings", "fastembed/nomic-v2-moe"] -# Qwen3 Embeddings (Candle backend, opt-in for v2.1.1 re-embedding) -qwen3-embeddings = ["embeddings", "fastembed/qwen3", "dep:candle-core"] - -# Backwards-compatible feature alias from the original v2.1.0 naming. -qwen3-reranker = ["qwen3-embeddings"] - -# External-source connectors (#57). The connector *contract*, normalization, -# and content-hashing are always compiled (pure, no network). This feature adds -# the network-backed reference connectors (GitHub Issues, …) via `reqwest`, so -# the default local-first build never links an HTTP client. -connectors = ["dep:reqwest"] - -# Hosted managed-sync backend (Vestige Cloud). Adds the HTTP `PortableSyncBackend` -# (`HttpPortableSyncBackend`) that pull-merge-pushes the portable archive to a -# hosted blob endpoint over HTTPS with a per-user sync key. Like `connectors`, -# this is the ONLY thing that links an HTTP client — the default local-first -# build stays network-free. Uses `reqwest`'s blocking client because the -# `PortableSyncBackend` trait methods are synchronous. -cloud-sync = ["dep:reqwest", "reqwest/blocking", "dep:chacha20poly1305", "dep:argon2"] +# Qwen3 Reranker (Candle backend, high-precision cross-encoder) +qwen3-reranker = ["embeddings", "fastembed/qwen3"] # Metal GPU acceleration on Apple Silicon (significantly faster inference) metal = ["fastembed/metal"] -# CUDA GPU acceleration on NVIDIA hardware (Windows / Linux, x86_64 + aarch64). -# Propagates to `candle-core/cuda`, which pulls in `cudarc` and `candle-kernels` -# for a per-build nvcc compile pass. Pair with `qwen3-embeddings` so the Candle -# backend is present in the build graph. -# -# Build: cargo build --release -p vestige-mcp --features qwen3-embeddings,cuda -cuda = ["qwen3-embeddings", "candle-core/cuda"] - -# cuDNN on top of CUDA — additional fused kernels and faster inference paths. -cudnn = ["cuda", "candle-core/cudnn"] - [dependencies] # Serialization @@ -124,9 +96,8 @@ notify = "8" # OPTIONAL: Embeddings (fastembed v5 - local ONNX inference, 2026 bleeding edge) # ============================================================================ # nomic-embed-text-v1.5: 768 dimensions, 8192 token context, Matryoshka support -# fastembed v5 provides Nomic v2 MoE and Qwen3 feature-gated model loaders. +# v5.11: Adds Nomic v2 MoE (nomic-v2-moe feature) + Qwen3 reranker (qwen3 feature) fastembed = { version = "5.11", default-features = false, features = ["hf-hub-native-tls", "image-models"], optional = true } -candle-core = { version = "0.10.2", optional = true } # ============================================================================ # OPTIONAL: Vector Search (USearch - HNSW, 20x faster than FAISS) @@ -135,44 +106,10 @@ candle-core = { version = "0.10.2", optional = true } # 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. -# -# Disable default features so release binaries do not include SimSIMD's -# Haswell+/AVX2/FMA dispatch targets. Those kernels can trigger illegal -# instructions on older x86_64 CPUs that Vestige otherwise supports (#71). -# -# But re-enable `fp16lib` explicitly. usearch's defaults are -# ["simsimd", "fp16lib"]; with BOTH off, build.rs sets USEARCH_USE_FP16LIB=0 -# and USEARCH_USE_SIMSIMD=0, which selects the bare half-precision `#else` -# branch in include/usearch/index_plugins.hpp. That branch carries a -# `#warning` directive, which MSVC's cl.exe treats as fatal error C1021, -# breaking the Windows build (GCC/Clang only warn). `fp16lib` is a scalar, -# self-contained fp16<->fp32 conversion library with NO SIMD intrinsics, so -# re-enabling it sets USEARCH_USE_FP16LIB=1 (taking the non-warning branch) -# WITHOUT reintroducing the SimSIMD illegal-instruction risk from #71. Do not -# drop this feature. -usearch = { version = "=2.23.0", default-features = false, features = ["fp16lib"], optional = true } +usearch = { version = "=2.23.0", optional = true } # LRU cache for query embeddings lru = "0.16" -trait-variant = "0.1" -blake3 = "1" - -# ============================================================================ -# OPTIONAL: External-source connectors (#57) -# ============================================================================ -# HTTP client for network-backed reference connectors (GitHub Issues, Redmine). -# rustls so connectors build with no system OpenSSL dependency. Behind the -# `connectors` feature — the default local-first build does not link reqwest. -reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"], optional = true } - -# ============================================================================ -# OPTIONAL: Zero-knowledge client-side encryption for Vestige Cloud sync -# ============================================================================ -# XChaCha20-Poly1305 AEAD + Argon2id passphrase KDF. The archive is encrypted on -# the client before upload, so the hosted service only ever stores ciphertext. -# Behind the `cloud-sync` feature — the default local-first build links neither. -chacha20poly1305 = { version = "0.10", optional = true } -argon2 = { version = "0.5", optional = true } [dev-dependencies] tempfile = "3" diff --git a/crates/vestige-core/src/advanced/chains.rs b/crates/vestige-core/src/advanced/chains.rs index eaba514..3b92bd3 100644 --- a/crates/vestige-core/src/advanced/chains.rs +++ b/crates/vestige-core/src/advanced/chains.rs @@ -491,60 +491,60 @@ impl MemoryChainBuilder { let mut steps = Vec::new(); - for (i, mem_id) in path.memories.iter().enumerate() { + for (i, (mem_id, conn)) in path + .memories + .iter() + .zip(path.connections.iter().chain(std::iter::once(&Connection { + from_id: path.memories.last().cloned().unwrap_or_default(), + to_id: to.to_string(), + connection_type: ConnectionType::SemanticSimilarity, + strength: 1.0, + created_at: Utc::now(), + }))) + .enumerate() + { let preview = self .graph .get(mem_id) .map(|n| n.content_preview.clone()) .unwrap_or_default(); - // The connection that LED INTO memories[i] is connections[i-1] (the - // edge memories[i-1] -> memories[i]). The start step (i==0) has no - // incoming edge. The old code paired memories[i] with connections[i] - // (the OUTGOING edge) and appended a synthetic connection to mask the - // resulting off-by-one, mislabeling every step's connection type. - let incoming = if i == 0 { - None - } else { - path.connections.get(i - 1) - }; - let reasoning = if i == 0 { format!("Starting from '{}'", preview) } else { - let prev_preview = self - .graph - .get(&path.memories[i - 1]) - .map(|n| n.content_preview.as_str()) - .unwrap_or(""); - let rel = incoming - .map(|c| c.connection_type.description()) - .unwrap_or("relates to"); - format!("'{}' {} '{}'", prev_preview, rel, preview) + format!( + "'{}' {} '{}'", + self.graph + .get( + &path + .memories + .get(i.saturating_sub(1)) + .cloned() + .unwrap_or_default() + ) + .map(|n| n.content_preview.as_str()) + .unwrap_or(""), + conn.connection_type.description(), + preview + ) }; steps.push(ChainStep { memory_id: mem_id.clone(), memory_preview: preview, - connection_type: incoming - .map(|c| c.connection_type.clone()) - .unwrap_or(ConnectionType::SemanticSimilarity), - connection_strength: incoming.map(|c| c.strength).unwrap_or(1.0), + connection_type: conn.connection_type.clone(), + connection_strength: conn.strength, reasoning, }); } - // Calculate overall confidence as the geometric mean of the CONNECTION - // strengths. The root must be the number of connections (the count of - // factors in the product), not memories.len() (= connections + 1), which - // systematically inflated the reported confidence. - let n = path.connections.len().max(1) as f64; + // Calculate overall confidence let confidence = path .connections .iter() .map(|c| c.strength) .fold(1.0, |acc, s| acc * s) - .powf(1.0 / n); // Geometric mean over connections + .powf(1.0 / path.memories.len() as f64); // Geometric mean // Generate explanation let explanation = self.generate_explanation(&steps); diff --git a/crates/vestige-core/src/advanced/compression.rs b/crates/vestige-core/src/advanced/compression.rs index e99458b..2ea2309 100644 --- a/crates/vestige-core/src/advanced/compression.rs +++ b/crates/vestige-core/src/advanced/compression.rs @@ -274,10 +274,7 @@ impl MemoryCompressor { // Update stats self.stats.memories_compressed += memories.len(); self.stats.compressions_created += 1; - // saturating_sub: short/repetitive memories can compress LARGER than the - // original (header + bullet facts > tiny inputs), which would underflow a - // usize subtraction (panic in debug, huge wrap in release). - self.stats.bytes_saved += original_size.saturating_sub(compressed.compressed_size); + self.stats.bytes_saved += original_size - compressed.compressed_size; self.stats.operations += 1; self.update_average_stats(&compressed); diff --git a/crates/vestige-core/src/advanced/cross_project.rs b/crates/vestige-core/src/advanced/cross_project.rs index 706b46b..ffda53a 100644 --- a/crates/vestige-core/src/advanced/cross_project.rs +++ b/crates/vestige-core/src/advanced/cross_project.rs @@ -411,12 +411,7 @@ impl CrossProjectLearner { based_on: pattern.pattern.name.clone(), confidence: applicable.applicability_confidence, evidence: applicable.supporting_memories.clone(), - // saturating_sub: when confidence is low (small base) and - // the suggestion index i exceeds it, a plain `-` underflows - // — panicking in debug, wrapping to a huge value in release - // (which corrupts the Reverse-priority sort). Saturate to 0. - priority: ((10.0 * applicable.applicability_confidence) as u32) - .saturating_sub(i as u32), + priority: (10.0 * applicable.applicability_confidence) as u32 - i as u32, }); } } diff --git a/crates/vestige-core/src/advanced/dreams.rs b/crates/vestige-core/src/advanced/dreams.rs index 7e32547..d01d8e2 100644 --- a/crates/vestige-core/src/advanced/dreams.rs +++ b/crates/vestige-core/src/advanced/dreams.rs @@ -93,9 +93,6 @@ const CONNECTION_DECAY_FACTOR: f64 = 0.95; /// Minimum connection strength to keep const MIN_CONNECTION_STRENGTH: f64 = 0.1; -/// Maximum discovered connections kept in the live dreamer buffer. -const MAX_STORED_DREAM_CONNECTIONS: usize = 200_000; - /// Maximum memories to replay per cycle const MAX_REPLAY_MEMORIES: usize = 100; @@ -357,13 +354,8 @@ impl ConsolidationScheduler { /// Stage 1: Replay recent memories in sequence fn stage1_replay(&self, memories: &[DreamMemory]) -> MemoryReplay { - // Select the MOST RECENT memories, then order them chronologically for - // sequential replay. The old code took the first N in arbitrary input - // order BEFORE sorting, so "recent" memories were dropped whenever the - // caller's slice was not already recency-ordered. - let mut sorted: Vec<_> = memories.iter().collect(); - sorted.sort_by_key(|m| std::cmp::Reverse(m.created_at)); - sorted.truncate(MAX_REPLAY_MEMORIES); + // Sort by creation time for sequential replay + let mut sorted: Vec<_> = memories.iter().take(MAX_REPLAY_MEMORIES).collect(); sorted.sort_by_key(|m| m.created_at); let sequence: Vec = sorted.iter().map(|m| m.id.clone()).collect(); @@ -1137,81 +1129,44 @@ impl MemoryDreamer { /// Run a dream cycle on provided memories pub async fn dream(&self, memories: &[DreamMemory]) -> DreamResult { - self.dream_with_connections(memories).await.0 - } - - /// Run a dream cycle with a temporary config. - pub async fn dream_with_config( - &self, - memories: &[DreamMemory], - config: DreamConfig, - ) -> DreamResult { - self.dream_with_config_and_connections(memories, config) - .await - .0 - } - - /// Run a dream cycle and return the exact connections found in that run. - pub async fn dream_with_connections( - &self, - memories: &[DreamMemory], - ) -> (DreamResult, Vec) { - self.run_dream(memories, &self.config).await - } - - /// Run a dream cycle with a temporary config and return this run's connections. - pub async fn dream_with_config_and_connections( - &self, - memories: &[DreamMemory], - config: DreamConfig, - ) -> (DreamResult, Vec) { - self.run_dream(memories, &config).await - } - - async fn run_dream( - &self, - memories: &[DreamMemory], - config: &DreamConfig, - ) -> (DreamResult, Vec) { let start = std::time::Instant::now(); let mut stats = DreamStats::default(); // Filter memories based on config - let working_memories: Vec<_> = if config.focus_tags.is_empty() { + let working_memories: Vec<_> = if self.config.focus_tags.is_empty() { memories .iter() - .take(config.max_memories_per_dream) + .take(self.config.max_memories_per_dream) .collect() } else { memories .iter() - .filter(|m| m.tags.iter().any(|t| config.focus_tags.contains(t))) - .take(config.max_memories_per_dream) + .filter(|m| m.tags.iter().any(|t| self.config.focus_tags.contains(t))) + .take(self.config.max_memories_per_dream) .collect() }; stats.memories_analyzed = working_memories.len(); // Phase 1: Discover new connections - let new_connections = - self.discover_connections(&working_memories, &mut stats, config.min_similarity); + let new_connections = self.discover_connections(&working_memories, &mut stats); // Phase 2: Find clusters/patterns let clusters = self.find_clusters(&working_memories, &new_connections); stats.clusters_found = clusters.len(); // Phase 3: Generate insights - let insights = self.generate_insights(&working_memories, &clusters, &mut stats, config); + let insights = self.generate_insights(&working_memories, &clusters, &mut stats); // Phase 4: Strengthen important memories (would update storage) - let memories_strengthened = if config.enable_strengthening { + let memories_strengthened = if self.config.enable_strengthening { self.identify_memories_to_strengthen(&working_memories, &new_connections) } else { 0 }; // Phase 5: Identify compression candidates (would compress in storage) - let memories_compressed = if config.enable_compression { + let memories_compressed = if self.config.enable_compression { self.identify_compression_candidates(&working_memories) } else { 0 @@ -1240,7 +1195,7 @@ impl MemoryDreamer { } } - (result, new_connections) + result } /// Synthesize insights from memories without full dream cycle @@ -1248,20 +1203,12 @@ impl MemoryDreamer { let mut stats = DreamStats::default(); // Find clusters - let connections = self.discover_connections( - &memories.iter().collect::>(), - &mut stats, - self.config.min_similarity, - ); + let connections = + self.discover_connections(&memories.iter().collect::>(), &mut stats); let clusters = self.find_clusters(&memories.iter().collect::>(), &connections); // Generate insights - self.generate_insights( - &memories.iter().collect::>(), - &clusters, - &mut stats, - &self.config, - ) + self.generate_insights(&memories.iter().collect::>(), &clusters, &mut stats) } /// Get all generated insights @@ -1310,7 +1257,6 @@ impl MemoryDreamer { &self, memories: &[&DreamMemory], stats: &mut DreamStats, - min_similarity: f64, ) -> Vec { let mut connections = Vec::new(); @@ -1325,7 +1271,7 @@ impl MemoryDreamer { // Calculate similarity let similarity = self.calculate_similarity(mem_a, mem_b); - if similarity >= min_similarity { + if similarity >= self.config.min_similarity { let connection_type = self.determine_connection_type(mem_a, mem_b, similarity); let reasoning = self.generate_connection_reasoning(mem_a, mem_b, &connection_type); @@ -1518,7 +1464,6 @@ impl MemoryDreamer { memories: &[&DreamMemory], clusters: &[Vec], stats: &mut DreamStats, - config: &DreamConfig, ) -> Vec { let mut insights = Vec::new(); let memory_map: HashMap<_, _> = memories.iter().map(|m| (&m.id, *m)).collect(); @@ -1538,12 +1483,12 @@ impl MemoryDreamer { // Try to generate insight from this cluster if let Some(insight) = self.generate_insight_from_cluster(&cluster_memories) - && insight.novelty_score >= config.min_novelty + && insight.novelty_score >= self.config.min_novelty { insights.push(insight); } - if insights.len() >= config.max_insights { + if insights.len() >= self.config.max_insights { break; } } @@ -1600,20 +1545,15 @@ impl MemoryDreamer { memories: &[&DreamMemory], common_tags: &[String], ) -> (String, InsightType) { - // Determine insight type based on memory characteristics. Seed the - // (min, max) fold from the ACTUAL first timestamp, not (now, now-365d) - // sentinels: those sentinels leaked into the span for clusters of old - // memories (all created > 365d ago), where min stayed pinned near `now` - // and inflated time_span_days past 30, fabricating a TemporalTrend. - let time_span_days = memories + // Determine insight type based on memory characteristics + let time_range = memories .iter() .map(|m| m.created_at) - .fold(None::<(DateTime, DateTime)>, |acc, t| match acc { - None => Some((t, t)), - Some((lo, hi)) => Some((lo.min(t), hi.max(t))), - }) - .map(|(lo, hi)| (hi - lo).num_days()) - .unwrap_or(0); + .fold((Utc::now(), Utc::now() - Duration::days(365)), |acc, t| { + (acc.0.min(t), acc.1.max(t)) + }); + + let time_span_days = (time_range.1 - time_range.0).num_days(); if time_span_days > 30 { // Temporal trend @@ -1766,7 +1706,7 @@ impl MemoryDreamer { fn store_connections(&self, connections: &[DiscoveredConnection]) { if let Ok(mut stored) = self.connections.write() { stored.extend(connections.iter().cloned()); - // Keep the highest-scoring connections using a composite score + // 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 @@ -1781,7 +1721,7 @@ impl MemoryDreamer { // Strong old connections are retained longer than weak new ones, // but eventually yield to fresh high-quality discoveries. let len = stored.len(); - if len > MAX_STORED_DREAM_CONNECTIONS { + if len > 1000 { let now = Utc::now(); stored.sort_unstable_by(|a, b| { let score = |c: &DiscoveredConnection| -> f64 { @@ -1797,7 +1737,7 @@ impl MemoryDreamer { .partial_cmp(&score(a)) .unwrap_or(std::cmp::Ordering::Equal) }); - stored.truncate(MAX_STORED_DREAM_CONNECTIONS); + stored.truncate(1000); } } } @@ -1914,31 +1854,6 @@ mod tests { assert!(result.stats.connections_evaluated > 0); } - #[tokio::test] - async fn test_dense_dream_keeps_more_than_legacy_connection_cap() { - let dreamer = MemoryDreamer::with_config(DreamConfig { - max_memories_per_dream: 50, - min_similarity: 0.1, - ..DreamConfig::default() - }); - - let memories: Vec<_> = (0..50) - .map(|i| { - make_memory( - &format!("dense-{i}"), - &format!("Dense single-domain memory {i} about shared identity systems"), - vec!["dense", "identity"], - ) - }) - .collect(); - - let (result, connections) = dreamer.dream_with_connections(&memories).await; - - assert_eq!(result.new_connections_found, 1_225); - assert_eq!(connections.len(), 1_225); - assert_eq!(dreamer.get_connections().len(), 1_225); - } - #[test] fn test_tag_similarity() { let dreamer = MemoryDreamer::new(); diff --git a/crates/vestige-core/src/advanced/importance.rs b/crates/vestige-core/src/advanced/importance.rs index 75d15bf..6c06376 100644 --- a/crates/vestige-core/src/advanced/importance.rs +++ b/crates/vestige-core/src/advanced/importance.rs @@ -183,14 +183,6 @@ impl ImportanceTracker { /// Update importance when a memory is retrieved pub fn on_retrieved(&self, memory_id: &str, was_helpful: bool) { - self.record_retrieval(memory_id, was_helpful, None); - } - - /// Push a usage event (with its context already populated) and update the - /// importance score. Context is set in the SAME critical section as the push - /// so a concurrent on_retrieved cannot slip an event in between and steal the - /// context via last_mut() (the previous two-lock approach raced). - fn record_retrieval(&self, memory_id: &str, was_helpful: bool, context: Option) { let now = Utc::now(); // Record the event @@ -198,7 +190,7 @@ impl ImportanceTracker { events.push(UsageEvent { memory_id: memory_id.to_string(), was_helpful, - context, + context: None, timestamp: now, }); @@ -235,7 +227,15 @@ impl ImportanceTracker { /// Update importance with additional context pub fn on_retrieved_with_context(&self, memory_id: &str, was_helpful: bool, context: &str) { - self.record_retrieval(memory_id, was_helpful, Some(context.to_string())); + self.on_retrieved(memory_id, was_helpful); + + // 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()); + } } /// Apply importance decay to all memories diff --git a/crates/vestige-core/src/advanced/merge_supersede.rs b/crates/vestige-core/src/advanced/merge_supersede.rs deleted file mode 100644 index ccc5b4b..0000000 --- a/crates/vestige-core/src/advanced/merge_supersede.rs +++ /dev/null @@ -1,458 +0,0 @@ -//! # Merge / Supersede Controls (Phase 3) -//! -//! Diff-previewed, confidence-gated, reversible, self-explaining combine / -//! dedupe / supersede operations on a never-delete (bitemporal) store. -//! -//! This module holds the **pure** logic: candidate scoring, two-threshold -//! classification, and the plan / operation data model. The actual persistence -//! (writing plans, applying them, recording the reversible operation log, and -//! bitemporally invalidating superseded nodes) lives in -//! [`crate::storage`]. Keeping the math here makes it unit-testable without a -//! database. -//! -//! ## Design north star -//! -//! Every combine/dedupe/supersede operation is: -//! -//! - **diff-previewed** — `plan_merge` / `plan_supersede` produce a [`MergePlan`] -//! you can inspect before anything mutates, -//! - **confidence-gated** — a Fellegi-Sunter two-threshold score classifies each -//! candidate as match / possible-match / non-match, -//! - **reversible** — every applied plan records a [`MergeOperation`] with an -//! undo payload (the "git reflog for your agent's memory"), -//! - **self-explaining** — each candidate carries the [`MatchSignals`] that -//! explain *why* the memories combined, -//! - **opt-in, never silent** — the default is preview/review, never auto-mutate, -//! - **audit-preserving** — superseding stamps `valid_until` and keeps the old -//! node queryable (Graphiti-style "invalidate, don't delete"). -//! -//! ## Why Fellegi-Sunter -//! -//! Pure hashing under-merges (misses paraphrases); aggressive LLM merging -//! over-merges and destroys the audit trail. Fellegi-Sunter record linkage uses -//! **two** thresholds to carve the score space into three zones, so the -//! borderline "possible match" cases are surfaced for review instead of being -//! force-decided. We reuse the embedding cosine similarity already in the store -//! plus cheap lexical signals (tag overlap, token Jaccard) as the match weight. - -use serde::{Deserialize, Serialize}; - -// ============================================================================ -// CONSTANTS — the two Fellegi-Sunter thresholds -// ============================================================================ - -/// Above this combined score → automatic-eligible "match". -pub const DEFAULT_MATCH_THRESHOLD: f32 = 0.86; - -/// Between the two thresholds → "possible match", surfaced for review. -/// Below this → "non-match" (never offered). -pub const DEFAULT_POSSIBLE_THRESHOLD: f32 = 0.72; - -/// Weight of embedding cosine similarity in the combined score. -const W_EMBEDDING: f32 = 0.70; -/// Weight of tag overlap (Jaccard) in the combined score. -const W_TAGS: f32 = 0.15; -/// Weight of content token overlap (Jaccard) in the combined score. -const W_TOKENS: f32 = 0.15; - -// ============================================================================ -// CLASSIFICATION -// ============================================================================ - -/// Fellegi-Sunter three-way classification of a candidate pair/cluster. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum MatchClass { - /// Score ≥ match threshold — strong duplicate, auto-merge eligible. - Match, - /// Between thresholds — surfaced for human/agent review, never auto-applied. - Possible, - /// Below the possible threshold — not offered as a candidate. - NonMatch, -} - -impl MatchClass { - /// String label used in tool output and the `classification` column. - pub fn as_str(&self) -> &'static str { - match self { - MatchClass::Match => "match", - MatchClass::Possible => "possible", - MatchClass::NonMatch => "non_match", - } - } -} - -/// Per-merge-policy thresholds. Wired to `vestige.toml` when present, else the -/// defaults above. `auto_apply` gates whether `Match`-class candidates may be -/// applied without an explicit preview step (default: false — never silent). -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub struct MergePolicy { - /// Score ≥ this → `Match`. - pub match_threshold: f32, - /// Score in `[possible_threshold, match_threshold)` → `Possible`. - pub possible_threshold: f32, - /// If true, `Match`-class candidates may be auto-applied. Default false: - /// the product promise is review/preview, not silent mutation. - pub auto_apply: bool, -} - -impl Default for MergePolicy { - fn default() -> Self { - Self { - match_threshold: DEFAULT_MATCH_THRESHOLD, - possible_threshold: DEFAULT_POSSIBLE_THRESHOLD, - auto_apply: false, - } - } -} - -impl MergePolicy { - /// Build a policy, clamping thresholds into `[0,1]` and ensuring - /// `possible_threshold <= match_threshold`. - pub fn new(match_threshold: f32, possible_threshold: f32, auto_apply: bool) -> Self { - let match_threshold = match_threshold.clamp(0.0, 1.0); - let possible_threshold = possible_threshold.clamp(0.0, match_threshold); - Self { - match_threshold, - possible_threshold, - auto_apply, - } - } - - /// Classify a combined match score. - pub fn classify(&self, score: f32) -> MatchClass { - if score >= self.match_threshold { - MatchClass::Match - } else if score >= self.possible_threshold { - MatchClass::Possible - } else { - MatchClass::NonMatch - } - } -} - -// ============================================================================ -// SIGNALS — the self-explaining "why did these combine?" -// ============================================================================ - -/// The individual signals behind a candidate's score. Surfaced verbatim so a -/// user can see *why* two memories were judged duplicates. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MatchSignals { - /// Cosine similarity of the two embeddings (0–1). - pub embedding_similarity: f32, - /// Jaccard overlap of the two tag sets (0–1). - pub tag_overlap: f32, - /// Jaccard overlap of content tokens (0–1). - pub token_overlap: f32, - /// Combined weighted score that was classified. - pub combined_score: f32, -} - -/// Compute the combined match score and its signal breakdown for a pair. -pub fn score_pair( - embedding_similarity: f32, - a_tags: &[String], - b_tags: &[String], - a_content: &str, - b_content: &str, -) -> MatchSignals { - let tag_overlap = jaccard(&tag_set(a_tags), &tag_set(b_tags)); - let token_overlap = jaccard(&token_set(a_content), &token_set(b_content)); - let combined_score = (W_EMBEDDING * embedding_similarity.clamp(0.0, 1.0) - + W_TAGS * tag_overlap - + W_TOKENS * token_overlap) - .clamp(0.0, 1.0); - MatchSignals { - embedding_similarity: embedding_similarity.clamp(0.0, 1.0), - tag_overlap, - token_overlap, - combined_score, - } -} - -fn tag_set(tags: &[String]) -> std::collections::HashSet { - tags.iter().map(|t| t.to_lowercase()).collect() -} - -fn token_set(content: &str) -> std::collections::HashSet { - content - .split(|c: char| !c.is_alphanumeric()) - .filter(|t| t.len() > 2) - .map(|t| t.to_lowercase()) - .collect() -} - -fn jaccard(a: &std::collections::HashSet, b: &std::collections::HashSet) -> f32 { - if a.is_empty() && b.is_empty() { - return 0.0; - } - let inter = a.intersection(b).count() as f32; - let union = a.union(b).count() as f32; - if union == 0.0 { 0.0 } else { inter / union } -} - -// ============================================================================ -// CANDIDATE -// ============================================================================ - -/// A surfaced merge candidate: a cluster of likely-duplicate memories with the -/// signals and classification that justify offering it. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MergeCandidate { - /// Node ids in the cluster. The first is the suggested survivor (highest - /// retention). - pub member_ids: Vec, - /// Short content previews, parallel to `member_ids`. - pub previews: Vec, - /// Suggested survivor id (kept after a merge). - pub survivor_id: String, - /// Combined match score for the cluster (min pairwise within the cluster — - /// the weakest link, so a cluster is only as confident as its loosest pair). - pub confidence: f32, - /// Three-way classification under the active policy. - pub classification: MatchClass, - /// Signals for the survivor↔closest-member pair (the explanation). - pub signals: MatchSignals, - /// True if any member is protected (pinned) — blocks auto-merge. - pub has_protected_member: bool, -} - -// ============================================================================ -// PLAN — the previewable diff -// ============================================================================ - -/// What kind of plan this is. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum PlanKind { - /// Combine N memories into one survivor. - Merge, - /// Invalidate A in favour of B (bitemporal, audit-preserving). - Supersede, -} - -impl PlanKind { - pub fn as_str(&self) -> &'static str { - match self { - PlanKind::Merge => "merge", - PlanKind::Supersede => "supersede", - } - } -} - -/// A previewable plan: exactly what *would* change, without changing anything. -/// Persisted to `merge_plans`; consumed by `apply_plan` via its `id`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MergePlan { - /// Plan id (UUID). - pub id: String, - /// merge | supersede. - pub kind: PlanKind, - /// Node kept after the operation. - pub survivor_id: String, - /// All node ids involved. - pub member_ids: Vec, - /// Resulting content of the survivor after applying. - pub result_content: String, - /// Resulting tag set of the survivor after applying. - pub result_tags: Vec, - /// Resulting provenance / source string after applying. - pub result_source: Option, - /// For supersede: ids that get bitemporally invalidated (their - /// `valid_until` stamped, kept queryable). For merge: the absorbed ids. - pub invalidated_ids: Vec, - /// Match confidence (0–1) for the plan. - pub confidence: f32, - /// Three-way classification. - pub classification: MatchClass, - /// Signals explaining the plan. - pub signals: MatchSignals, - /// Human-readable explanation of what this plan does. - pub explanation: String, -} - -// ============================================================================ -// OPERATION LOG — the reversible "memory reflog" -// ============================================================================ - -/// A recorded, reversible operation. One row in `merge_operations`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MergeOperation { - /// Operation id (UUID). - pub id: String, - /// Plan id this came from (if any). - pub plan_id: Option, - /// merge | supersede | undo. - pub op_type: String, - /// applied | reverted. - pub status: String, - /// When recorded (RFC3339). - pub created_at: String, - /// When reverted (RFC3339), if reverted. - pub reverted_at: Option, - /// For undo ops: the op id being reversed. - pub reverts_op_id: Option, - /// Survivor node id. - pub survivor_id: Option, - /// Node ids touched by the op. - pub affected_ids: Vec, - /// Match confidence. - pub confidence: Option, - /// Human-readable reason. - pub reason: Option, -} - -// ============================================================================ -// MERGE COMPOSITION — pure helpers used by the storage apply path -// ============================================================================ - -/// Compose merged content from an ordered list of (id, content) members. -/// Survivor content leads; each absorbed member is appended with provenance so -/// nothing is silently dropped (anti-pattern: Mem0 #4896 double-store / -/// contradiction loss). -pub fn compose_merged_content(members: &[(String, String)]) -> String { - if members.is_empty() { - return String::new(); - } - // Dedup on EXACT normalized content, not substring containment. The old - // `out.contains(c)` test silently dropped a distinct member whose text merely - // appeared as a substring of the accumulated output (e.g. "cat" inside - // "cathedral"), losing its content and provenance. - let norm = |s: &str| s.trim().to_lowercase(); - let first = members[0].1.trim().to_string(); - let mut seen: std::collections::HashSet = std::collections::HashSet::new(); - seen.insert(norm(&first)); - let mut out = first; - for (id, content) in &members[1..] { - let c = content.trim(); - if c.is_empty() || !seen.insert(norm(c)) { - continue; // empty or an exact (normalized) duplicate already present - } - out.push_str("\n\n[merged from "); - out.push_str(id); - out.push_str("]\n"); - out.push_str(c); - } - out -} - -/// Union the tag sets of all members, preserving first-seen order. -pub fn compose_merged_tags(member_tags: &[Vec]) -> Vec { - let mut seen = std::collections::HashSet::new(); - let mut out = Vec::new(); - for tags in member_tags { - for t in tags { - if seen.insert(t.to_lowercase()) { - out.push(t.clone()); - } - } - } - out -} - -// ============================================================================ -// TESTS -// ============================================================================ - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn classify_three_zones() { - let policy = MergePolicy::default(); - assert_eq!(policy.classify(0.95), MatchClass::Match); - assert_eq!(policy.classify(0.80), MatchClass::Possible); - assert_eq!(policy.classify(0.50), MatchClass::NonMatch); - // boundaries are inclusive at the lower edge of each higher zone - assert_eq!(policy.classify(DEFAULT_MATCH_THRESHOLD), MatchClass::Match); - assert_eq!( - policy.classify(DEFAULT_POSSIBLE_THRESHOLD), - MatchClass::Possible - ); - } - - #[test] - fn policy_clamps_and_orders() { - // possible above match gets clamped down to match - let p = MergePolicy::new(0.8, 0.95, true); - assert!(p.possible_threshold <= p.match_threshold); - // out-of-range clamps to [0,1] - let p2 = MergePolicy::new(2.0, -1.0, false); - assert_eq!(p2.match_threshold, 1.0); - assert_eq!(p2.possible_threshold, 0.0); - } - - #[test] - fn score_pair_combines_signals() { - let s = score_pair( - 1.0, - &["rust".into(), "async".into()], - &["rust".into(), "async".into()], - "use tokio for async rust", - "use tokio for async rust", - ); - assert!((s.embedding_similarity - 1.0).abs() < 1e-6); - assert!((s.tag_overlap - 1.0).abs() < 1e-6); - assert!(s.token_overlap > 0.9); - assert!(s.combined_score > 0.95); - } - - #[test] - fn score_pair_disjoint_is_low() { - let s = score_pair( - 0.1, - &["a".into()], - &["b".into()], - "completely different topic alpha", - "totally unrelated subject beta", - ); - assert!(s.combined_score < 0.3); - assert_eq!( - MergePolicy::default().classify(s.combined_score), - MatchClass::NonMatch - ); - } - - #[test] - fn jaccard_basics() { - let a: std::collections::HashSet = ["x".into(), "y".into()].into_iter().collect(); - let b: std::collections::HashSet = ["y".into(), "z".into()].into_iter().collect(); - assert!((jaccard(&a, &b) - (1.0 / 3.0)).abs() < 1e-6); - let empty: std::collections::HashSet = Default::default(); - assert_eq!(jaccard(&empty, &empty), 0.0); - } - - #[test] - fn compose_merged_content_dedups_and_attributes() { - let members = vec![ - ("a".into(), "Keep this.".into()), - ("b".into(), "Extra detail.".into()), - ("c".into(), "Keep this.".into()), // duplicate of survivor → skipped - ]; - let merged = compose_merged_content(&members); - assert!(merged.starts_with("Keep this.")); - assert!(merged.contains("[merged from b]")); - assert!(merged.contains("Extra detail.")); - // duplicate content not appended twice - assert_eq!(merged.matches("Keep this.").count(), 1); - } - - #[test] - fn compose_merged_tags_unions_in_order() { - let tags = vec![ - vec!["rust".into(), "async".into()], - vec!["async".into(), "tokio".into()], - ]; - let merged = compose_merged_tags(&tags); - assert_eq!(merged, vec!["rust", "async", "tokio"]); - } - - #[test] - fn match_class_labels() { - assert_eq!(MatchClass::Match.as_str(), "match"); - assert_eq!(MatchClass::Possible.as_str(), "possible"); - assert_eq!(MatchClass::NonMatch.as_str(), "non_match"); - } -} diff --git a/crates/vestige-core/src/advanced/mod.rs b/crates/vestige-core/src/advanced/mod.rs index d737b10..fdbdfe4 100644 --- a/crates/vestige-core/src/advanced/mod.rs +++ b/crates/vestige-core/src/advanced/mod.rs @@ -23,10 +23,8 @@ pub mod cross_project; pub mod dreams; pub mod importance; pub mod intent; -pub mod merge_supersede; pub mod prediction_error; pub mod reconsolidation; -pub mod retroactive_backfill; pub mod speculative; // Re-exports for convenient access @@ -63,11 +61,6 @@ pub use dreams::{ }; pub use importance::{ImportanceDecayConfig, ImportanceScore, ImportanceTracker, UsageEvent}; pub use intent::{ActionType, DetectedIntent, IntentDetector, MaintenanceType, UserAction}; -pub use merge_supersede::{ - DEFAULT_MATCH_THRESHOLD, DEFAULT_POSSIBLE_THRESHOLD, MatchClass, MatchSignals, MergeCandidate, - MergeOperation, MergePlan, MergePolicy, PlanKind, compose_merged_content, compose_merged_tags, - score_pair, -}; pub use prediction_error::{ CandidateMemory, CreateReason, EvaluationIntent, GateDecision, GateStats, MergeStrategy, PredictionErrorConfig, PredictionErrorGate, SimilarityResult, SupersedeReason, UpdateType, @@ -78,7 +71,4 @@ pub use reconsolidation::{ Modification, ReconsolidatedMemory, ReconsolidationManager, ReconsolidationStats, RelationshipType, RetrievalRecord, }; -pub use retroactive_backfill::{ - BackfillCandidate, BackfillResult, BackfilledCause, FailureEvent, RetroactiveBackfill, -}; 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 7d79cd6..3693d77 100644 --- a/crates/vestige-core/src/advanced/prediction_error.rs +++ b/crates/vestige-core/src/advanced/prediction_error.rs @@ -493,10 +493,6 @@ impl PredictionErrorGate { ) -> GateDecision { match intent { EvaluationIntent::ForceCreate => { - // Count this evaluation: the fallback branches reach evaluate() - // (which counts), but these direct branches must count themselves - // or create/update/supersede rates can exceed 1.0. - self.stats.total_evaluations += 1; self.stats.creates += 1; GateDecision::Create { reason: CreateReason::ExplicitCreate, @@ -508,7 +504,6 @@ impl PredictionErrorGate { // Find the target candidate if let Some(c) = candidates.iter().find(|c| c.id == target_id) { let similarity = cosine_similarity(new_embedding, &c.embedding); - self.stats.total_evaluations += 1; self.stats.updates += 1; GateDecision::Update { target_id: target_id.clone(), @@ -527,7 +522,6 @@ impl PredictionErrorGate { } => { if let Some(c) = candidates.iter().find(|c| c.id == old_memory_id) { let similarity = cosine_similarity(new_embedding, &c.embedding); - self.stats.total_evaluations += 1; self.stats.supersedes += 1; GateDecision::Supersede { old_memory_id, @@ -550,28 +544,23 @@ impl PredictionErrorGate { let new_lower = new_content.to_lowercase(); let old_lower = old_content.to_lowercase(); - // Check for explicit negation patterns — a real polarity FLIP, not the - // mere presence of a negation word. We require the negation term in the - // new content AND its paired positive term in the old content (old: "use - // X"; new: "avoid X"). The previous logic fired whenever the new content - // merely contained a negation word the old one lacked, so ordinary - // additive notes ("Do not forget to configure X" — contains "not ") were - // misread as corrections and demoted a correct memory. Bare triggers with - // no paired positive term ("not ", "instead of", "rather than") are - // dropped for the same reason: they match complementary phrasing. + // Check for explicit negation patterns let negation_pairs = [ - ("don't use", "use"), ("don't", "do"), ("never", "always"), ("avoid", "use"), ("wrong", "right"), + ("bad", "good"), ("incorrect", "correct"), ("deprecated", "recommended"), ("outdated", "current"), + ("instead of", ""), + ("rather than", ""), + ("not ", ""), ]; - for (neg, pos) in negation_pairs.iter() { - if new_lower.contains(neg) && old_lower.contains(pos) && !old_lower.contains(neg) { + for (neg, _pos) in negation_pairs.iter() { + if new_lower.contains(neg) && !old_lower.contains(neg) { return true; } } @@ -840,24 +829,6 @@ mod tests { "Use async/await for performance", "Use async patterns when needed" )); - - // Regression: a benign ADDITIVE note that merely contains a negation word - // ("do not", "cannot") must NOT be flagged as a contradiction. Previously - // the bare "not " substring fired here and demoted the correct memory. - assert!( - !gate.detect_contradiction( - "Do not forget to configure the async runtime for the worker pool", - "Use the async runtime for the worker pool" - ), - "additive 'do not forget' note must not read as a contradiction" - ); - assert!( - !gate.detect_contradiction( - "You cannot skip the migration step", - "Run the migration step before deploying" - ), - "'cannot' in complementary guidance must not read as a contradiction" - ); } #[test] diff --git a/crates/vestige-core/src/advanced/reconsolidation.rs b/crates/vestige-core/src/advanced/reconsolidation.rs index 93c99e6..933442c 100644 --- a/crates/vestige-core/src/advanced/reconsolidation.rs +++ b/crates/vestige-core/src/advanced/reconsolidation.rs @@ -551,10 +551,6 @@ impl ReconsolidationManager { /// /// Returns the reconsolidation result with all applied modifications. pub fn reconsolidate(&mut self, memory_id: &str) -> Option { - // remove() already guarantees idempotency: a second call finds no entry - // and returns None via `?`. The `reconsolidated` flag was never set to - // true anywhere, so the guard below was dead — but keep it as a correct, - // explicit belt-and-suspenders in case the entry is ever retained. let state = self.labile_memories.remove(memory_id)?; if state.reconsolidated { diff --git a/crates/vestige-core/src/advanced/retroactive_backfill.rs b/crates/vestige-core/src/advanced/retroactive_backfill.rs deleted file mode 100644 index d298b3c..0000000 --- a/crates/vestige-core/src/advanced/retroactive_backfill.rs +++ /dev/null @@ -1,497 +0,0 @@ -//! # Retroactive Salience Backfill -//! -//! Memory with hindsight. When a salient *failure* event lands (a bug, crash, -//! regression — the "aversive event"), this reaches **backward in time** and -//! promotes the quiet earlier memory that secretly caused it — the one a pure -//! semantic search will never surface because it isn't *similar* to the failure, -//! only *causally upstream* of it. -//! -//! ## Scientific basis -//! -//! Faithful port of Zaki, Cai et al. (2024), *Nature* 637:145-155, "Offline -//! ensemble co-reactivation links memories across days." Key findings ported: -//! -//! - A **neutral** memory formed earlier is retroactively promoted to important -//! only when a **salient** event later co-reactivates the two ensembles -//! offline. (Here: the dream/consolidation pass is the offline window.) -//! - **The asymmetry is backward-only**: "fear links retrospectively, but not -//! prospectively." A failure promotes the *past* cause, never a future memory. -//! This is also exactly correct for software: a root cause is always upstream -//! in time. The biological directionality earns its keep, it is not decorative. -//! - Linking flows along the **overlap ensemble** — memories that share entities -//! (same file, env var, service, symbol). That shared-entity edge is the join -//! key the backward scan follows; semantic similarity is deliberately NOT the -//! ranking signal (that is the whole point — RAG already covers similarity). -//! -//! Honesty note for callers: this is scoped to *failure → backward causal -//! backfill*, not a universal "all salience flows backward" law. The Cai paper -//! is an aversive→neutral paradigm; we mirror that scope intentionally. - -use serde::{Deserialize, Serialize}; -use std::collections::HashSet; - -// ============================================================================ -// CONSTANTS -// ============================================================================ - -/// A memory must be at least this surprising (prediction error, 0..1) to count -/// as a salient "aversive event" that can trigger a backfill. Mirrors the gate's -/// own surprise scale. Manual triggers bypass this. -pub const DEFAULT_SALIENCE_THRESHOLD: f32 = 0.55; - -/// How far back in time the backward reach scans, in days. The Cai paradigm -/// linked across ~2 days; software causes can be older, so we default wider. -pub const DEFAULT_LOOKBACK_DAYS: i64 = 30; - -/// A candidate must share at least this many entities with the failure to be -/// considered causally upstream (1 shared file/env-var/service is enough). -pub const MIN_SHARED_ENTITIES: usize = 1; - -/// Words that mark a memory as a failure/"aversive" event when auto-detecting. -/// Lowercased substring match against content + tags. -pub const FAILURE_MARKERS: &[&str] = &[ - "error", "bug", "crash", "crashed", "regression", "broke", "broken", - "failure", "failed", "panic", "exception", "fault", "outage", "incident", - // NOTE: bare "500" was removed — it matched benign content like "$500", - // "500 users", or "line 500" and wrongly flagged a quiet CAUSE memory as a - // failure, excluding it from the backward reach. The specific HTTP error - // codes 502/503/504 below stay; a genuine "HTTP 500" is still caught by - // "error"/"failed"/"exception" in any real incident note. - "timeout", "deadlock", "leak", "corrupt", "stack overflow", - // performance/degradation failures (an agent should backfill from these too) - "spiked", "latency", "degraded", "slow", "hang", "hung", "throttled", - "oom", "502", "503", "504", "rejected", "denied", "flaky", - // real-incident vocabulary (CauseBench found these missing — postmortems often - // describe failures without the classic crash words above) - "pinned", "saturated", "saturation", "stalled", "exhausted", "exhaustion", - "overload", "overloaded", "backlog", "fell behind", "lag", "lagging", - "unavailable", "down", "dropped", "reset", "refused", "stampede", - "thrashing", "starved", "starvation", "expired", "expiry", "overflow", -]; - -/// How strongly to promote the backfilled cause: multiply its stability by this -/// (capped). A real boost so the cause stops decaying and surfaces in future -/// recalls — without overwriting the FSRS history. -pub const PROMOTION_STABILITY_FACTOR: f64 = 2.5; - -// ============================================================================ -// INPUT TYPES -// ============================================================================ - -/// The minimal view of a memory the backfill needs. Built from a KnowledgeNode -/// by the caller (keeps this module storage-agnostic + trivially testable). -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BackfillCandidate { - pub id: String, - pub content: String, - /// Entities this memory mentions: files, env vars, services, symbols. - pub entities: Vec, - /// Age in days relative to the failure event (older = larger). Negative or - /// zero means it is NOT in the past relative to the failure → excluded. - pub age_days_before_failure: f64, - /// Current FSRS stability (we promote by boosting this). - pub stability: f64, - /// Optional cosine similarity to the failure, ONLY used to demonstrate that - /// the cause ranks LOW on similarity (the thing RAG misses). Not a ranker. - pub similarity_to_failure: Option, -} - -/// The salient failure event that triggers the backward reach. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FailureEvent { - pub id: String, - pub content: String, - pub entities: Vec, - /// The failure's tags — failure markers can live in a tag, so salience - /// detection must see them too. - #[serde(default)] - pub tags: Vec, - /// Prediction error / surprise of this event (0..1). - pub prediction_error: f32, - /// True if a caller explicitly marked this salient (manual override path). - pub manual: bool, -} - -/// Pull shared-entity join keys from content + tags (single source of truth used -/// by the MCP tool, CLI, and offline pass so they never diverge). -pub fn extract_entities(content: &str, tags: &[String]) -> Vec { - use std::collections::HashSet; - let mut set: HashSet = tags.iter().map(|t| t.to_lowercase()).collect(); - for raw in content.split(|c: char| { - !(c.is_alphanumeric() || c == '_' || c == '.' || c == '/' || c == '-') - }) { - let tok = raw.trim_matches(|c: char| c == '.' || c == '/' || c == '-'); - if tok.len() < 3 { - continue; - } - let is_env = tok.len() >= 3 - && tok.chars().all(|c| c.is_ascii_uppercase() || c == '_' || c.is_ascii_digit()) - && tok.chars().any(|c| c.is_ascii_uppercase()); - let is_path = (tok.contains('/') || tok.contains('.')) - && tok.chars().any(|c| c.is_ascii_alphabetic()); - if is_env || is_path { - set.insert(tok.to_lowercase()); - } - } - set.into_iter().collect() -} - -/// Whole-word marker match: `marker` must appear bounded by non-alphanumeric -/// chars, not embedded in a larger identifier. This is the difference between -/// matching "timeout" in "a request timeout" (a real failure) and NOT matching it -/// inside the config var `API_TIMEOUT` (a perfectly ordinary env var). Plain -/// substring over-fires: "timeout" hits "API_TIMEOUT", "leak" hits "leaky", "500" -/// hits "$500" — which wrongly flags a quiet CAUSE as a failure and excludes it -/// from the backward reach. -fn contains_marker_word(hay: &str, marker: &str) -> bool { - let mut from = 0usize; - while let Some(pos) = hay[from..].find(marker) { - let start = from + pos; - let end = start + marker.len(); - // Inspect the actual char before/after the match, not a raw byte cast to - // char: for a multibyte UTF-8 boundary the raw byte is a continuation - // byte (0x80-0xBF), which `as char` misreads as a non-alphanumeric and - // wrongly passes the word-boundary check. char iteration is boundary-safe. - let before_ok = hay[..start] - .chars() - .next_back() - .is_none_or(|c| !(c.is_alphanumeric() || c == '_')); - let after_ok = hay[end..] - .chars() - .next() - .is_none_or(|c| !(c.is_alphanumeric() || c == '_')); - if before_ok && after_ok { - return true; - } - from = start + 1; - } - false -} - -/// Does this content/tags pair read like a failure? Whole-word marker match over -/// content + tags (see [`contains_marker_word`] for why whole-word, not substring). -/// Shared by every caller so failure detection never drifts. -pub fn looks_like_failure(content: &str, tags: &[String]) -> bool { - let hay = content.to_lowercase(); - if FAILURE_MARKERS.iter().any(|m| contains_marker_word(&hay, m)) { - return true; - } - tags.iter().any(|t| { - let tl = t.to_lowercase(); - FAILURE_MARKERS.iter().any(|m| contains_marker_word(&tl, m)) - }) -} - -impl FailureEvent { - /// Auto-detection: is this memory a salient "aversive event"? True when it - /// is sufficiently surprising AND carries a failure marker — or when a caller - /// manually flagged it. (The "both" trigger: auto-detect + manual override.) - pub fn is_salient(&self, salience_threshold: f32) -> bool { - if self.manual { - return true; - } - if self.prediction_error < salience_threshold { - return false; - } - looks_like_failure(&self.content, &self.tags) - } -} - -// ============================================================================ -// OUTPUT TYPES -// ============================================================================ - -/// One promoted memory: a quiet earlier cause the failure reached back to. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct BackfilledCause { - pub memory_id: String, - /// The entities it shares with the failure (the causal join). - pub shared_entities: Vec, - /// Days before the failure this memory was formed. - pub age_days: f64, - /// Backfill score (higher = stronger candidate cause). - pub score: f64, - /// New stability after promotion (= old * factor, capped). - pub promoted_stability: f64, - /// Its similarity rank position among candidates by similarity (1 = most - /// similar). A high number here is the proof: the cause is NOT what a - /// similarity search would have surfaced. - pub similarity_rank: Option, - /// Human-readable why. - pub reason: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BackfillResult { - pub triggered: bool, - pub failure_id: String, - pub causes: Vec, - pub scanned: usize, -} - -// ============================================================================ -// THE BACKFILL -// ============================================================================ - -#[derive(Debug, Clone)] -pub struct RetroactiveBackfill { - pub salience_threshold: f32, - pub lookback_days: i64, - pub min_shared_entities: usize, - pub max_causes: usize, -} - -impl Default for RetroactiveBackfill { - fn default() -> Self { - Self { - salience_threshold: DEFAULT_SALIENCE_THRESHOLD, - lookback_days: DEFAULT_LOOKBACK_DAYS, - min_shared_entities: MIN_SHARED_ENTITIES, - max_causes: 3, - } - } -} - -impl RetroactiveBackfill { - pub fn new() -> Self { - Self::default() - } - - /// Run the backward reach. Given a (possibly salient) failure and the pool of - /// earlier candidate memories, return which past memories to promote and why. - /// - /// Backward-only by construction: candidates with `age_days_before_failure` - /// <= 0 (i.e. concurrent or future) are never considered. - pub fn run(&self, failure: &FailureEvent, candidates: &[BackfillCandidate]) -> BackfillResult { - if !failure.is_salient(self.salience_threshold) { - return BackfillResult { - triggered: false, - failure_id: failure.id.clone(), - causes: vec![], - scanned: 0, - }; - } - - let failure_entities: HashSet<&str> = - failure.entities.iter().map(|s| s.as_str()).collect(); - - // similarity ranking (only to PROVE the cause ranks low on similarity) - let mut by_sim: Vec<(&str, f32)> = candidates - .iter() - .filter_map(|c| c.similarity_to_failure.map(|s| (c.id.as_str(), s))) - .collect(); - by_sim.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); - let sim_rank = |id: &str| -> Option { - by_sim.iter().position(|(cid, _)| *cid == id).map(|p| p + 1) - }; - - let mut scored: Vec = candidates - .iter() - // backward-only: must be strictly in the past, within lookback - .filter(|c| { - c.age_days_before_failure > 0.0 - && c.age_days_before_failure <= self.lookback_days as f64 - }) - .filter_map(|c| { - let shared: Vec = c - .entities - .iter() - .filter(|e| failure_entities.contains(e.as_str())) - .cloned() - .collect(); - if shared.len() < self.min_shared_entities { - return None; - } - let score = self.score(c, shared.len()); - let promoted = (c.stability * PROMOTION_STABILITY_FACTOR).min(c.stability + 365.0); - let rank = sim_rank(&c.id); - let reason = format!( - "Reached back {:.1}d to a quiet memory sharing {} entit{} ({}) with the failure; \ - it ranked {} on similarity, so semantic search would have missed it.", - c.age_days_before_failure, - shared.len(), - if shared.len() == 1 { "y" } else { "ies" }, - shared.join(", "), - rank.map(|r| format!("#{r}")).unwrap_or_else(|| "untracked".into()), - ); - Some(BackfilledCause { - memory_id: c.id.clone(), - shared_entities: shared, - age_days: c.age_days_before_failure, - score, - promoted_stability: promoted, - similarity_rank: rank, - reason, - }) - }) - .collect(); - - scored.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal)); - scored.truncate(self.max_causes); - - BackfillResult { - triggered: true, - failure_id: failure.id.clone(), - causes: scored, - scanned: candidates.len(), - } - } - - /// Score a candidate cause. More shared entities = stronger causal join. - /// Recency among the past matters a little (a change yesterday is a more - /// likely cause than one a month ago) but is deliberately a *weak* term so - /// genuinely old causes still surface — the opposite of recency-only ranking. - /// LOW similarity is rewarded slightly: a cause that is dissimilar to the - /// failure is exactly the one RAG cannot find, so it is the most valuable - /// to backfill. - fn score(&self, c: &BackfillCandidate, shared: usize) -> f64 { - let entity_term = shared as f64; // dominant signal - // gentle recency-in-the-past: 1.0 at the failure, fading with age - let recency_term = - 0.3 * (1.0 / (1.0 + c.age_days_before_failure / self.lookback_days as f64)); - // dissimilarity bonus: the less similar, the more "RAG would miss it" - let dissim_term = c - .similarity_to_failure - .map(|s| 0.5 * (1.0 - s as f64).max(0.0)) - .unwrap_or(0.0); - entity_term + recency_term + dissim_term - } -} - -// ============================================================================ -// TESTS — the receipt: plant a cause, inject a failure, assert backfill finds -// the cause that a similarity search ranks near the bottom. -// ============================================================================ - -#[cfg(test)] -mod tests { - use super::*; - - fn failure() -> FailureEvent { - FailureEvent { - id: "fail-wed".into(), - content: "Service crashed: 500 Internal Server Error on the auth endpoint".into(), - entities: vec!["auth-service".into(), "API_TIMEOUT".into()], - tags: vec![], - prediction_error: 0.9, - manual: false, - } - } - - /// The headline scenario: a quiet env-var change days ago caused a crash now. - /// Semantic search ranks it LAST (it's not similar to "crash"); backfill - /// promotes it because it shares the API_TIMEOUT entity, backward in time. - #[test] - fn backfill_surfaces_the_cause_rag_misses() { - let candidates = vec![ - // the actual cause: a quiet config note from 3 days ago. Low similarity. - BackfillCandidate { - id: "cause-mon".into(), - content: "Set API_TIMEOUT=2 in the deploy env to speed up cold starts".into(), - entities: vec!["API_TIMEOUT".into(), "deploy-env".into()], - age_days_before_failure: 3.0, - stability: 5.0, - similarity_to_failure: Some(0.11), // dissimilar — RAG would miss it - }, - // a noisy distractor: semantically similar to the crash, but NOT causal - // (shares no entity with the failure). - BackfillCandidate { - id: "noise-similar".into(), - content: "Another 500 error happened in the billing service last month".into(), - entities: vec!["billing-service".into()], - age_days_before_failure: 20.0, - stability: 3.0, - similarity_to_failure: Some(0.82), // similar — RAG WOULD surface this - }, - // a future memory — must never be backfilled (backward-only). - BackfillCandidate { - id: "future".into(), - content: "Plan to add API_TIMEOUT retries next sprint".into(), - entities: vec!["API_TIMEOUT".into()], - age_days_before_failure: -1.0, - stability: 2.0, - similarity_to_failure: Some(0.4), - }, - ]; - - let result = RetroactiveBackfill::new().run(&failure(), &candidates); - - assert!(result.triggered, "high-PE failure with markers must trigger"); - assert!(!result.causes.is_empty(), "must surface at least one cause"); - - let top = &result.causes[0]; - // the promoted memory is the real cause, not the similar distractor - assert_eq!(top.memory_id, "cause-mon", "must promote the causal env-var note"); - assert!(top.shared_entities.contains(&"API_TIMEOUT".to_string())); - // and it is provably NOT what similarity search would have surfaced: - assert!( - top.similarity_rank.unwrap() > 1, - "the cause must rank below the similar distractor on similarity (that's the point)" - ); - // backward-only: the future memory is never promoted - assert!( - !result.causes.iter().any(|c| c.memory_id == "future"), - "backward-only: a future memory must never be backfilled" - ); - // it gets a real stability boost (stops decaying, will surface next time) - assert!(top.promoted_stability > 5.0, "the cause must be promoted (boosted stability)"); - } - - #[test] - fn non_salient_event_does_not_trigger() { - let calm = FailureEvent { - id: "calm".into(), - content: "Refactored the logging format for readability".into(), - entities: vec!["logger".into()], - tags: vec![], - prediction_error: 0.2, // low surprise - manual: false, - }; - let result = RetroactiveBackfill::new().run(&calm, &[]); - assert!(!result.triggered, "a calm, low-surprise note must not fire a backfill"); - } - - #[test] - fn manual_override_triggers_without_markers() { - // No failure word, low PE — but the caller explicitly marked it salient. - let manual = FailureEvent { - id: "manual".into(), - content: "Latency crept up on the checkout path".into(), - entities: vec!["checkout".into()], - tags: vec![], - prediction_error: 0.1, - manual: true, - }; - let candidates = vec![BackfillCandidate { - id: "cause".into(), - content: "Disabled the checkout cache while debugging".into(), - entities: vec!["checkout".into()], - age_days_before_failure: 2.0, - stability: 4.0, - similarity_to_failure: Some(0.3), - }]; - let result = RetroactiveBackfill::new().run(&manual, &candidates); - assert!(result.triggered, "manual override must trigger regardless of markers/PE"); - assert_eq!(result.causes[0].memory_id, "cause"); - } - - #[test] - fn requires_a_shared_entity_no_spurious_links() { - // A salient failure but the only past memory shares NO entity — we must - // NOT invent a causal link (avoids the A-B,B-C spurious-edge failure mode). - let candidates = vec![BackfillCandidate { - id: "unrelated".into(), - content: "Updated the README badges".into(), - entities: vec!["README".into()], - age_days_before_failure: 1.0, - stability: 4.0, - similarity_to_failure: Some(0.05), - }]; - let result = RetroactiveBackfill::new().run(&failure(), &candidates); - assert!(result.triggered); - assert!( - result.causes.is_empty(), - "no shared entity => no backfill (don't fabricate a cause)" - ); - } -} diff --git a/crates/vestige-core/src/advanced/speculative.rs b/crates/vestige-core/src/advanced/speculative.rs index 3adaa41..eebe947 100644 --- a/crates/vestige-core/src/advanced/speculative.rs +++ b/crates/vestige-core/src/advanced/speculative.rs @@ -268,23 +268,13 @@ impl SpeculativeRetriever { } } - // Update file-memory associations. Dedupe and cap per file so the map - // cannot grow without bound in a long-running server (mirrors the - // MAX_PATTERN_HISTORY trim on access_sequence above). + // Update file-memory associations if let Some(file) = file_context && let Ok(mut map) = self.file_memory_map.write() { - let ids = map.entry(file.to_string()).or_insert_with(Vec::new); - let id = memory_id.to_string(); - if !ids.contains(&id) { - ids.push(id); - // keep only the most recent N associations per file - const MAX_FILE_MEMORIES: usize = 256; - if ids.len() > MAX_FILE_MEMORIES { - let excess = ids.len() - MAX_FILE_MEMORIES; - ids.drain(0..excess); - } - } + map.entry(file.to_string()) + .or_insert_with(Vec::new) + .push(memory_id.to_string()); } } @@ -432,12 +422,7 @@ impl SpeculativeRetriever { let mut time_counts: HashMap = HashMap::new(); for event in sequence.iter() { - // Circular hour distance so the ±1h window wraps around midnight: - // 23:00 is 1 hour from 00:00, not 23. Without this, same-time - // predictions straddling midnight were silently dropped. - let raw = (event.timestamp.hour() as i32 - hour as i32).abs(); - let circular = raw.min(24 - raw); - if circular <= 1 { + if (event.timestamp.hour() as i32 - hour as i32).abs() <= 1 { *time_counts.entry(event.memory_id.clone()).or_insert(0) += 1; } } @@ -481,11 +466,7 @@ impl SpeculativeRetriever { fn store_pending_predictions(&self, predictions: &[PredictedMemory]) { if let Ok(mut pending) = self.pending_predictions.write() { - // Merge (do NOT clear): two predict() calls without an intervening - // record_usage() would otherwise wipe the first batch's pending - // entries, destroying the right/wrong accounting record_usage relies - // on. Newer predictions overwrite same-id entries; older survive - // until consumed. + pending.clear(); for pred in predictions { pending.insert(pred.memory_id.clone(), pred.clone()); } diff --git a/crates/vestige-core/src/codebase/relationships.rs b/crates/vestige-core/src/codebase/relationships.rs index 21ac580..ce7a1d2 100644 --- a/crates/vestige-core/src/codebase/relationships.rs +++ b/crates/vestige-core/src/codebase/relationships.rs @@ -176,15 +176,6 @@ impl RelationshipTracker { let id = relationship.id.clone(); - // Reject duplicate ids: a second insert with the same id but different - // files would overwrite the relationship while leaving the stale id in - // each previous file's index, corrupting get_related_files. - if self.relationships.contains_key(&id) { - return Err(RelationshipError::Invalid(format!( - "duplicate relationship id: {id}" - ))); - } - // Index by each file for file in &relationship.files { self.file_relationships @@ -567,16 +558,6 @@ impl RelationshipTracker { /// Load relationships from storage pub fn load_relationships(&mut self, relationships: Vec) -> Result<()> { for relationship in relationships { - // Advance next_id past any loaded "rel-N" id so a later new_id() can't - // collide with a persisted one (next_id starts at 1 and is otherwise - // never reconciled with loaded data). - if let Some(n) = relationship - .id - .strip_prefix("rel-") - .and_then(|s| s.parse::().ok()) - { - self.next_id = self.next_id.max(n + 1); - } self.add_relationship(relationship)?; } Ok(()) diff --git a/crates/vestige-core/src/config.rs b/crates/vestige-core/src/config.rs deleted file mode 100644 index c55e19b..0000000 --- a/crates/vestige-core/src/config.rs +++ /dev/null @@ -1,386 +0,0 @@ -//! Vestige configuration file (`vestige.toml`). -//! -//! Phase 2 "Configurable Output" of the adoption roadmap. A small, optional -//! config file lives alongside the SQLite database in the active Vestige data -//! directory (`/vestige.toml`). It lets users tune the default shape -//! of high-traffic MCP responses (detail level, result limit, output profile) -//! without recompiling and without a cloud service. -//! -//! Precedence, from highest to lowest: -//! -//! 1. An explicit MCP call parameter (e.g. `detail_level` on a `search` call). -//! 2. The config file `[defaults]` (and the selected output profile). -//! 3. The built-in default, which preserves the historical behavior so nothing -//! changes for users who never write a `vestige.toml`. -//! -//! The parser is intentionally a tiny, dependency-free subset of TOML: section -//! headers (`[defaults]`) and `key = value` lines with string or integer -//! values. This keeps the local-first binary lean and avoids pulling a full -//! TOML crate into the dependency tree for a three-key schema. Unknown keys and -//! unknown sections are ignored so the file can grow in future phases without -//! breaking older binaries. - -use std::path::{Path, PathBuf}; - -/// Canonical config file name, resolved inside the active data directory. -pub const CONFIG_FILE: &str = "vestige.toml"; - -/// Output profiles preset a coherent bundle of detail/field choices. -/// -/// `Default` MUST reproduce the pre-Phase-2 behavior exactly so existing users -/// see no change. The other profiles are opt-in presets. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum OutputProfile { - /// Smallest responses: brief detail, scores and timestamps suppressed. - /// Use when context budget matters more than provenance. - Lean, - /// Historical behavior. `summary` detail with content + dates. Unchanged. - #[default] - Default, - /// Maximum provenance: `full` detail with every field, score, and timestamp. - /// Use when reviewing or debugging memory state. - Audit, - /// Like `audit` but tuned for larger result sets (higher default limit). - Research, -} - -impl OutputProfile { - /// Parse a profile name. Returns `None` for unknown names so the caller can - /// decide whether that is an error (MCP param) or ignorable (config file). - pub fn from_name(name: &str) -> Option { - match name.trim().to_ascii_lowercase().as_str() { - "lean" => Some(Self::Lean), - "default" => Some(Self::Default), - "audit" => Some(Self::Audit), - "research" => Some(Self::Research), - _ => None, - } - } - - /// Canonical lowercase name, suitable for echoing back in responses. - pub fn as_str(self) -> &'static str { - match self { - Self::Lean => "lean", - Self::Default => "default", - Self::Audit => "audit", - Self::Research => "research", - } - } - - /// The detail level this profile presets when the user has not set one - /// explicitly via an MCP param or `[defaults] detail_level`. - pub fn detail_level(self) -> &'static str { - match self { - Self::Lean => "brief", - Self::Default => "summary", - Self::Audit | Self::Research => "full", - } - } - - /// The result limit this profile presets when the user has not set one - /// explicitly. `None` means "use the tool's own historical default", which - /// keeps `default` fully backward-compatible. - pub fn limit(self) -> Option { - match self { - Self::Lean => Some(5), - Self::Default => None, - Self::Audit => None, - Self::Research => Some(25), - } - } - - /// Whether scores (combined/keyword/semantic) should be shown by default. - /// Lean drops them to save tokens; the rest keep whatever the detail level - /// already includes. - pub fn show_scores(self) -> bool { - !matches!(self, Self::Lean) - } - - /// Whether timestamps should be shown by default. Lean drops them. - pub fn show_timestamps(self) -> bool { - !matches!(self, Self::Lean) - } -} - -/// The `[defaults]` table from `vestige.toml`. All fields optional. -#[derive(Debug, Clone, Default, PartialEq)] -pub struct OutputDefaults { - /// Default detail level (`brief` | `summary` | `full`). Overrides the - /// profile's preset detail level when set. - pub detail_level: Option, - /// Default result limit for high-traffic tools. Overrides the profile's - /// preset limit when set. - pub limit: Option, - /// Selected output profile. Defaults to `default` (historical behavior). - pub profile: OutputProfile, -} - -/// Parsed `vestige.toml`. Currently only the `[defaults]` table is meaningful; -/// the struct exists so future phases can add tables without churn. -#[derive(Debug, Clone, Default, PartialEq)] -pub struct VestigeConfig { - pub defaults: OutputDefaults, -} - -impl VestigeConfig { - /// Resolve the config path for a given data directory. - pub fn path_for_data_dir(data_dir: &Path) -> PathBuf { - data_dir.join(CONFIG_FILE) - } - - /// Load config from a data directory. A missing or unreadable file yields - /// the built-in default (never an error) so a fresh install just works. - /// A present-but-malformed file is parsed leniently: only well-formed lines - /// are honored. - pub fn load_from_data_dir(data_dir: &Path) -> Self { - let path = Self::path_for_data_dir(data_dir); - match std::fs::read_to_string(&path) { - Ok(contents) => Self::parse(&contents), - Err(_) => Self::default(), - } - } - - /// Parse the minimal TOML subset. Lenient by design. - pub fn parse(contents: &str) -> Self { - let mut config = Self::default(); - let mut section = String::new(); - - for raw in contents.lines() { - let line = strip_comment(raw).trim(); - if line.is_empty() { - continue; - } - - if let Some(name) = line.strip_prefix('[').and_then(|s| s.strip_suffix(']')) { - section = name.trim().to_ascii_lowercase(); - continue; - } - - let Some((key, value)) = line.split_once('=') else { - continue; - }; - let key = key.trim().to_ascii_lowercase(); - let value = unquote(value.trim()); - - if section == "defaults" { - match key.as_str() { - "detail_level" => { - let v = value.trim().to_ascii_lowercase(); - if matches!(v.as_str(), "brief" | "summary" | "full") { - config.defaults.detail_level = Some(v); - } - } - "limit" => { - if let Ok(n) = value.trim().parse::() - && n > 0 - { - config.defaults.limit = Some(n); - } - } - "profile" => { - if let Some(p) = OutputProfile::from_name(&value) { - config.defaults.profile = p; - } - } - _ => {} - } - } - } - - config - } - - /// Effective output config after applying the profile, with `[defaults]` - /// detail_level / limit overriding the profile presets. - pub fn output(&self) -> OutputConfig { - let profile = self.defaults.profile; - OutputConfig { - profile, - detail_level: self - .defaults - .detail_level - .clone() - .unwrap_or_else(|| profile.detail_level().to_string()), - limit: self.defaults.limit.or_else(|| profile.limit()), - show_scores: profile.show_scores(), - show_timestamps: profile.show_timestamps(), - } - } -} - -/// The resolved, ready-to-apply output configuration handed to MCP tools. -/// -/// Tools treat each field as the *fallback* used only when the corresponding -/// explicit MCP call parameter is absent, preserving the precedence -/// `MCP param > config file > built-in default`. -#[derive(Debug, Clone, PartialEq)] -pub struct OutputConfig { - pub profile: OutputProfile, - pub detail_level: String, - pub limit: Option, - pub show_scores: bool, - pub show_timestamps: bool, -} - -impl Default for OutputConfig { - /// The built-in default == the historical behavior == the `default` profile. - fn default() -> Self { - VestigeConfig::default().output() - } -} - -impl OutputConfig { - /// Resolve the detail level to use, given an optional explicit MCP param. - /// Explicit param always wins (precedence layer 1). - pub fn resolve_detail_level(&self, explicit: Option<&str>) -> String { - explicit - .map(|s| s.to_string()) - .unwrap_or_else(|| self.detail_level.clone()) - } - - /// Resolve the limit to use, given an optional explicit MCP param and the - /// tool's own built-in fallback (used only when neither param nor config - /// supplies one). - pub fn resolve_limit(&self, explicit: Option, builtin_default: i32) -> i32 { - explicit.or(self.limit).unwrap_or(builtin_default) - } -} - -/// Strip a `#` comment that is not inside a quoted string. -fn strip_comment(line: &str) -> &str { - let mut in_quotes = false; - for (idx, ch) in line.char_indices() { - match ch { - '"' => in_quotes = !in_quotes, - '#' if !in_quotes => return &line[..idx], - _ => {} - } - } - line -} - -/// Remove a single layer of matching surrounding double quotes, if present. -fn unquote(value: &str) -> String { - let bytes = value.as_bytes(); - if bytes.len() >= 2 && bytes[0] == b'"' && bytes[bytes.len() - 1] == b'"' { - value[1..value.len() - 1].to_string() - } else { - value.to_string() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn default_preserves_historical_behavior() { - let out = OutputConfig::default(); - assert_eq!(out.profile, OutputProfile::Default); - assert_eq!(out.detail_level, "summary"); - assert_eq!(out.limit, None); - assert!(out.show_scores); - assert!(out.show_timestamps); - } - - #[test] - fn empty_or_missing_file_is_default() { - assert_eq!(VestigeConfig::parse(""), VestigeConfig::default()); - assert_eq!( - VestigeConfig::parse("\n\n# just a comment\n"), - VestigeConfig::default() - ); - } - - #[test] - fn parses_defaults_table() { - let cfg = VestigeConfig::parse( - r#" - [defaults] - detail_level = "full" - limit = 25 - profile = "research" - "#, - ); - assert_eq!(cfg.defaults.detail_level.as_deref(), Some("full")); - assert_eq!(cfg.defaults.limit, Some(25)); - assert_eq!(cfg.defaults.profile, OutputProfile::Research); - } - - #[test] - fn unquoted_and_commented_values() { - let cfg = VestigeConfig::parse("[defaults]\nprofile = lean # inline comment\nlimit = 7\n"); - assert_eq!(cfg.defaults.profile, OutputProfile::Lean); - assert_eq!(cfg.defaults.limit, Some(7)); - } - - #[test] - fn invalid_values_are_ignored() { - let cfg = VestigeConfig::parse( - "[defaults]\ndetail_level = \"loud\"\nlimit = -3\nprofile = \"galaxy\"\n", - ); - // All invalid -> fall back to defaults. - assert_eq!(cfg.defaults.detail_level, None); - assert_eq!(cfg.defaults.limit, None); - assert_eq!(cfg.defaults.profile, OutputProfile::Default); - } - - #[test] - fn unknown_sections_and_keys_ignored() { - let cfg = VestigeConfig::parse( - "[future_phase]\nfoo = 1\n[defaults]\nprofile = audit\nbar = baz\n", - ); - assert_eq!(cfg.defaults.profile, OutputProfile::Audit); - } - - #[test] - fn profile_presets() { - // lean: brief + dropped scores/timestamps + small limit - let lean = VestigeConfig::parse("[defaults]\nprofile=lean").output(); - assert_eq!(lean.detail_level, "brief"); - assert_eq!(lean.limit, Some(5)); - assert!(!lean.show_scores); - assert!(!lean.show_timestamps); - - // audit: full detail, no forced limit - let audit = VestigeConfig::parse("[defaults]\nprofile=audit").output(); - assert_eq!(audit.detail_level, "full"); - assert_eq!(audit.limit, None); - - // research: full detail, larger limit - let research = VestigeConfig::parse("[defaults]\nprofile=research").output(); - assert_eq!(research.detail_level, "full"); - assert_eq!(research.limit, Some(25)); - } - - #[test] - fn explicit_defaults_override_profile_presets() { - // profile=lean would give brief/limit 5, but explicit keys win. - let out = - VestigeConfig::parse("[defaults]\nprofile=lean\ndetail_level=\"full\"\nlimit=42\n") - .output(); - assert_eq!(out.detail_level, "full"); - assert_eq!(out.limit, Some(42)); - } - - #[test] - fn precedence_mcp_param_wins() { - let out = VestigeConfig::parse("[defaults]\nprofile=lean").output(); - // Config says brief, but an explicit MCP param wins. - assert_eq!(out.resolve_detail_level(Some("full")), "full"); - // No explicit param -> config (lean -> brief). - assert_eq!(out.resolve_detail_level(None), "brief"); - } - - #[test] - fn precedence_limit_layers() { - let out = VestigeConfig::parse("[defaults]\nprofile=research").output(); - // explicit param wins over everything - assert_eq!(out.resolve_limit(Some(3), 10), 3); - // no param -> config (research -> 25) - assert_eq!(out.resolve_limit(None, 10), 25); - // default profile has no limit -> builtin fallback used - let def = OutputConfig::default(); - assert_eq!(def.resolve_limit(None, 10), 10); - } -} diff --git a/crates/vestige-core/src/connectors/github.rs b/crates/vestige-core/src/connectors/github.rs deleted file mode 100644 index 3d6c23c..0000000 --- a/crates/vestige-core/src/connectors/github.rs +++ /dev/null @@ -1,610 +0,0 @@ -//! GitHub Issues connector (#57). -//! -//! Indexes a repository's issues + comments into source-aware Vestige memories -//! so an agent can search and reason over the full issue history **offline**, -//! **semantically**, and **cited back to the canonical issue URL**. Unlike the -//! official GitHub MCP server — a stateless live API proxy — this builds a -//! durable, embedded, temporally-versioned local index. -//! -//! ## Incremental sync (per the connector sync contract) -//! -//! - `state=all` so closing an issue is not mistaken for a deletion. -//! - `sort=updated&direction=asc` so we page forward in cursor order and a -//! mid-run interruption resumes safely. -//! - `since=` filters on `updated_at`; the overlap + the -//! `content_hash` no-op makes re-scans safe and cheap. -//! - `Link: rel="next"` drives pagination (never hand-built page urls). -//! - Entries carrying a `pull_request` key are dropped (PRs are not issues). -//! - Per issue we fold the body + comments into one memory; the hash covers -//! the stable fields only (title, body, state, labels, comments) — never the -//! cursor timestamp or volatile counts. -//! -//! GitHub has no deletion feed, so deletions are reconciled out-of-band via -//! [`list_live_ids`](Connector::list_live_ids). - -use chrono::{DateTime, Utc}; -use serde::Deserialize; - -use super::{ - Connector, ConnectorError, ConnectorResult, FetchPage, NormalizedRecord, content_hash, -}; -use crate::memory::SourceEnvelope; - -const API_ROOT: &str = "https://api.github.com"; -const USER_AGENT: &str = concat!("vestige-connector/", env!("CARGO_PKG_VERSION")); -const PER_PAGE: u32 = 100; - -/// Configuration for a GitHub Issues connector instance. -#[derive(Clone)] -pub struct GithubConfig { - /// Repository owner (user or org). - pub owner: String, - /// Repository name. - pub repo: String, - /// Personal access token. Optional for public repos (60 req/hr - /// unauthenticated) but strongly recommended (5000 req/hr authenticated). - pub token: Option, - /// Override the API root (for GitHub Enterprise or tests). - pub api_root: Option, - /// Max comments to fold into one issue memory (defense against huge threads). - pub max_comments: usize, -} - -// Manual Debug that NEVER prints the token — a derived Debug would leak the -// bearer credential into any `{:?}` log line or panic message. -impl std::fmt::Debug for GithubConfig { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("GithubConfig") - .field("owner", &self.owner) - .field("repo", &self.repo) - .field("token", &self.token.as_ref().map(|_| "")) - .field("api_root", &self.api_root) - .field("max_comments", &self.max_comments) - .finish() - } -} - -impl GithubConfig { - pub fn new(owner: impl Into, repo: impl Into) -> Self { - Self { - owner: owner.into(), - repo: repo.into(), - token: None, - api_root: None, - max_comments: 50, - } - } - - pub fn with_token(mut self, token: Option) -> Self { - self.token = token; - self - } - - fn scope(&self) -> String { - format!("{}/{}", self.owner, self.repo) - } - - fn root(&self) -> &str { - self.api_root.as_deref().unwrap_or(API_ROOT) - } -} - -/// A GitHub Issues connector bound to one repository. -pub struct GithubConnector { - config: GithubConfig, - scope: String, - client: reqwest::Client, -} - -impl GithubConnector { - pub fn new(config: GithubConfig) -> ConnectorResult { - if config.owner.is_empty() || config.repo.is_empty() { - return Err(ConnectorError::Config( - "owner and repo are required".to_string(), - )); - } - // owner/repo are interpolated raw into request URLs; restrict them to - // GitHub's actual charset so `/`, `%`, `?`, `#`, traversal sequences, etc. - // cannot break out of the path or redirect the request. - let valid = |s: &str| { - s.chars() - .all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.')) - }; - if !valid(&config.owner) || !valid(&config.repo) { - return Err(ConnectorError::Config( - "owner/repo may only contain [A-Za-z0-9._-]".to_string(), - )); - } - let client = reqwest::Client::builder() - .user_agent(USER_AGENT) - .build() - .map_err(|e| ConnectorError::Transport(e.to_string()))?; - let scope = config.scope(); - Ok(Self { - config, - scope, - client, - }) - } - - fn auth(&self, req: reqwest::RequestBuilder) -> reqwest::RequestBuilder { - let req = req - .header("Accept", "application/vnd.github+json") - .header("X-GitHub-Api-Version", "2022-11-28"); - match &self.config.token { - Some(t) => req.bearer_auth(t), - None => req, - } - } - - /// Map an HTTP response status into a connector error, honoring rate-limit - /// signals so the driver can back off politely. - fn classify_status(resp: &reqwest::Response) -> Option { - let status = resp.status(); - if status.is_success() { - return None; - } - // Primary rate limit: 403/429 with remaining=0. - if status.as_u16() == 403 || status.as_u16() == 429 { - let remaining = resp - .headers() - .get("x-ratelimit-remaining") - .and_then(|v| v.to_str().ok()) - .and_then(|s| s.parse::().ok()); - if remaining == Some(0) || status.as_u16() == 429 { - let retry = resp - .headers() - .get("retry-after") - .and_then(|v| v.to_str().ok()) - .and_then(|s| s.parse::().ok()) - .map(std::time::Duration::from_secs); - return Some(ConnectorError::RateLimited(retry)); - } - } - Some(ConnectorError::Source { - status: status.as_u16(), - message: status - .canonical_reason() - .unwrap_or("request failed") - .to_string(), - }) - } - - /// Parse the `Link` header for the `rel="next"` url, if any. - /// - /// The `next` url comes from the server response, so we pin it to the - /// configured API host before following it: otherwise a malicious or - /// compromised endpoint could redirect the connector — which attaches the - /// bearer token to every request — to an attacker-controlled URL and - /// exfiltrate the credential (SSRF / token leak). `expected_host` is the - /// host of the connector's API root. - fn next_link(resp: &reqwest::Response, expected_host: Option<&str>) -> Option { - let link = resp.headers().get(reqwest::header::LINK)?.to_str().ok()?; - for part in link.split(',') { - let part = part.trim(); - if part.contains("rel=\"next\"") - && let (Some(start), Some(end)) = (part.find('<'), part.find('>')) - && start < end - { - let url = &part[start + 1..end]; - // Host-pin: only follow a next-url on the same host as the API - // root we were configured with. FAIL-CLOSED: if we could not - // determine the expected host (unparseable/hostless api_root), we - // must NOT follow the url — the bearer token would otherwise ride - // along to an attacker-influenced host (SSRF / token exfiltration). - let Some(expected) = expected_host else { - tracing::warn!( - next_url = url, - "dropping Link next url: no pinned host (fail-closed)" - ); - return None; - }; - match reqwest::Url::parse(url) { - Ok(parsed) if parsed.host_str() == Some(expected) => { - return Some(url.to_string()); - } - _ => { - tracing::warn!( - next_url = url, - "dropping cross-host Link next url (host pin)" - ); - return None; - } - } - } - } - None - } - - /// Host of the configured API root, used to pin Link `next` urls. - fn api_host(&self) -> Option { - reqwest::Url::parse(self.config.root()) - .ok() - .and_then(|u| u.host_str().map(|h| h.to_string())) - } - - /// Fetch the comments for one issue (a single page; capped by `max_comments`). - async fn fetch_comments(&self, issue_number: u64) -> ConnectorResult> { - let url = format!( - "{}/repos/{}/{}/issues/{}/comments?per_page={}", - self.config.root(), - self.config.owner, - self.config.repo, - issue_number, - self.config.max_comments.min(100), - ); - let resp = self - .auth(self.client.get(&url)) - .send() - .await - .map_err(|e| ConnectorError::Transport(e.to_string()))?; - if let Some(err) = Self::classify_status(&resp) { - return Err(err); - } - resp.json::>() - .await - .map_err(|e| ConnectorError::Transport(e.to_string())) - } - - /// Fold a raw issue + its comments into one normalized memory record. - fn normalize(&self, issue: &RawIssue, comments: &[RawComment]) -> NormalizedRecord { - let author = issue.user.as_ref().map(|u| u.login.clone()); - - // Human-readable content: header + body + chronological comments. - let mut content = format!( - "[{}#{}] {}\nState: {}\n", - self.scope, issue.number, issue.title, issue.state - ); - if let Some(body) = &issue.body - && !body.trim().is_empty() - { - content.push('\n'); - content.push_str(body.trim()); - content.push('\n'); - } - let mut sorted_comments: Vec<&RawComment> = comments.iter().collect(); - sorted_comments.sort_by_key(|c| c.id); - for c in &sorted_comments { - let who = c.user.as_ref().map(|u| u.login.as_str()).unwrap_or("?"); - content.push_str(&format!("\n— {who}: {}", c.body.trim())); - } - - // Labels, sorted for a stable hash. - let mut labels: Vec = issue.labels.iter().map(|l| l.name.clone()).collect(); - labels.sort(); - - // Stable content hash — meaning only, never the cursor timestamp or - // volatile counts. Comments contribute their id+body in id order. - let comments_blob = sorted_comments - .iter() - .map(|c| format!("{}:{}", c.id, c.body.trim())) - .collect::>() - .join("\u{1f}"); - let labels_blob = labels.join(","); - let number_str = issue.number.to_string(); - let body_str = issue.body.clone().unwrap_or_default(); - let hash = content_hash(&[ - ("number", &number_str), - ("title", &issue.title), - ("state", &issue.state), - ("body", &body_str), - ("labels", &labels_blob), - ("comments", &comments_blob), - ]); - - let mut tags = vec![ - "github".to_string(), - "issue".to_string(), - format!("state:{}", issue.state), - ]; - tags.extend(labels.into_iter().map(|l| format!("label:{l}"))); - - let envelope = SourceEnvelope { - source_system: Some("github".to_string()), - source_id: Some(issue.number.to_string()), - source_url: Some(issue.html_url.clone()), - source_updated_at: DateTime::parse_from_rfc3339(&issue.updated_at) - .ok() - .map(|d| d.with_timezone(&Utc)), - content_hash: Some(hash), - synced_at: Some(Utc::now()), - source_project: Some(self.scope.clone()), - source_type: Some("issue".to_string()), - source_author: author, - }; - - NormalizedRecord { - content, - tags, - envelope, - } - } -} - -impl Connector for GithubConnector { - fn source_system(&self) -> &str { - "github" - } - - fn scope(&self) -> &str { - &self.scope - } - - async fn fetch_updated( - &self, - since: Option>, - cursor: Option, - ) -> ConnectorResult { - // `cursor` is a full next-page url from a previous Link header; on the - // first page we build the url from owner/repo + since. - let url = match cursor { - Some(u) => u, - None => { - let mut u = format!( - "{}/repos/{}/{}/issues?state=all&sort=updated&direction=asc&per_page={}", - self.config.root(), - self.config.owner, - self.config.repo, - PER_PAGE, - ); - if let Some(s) = since { - // GitHub documents the `since` format as YYYY-MM-DDTHH:MM:SSZ. - // `to_rfc3339()` emits the `+00:00` offset form, and the `+` - // is a reserved query char that the server decodes as a - // space — corrupting the timestamp and silently re-fetching - // all history every run. Emit the `Z` form (no reserved - // char, exact documented format) instead. - let since_z = s.to_rfc3339_opts(chrono::SecondsFormat::Secs, true); - u.push_str(&format!("&since={since_z}")); - } - u - } - }; - - let resp = self - .auth(self.client.get(&url)) - .send() - .await - .map_err(|e| ConnectorError::Transport(e.to_string()))?; - if let Some(err) = Self::classify_status(&resp) { - return Err(err); - } - let next_cursor = Self::next_link(&resp, self.api_host().as_deref()); - let issues: Vec = resp - .json() - .await - .map_err(|e| ConnectorError::Transport(e.to_string()))?; - - let mut records = Vec::new(); - for issue in &issues { - // Drop pull requests — "every PR is an issue, but not vice versa". - if issue.pull_request.is_some() { - continue; - } - // Fetch comments only when the issue has any. Propagate failures - // instead of swallowing them: a silent unwrap_or_default() stored a - // comment-less record with a corrupted content hash AND let the - // cursor advance past it, so the issue would never be re-synced. A - // propagated error keeps the issue in the next window (cursor clamp). - let comments = if issue.comments > 0 { - self.fetch_comments(issue.number).await? - } else { - Vec::new() - }; - records.push(self.normalize(issue, &comments)); - } - - Ok(FetchPage { - records, - next_cursor, - }) - } - - async fn list_live_ids(&self) -> ConnectorResult>> { - // Enumerate all issue numbers (ids only) for the reconcile pass, paging - // via Link. Cheap relative to full sync (no comment fetch, no bodies). - let mut ids = Vec::new(); - let mut url = Some(format!( - "{}/repos/{}/{}/issues?state=all&per_page={}", - self.config.root(), - self.config.owner, - self.config.repo, - PER_PAGE, - )); - while let Some(u) = url { - let resp = self - .auth(self.client.get(&u)) - .send() - .await - .map_err(|e| ConnectorError::Transport(e.to_string()))?; - if let Some(err) = Self::classify_status(&resp) { - return Err(err); - } - let next = Self::next_link(&resp, self.api_host().as_deref()); - let issues: Vec = resp - .json() - .await - .map_err(|e| ConnectorError::Transport(e.to_string()))?; - for issue in issues { - if issue.pull_request.is_none() { - ids.push(issue.number.to_string()); - } - } - url = next; - } - Ok(Some(ids)) - } -} - -// --------------------------------------------------------------------------- -// Raw GitHub API shapes (only the fields we use) -// --------------------------------------------------------------------------- - -#[derive(Debug, Deserialize)] -struct RawIssue { - number: u64, - title: String, - #[serde(default)] - body: Option, - state: String, - html_url: String, - updated_at: String, - #[serde(default)] - comments: u64, - #[serde(default)] - labels: Vec, - #[serde(default)] - user: Option, - /// Present iff this "issue" is actually a pull request. - #[serde(default)] - pull_request: Option, -} - -#[derive(Debug, Deserialize)] -struct RawLabel { - name: String, -} - -#[derive(Debug, Deserialize)] -struct RawUser { - login: String, -} - -#[derive(Debug, Deserialize)] -struct RawComment { - id: u64, - body: String, - #[serde(default)] - user: Option, -} - -#[cfg(test)] -mod tests { - use super::*; - - fn issue(number: u64, title: &str, body: &str, state: &str) -> RawIssue { - RawIssue { - number, - title: title.to_string(), - body: Some(body.to_string()), - state: state.to_string(), - html_url: format!("https://github.com/o/r/issues/{number}"), - updated_at: "2026-06-19T00:00:00Z".to_string(), - comments: 0, - labels: vec![RawLabel { - name: "bug".to_string(), - }], - user: Some(RawUser { - login: "octocat".to_string(), - }), - pull_request: None, - } - } - - fn connector() -> GithubConnector { - GithubConnector::new(GithubConfig::new("o", "r")).unwrap() - } - - #[test] - fn normalize_builds_keyed_envelope_with_citation() { - let c = connector(); - let rec = c.normalize(&issue(57, "Connectors", "Add Redmine", "open"), &[]); - let env = &rec.envelope; - assert!(env.has_key()); - assert_eq!(env.source_system.as_deref(), Some("github")); - assert_eq!(env.source_id.as_deref(), Some("57")); - assert_eq!( - env.source_url.as_deref(), - Some("https://github.com/o/r/issues/57") - ); - assert_eq!(env.source_project.as_deref(), Some("o/r")); - assert!(rec.content.contains("Connectors")); - assert!(rec.tags.contains(&"state:open".to_string())); - assert!(rec.tags.contains(&"label:bug".to_string())); - } - - #[test] - fn hash_stable_across_label_order_and_changes_on_edit() { - let c = connector(); - let mut a = issue(1, "T", "body", "open"); - a.labels = vec![RawLabel { name: "b".into() }, RawLabel { name: "a".into() }]; - let mut b = issue(1, "T", "body", "open"); - b.labels = vec![RawLabel { name: "a".into() }, RawLabel { name: "b".into() }]; - let ha = c.normalize(&a, &[]).envelope.content_hash; - let hb = c.normalize(&b, &[]).envelope.content_hash; - assert_eq!(ha, hb, "label order must not change the hash"); - - // Editing the body must change the hash. - let edited = c - .normalize(&issue(1, "T", "EDITED", "open"), &[]) - .envelope - .content_hash; - assert_ne!(ha, edited); - - // Closing the issue changes state → changes the hash (not a no-op). - let closed = c - .normalize(&issue(1, "T", "body", "closed"), &[]) - .envelope - .content_hash; - assert_ne!(ha, closed); - } - - #[test] - fn comments_fold_in_id_order_and_affect_hash() { - let c = connector(); - let comments = vec![ - RawComment { - id: 2, - body: "second".into(), - user: Some(RawUser { login: "x".into() }), - }, - RawComment { - id: 1, - body: "first".into(), - user: Some(RawUser { login: "y".into() }), - }, - ]; - let rec = c.normalize(&issue(1, "T", "body", "open"), &comments); - // Folded in id order regardless of input order. - let first_pos = rec.content.find("first").unwrap(); - let second_pos = rec.content.find("second").unwrap(); - assert!(first_pos < second_pos, "comments must fold in id order"); - - let no_comments = c - .normalize(&issue(1, "T", "body", "open"), &[]) - .envelope - .content_hash; - assert_ne!( - rec.envelope.content_hash, no_comments, - "comments must contribute to the hash" - ); - } - - #[test] - fn rejects_empty_owner_repo() { - assert!(GithubConnector::new(GithubConfig::new("", "r")).is_err()); - assert!(GithubConnector::new(GithubConfig::new("o", "")).is_err()); - } - - #[test] - fn since_uses_z_form_not_plus_offset() { - // Regression: to_rfc3339() emits `+00:00`; the `+` decodes to a space - // server-side and corrupts the cursor. We must emit the `Z` form. - let ts = DateTime::parse_from_rfc3339("2026-06-19T00:00:00Z") - .unwrap() - .with_timezone(&Utc); - let z = ts.to_rfc3339_opts(chrono::SecondsFormat::Secs, true); - assert_eq!(z, "2026-06-19T00:00:00Z"); - assert!(!z.contains('+'), "since must not contain a reserved '+'"); - } - - #[test] - fn next_link_host_pin_drops_cross_host_url() { - // The host-pin parsing logic (used to prevent token exfiltration via a - // malicious Link header) must reject a different host. - let same = reqwest::Url::parse("https://api.github.com/x?page=2").unwrap(); - let other = reqwest::Url::parse("https://evil.example/x?page=2").unwrap(); - assert_eq!(same.host_str(), Some("api.github.com")); - assert_ne!(other.host_str(), Some("api.github.com")); - } -} diff --git a/crates/vestige-core/src/connectors/mod.rs b/crates/vestige-core/src/connectors/mod.rs deleted file mode 100644 index e87c26b..0000000 --- a/crates/vestige-core/src/connectors/mod.rs +++ /dev/null @@ -1,393 +0,0 @@ -//! External-source connectors (#57). -//! -//! A connector turns records in a long-lived external system (a ticket tracker, -//! an issue board, a support queue) into source-aware Vestige memories, so an -//! investigative agent can search and reason over years of history **offline**, -//! **semantically**, and **cited back to the canonical record** — something no -//! live ticket-system MCP proxy can do. -//! -//! ## Layering -//! -//! - The [`Connector`] contract, [`NormalizedRecord`] shape, and the stable -//! [`content_hash`] are pure (no network) and always compiled, so the sync -//! semantics are unit-testable without hitting an API. -//! - Network-backed reference connectors ([`github`] and [`redmine`]) live -//! behind the `connectors` cargo feature so the default local-first build -//! links no HTTP client. -//! -//! ## Sync contract (the part that makes re-running safe) -//! -//! Every connector produces [`NormalizedRecord`]s. Each carries a -//! [`SourceEnvelope`](crate::memory::SourceEnvelope) whose -//! `(source_system, source_id)` is the idempotency key and whose `content_hash` -//! is the change detector. The driver routes each record through -//! [`upsert_by_source`](crate::storage::SqliteMemoryStore::upsert_by_source): -//! -//! - unseen record → insert -//! - changed `content_hash` → update in place (+ re-embed) -//! - same `content_hash` → no-op (only liveness advances) -//! -//! Because neither GitHub nor Redmine expose a deletion feed, deletions are -//! handled out-of-band by a periodic reconcile pass -//! ([`reconcile_source_tombstones`](crate::storage::SqliteMemoryStore::reconcile_source_tombstones)). - -use chrono::{DateTime, Utc}; - -use crate::memory::{IngestInput, SourceEnvelope}; -use crate::storage::ConnectorCursor; - -#[cfg(feature = "connectors")] -pub mod github; - -#[cfg(feature = "connectors")] -pub mod redmine; - -/// A single external record, already normalized into the fields Vestige needs. -/// -/// The connector is responsible for flattening a possibly-rich source record -/// (an issue plus its comments / journals / status changes) into a single -/// retrievable `content` blob plus the structured envelope. Keeping one memory -/// per logical record (rather than per comment) keeps retrieval coherent and -/// the idempotency key simple. -#[derive(Debug, Clone)] -pub struct NormalizedRecord { - /// Human-readable content to embed and search over. - pub content: String, - /// Tags for categorization (e.g. `["github", "issue", "state:open"]`). - pub tags: Vec, - /// The provenance envelope. `source_system`, `source_id`, and `content_hash` - /// MUST be set for idempotent upsert. - pub envelope: SourceEnvelope, -} - -impl NormalizedRecord { - /// Convert into an [`IngestInput`] ready for `upsert_by_source`. - pub fn into_ingest_input(self) -> IngestInput { - IngestInput { - content: self.content, - node_type: "event".to_string(), - source: self.envelope.source_url.clone(), - tags: self.tags, - source_envelope: Some(self.envelope), - ..Default::default() - } - } -} - -/// One page of records plus the cursor needed to fetch the next page. -#[derive(Debug, Clone, Default)] -pub struct FetchPage { - pub records: Vec, - /// Opaque token to resume after this page, or `None` when exhausted. - pub next_cursor: Option, -} - -/// Errors a connector can surface. -#[derive(Debug, thiserror::Error)] -pub enum ConnectorError { - #[error("configuration error: {0}")] - Config(String), - #[error("transport error: {0}")] - Transport(String), - #[error("rate limited; retry after {0:?}")] - RateLimited(Option), - #[error("source error ({status}): {message}")] - Source { status: u16, message: String }, -} - -pub type ConnectorResult = Result; - -/// The contract every external-source connector implements. -/// -/// Intentionally minimal: fetch a window of records updated since a cursor, -/// page through them, and (separately) enumerate currently-live ids for the -/// deletion-reconcile pass. The driver owns persistence, embedding, and cursor -/// checkpointing — a connector is just a typed, incremental reader. -#[allow(async_fn_in_trait)] -pub trait Connector { - /// Stable system identifier written into every envelope (`github`, …). - fn source_system(&self) -> &str; - - /// The scope this connector instance is bound to (`owner/repo`, project id). - fn scope(&self) -> &str; - - /// Fetch one page of records whose source-updated time is `>= since` - /// (inclusive on purpose — see the overlap note below), resuming from - /// `cursor` when provided. Records should be returned in ascending - /// update-time order so a mid-run interruption resumes safely. - /// - /// Callers pass `since = checkpoint − overlap` (a few minutes) so a record - /// written with a slightly-behind upstream clock, or one sharing the exact - /// boundary second, is never skipped. The `content_hash` short-circuit in - /// `upsert_by_source` makes the resulting re-scan free. - async fn fetch_updated( - &self, - since: Option>, - cursor: Option, - ) -> ConnectorResult; - - /// Enumerate the ids currently visible upstream for this scope, for the - /// deletion-reconcile pass. Cheap (ids only). `None` means the connector - /// cannot enumerate, so the driver must skip reconciliation rather than - /// tombstone everything. - async fn list_live_ids(&self) -> ConnectorResult>> { - Ok(None) - } -} - -/// Recommended overlap subtracted from the saved cursor before the next fetch, -/// to absorb clock skew and same-second boundary updates (the `>=` window). -pub const CURSOR_OVERLAP_SECS: i64 = 120; - -/// Summary of one sync run, returned to the caller / surfaced by the MCP tool. -#[derive(Debug, Clone, Default, serde::Serialize)] -pub struct SyncReport { - pub source_system: String, - pub scope: String, - pub created: usize, - pub updated: usize, - pub unchanged: usize, - pub tombstoned: usize, - /// New high-water mark persisted as the cursor for the next run. - pub new_cursor: Option>, - /// Whether a deletion-reconcile pass ran this time. - pub reconciled: bool, - /// Non-fatal warnings (e.g. a page that failed and was skipped). - pub warnings: Vec, -} - -/// Drive a full incremental sync of one connector into the store (#57). -/// -/// This is the orchestration the MCP `source_sync` tool calls. It: -/// 1. loads the saved checkpoint and starts from `cursor − overlap` (the `>=` -/// window that prevents missing same-second / clock-skewed updates); -/// 2. pages the connector forward in update order, routing each record through -/// [`upsert_by_source`](crate::storage::SqliteMemoryStore::upsert_by_source) -/// (insert / update-in-place / no-op by content hash); -/// 3. advances the cursor to the max `source_updated_at` actually observed, -/// persisting it only after the run so a crash re-scans rather than skips; -/// 4. optionally reconciles deletions when `reconcile` is set and the connector -/// can enumerate live ids. -/// -/// `max_pages` bounds a single run (so a first sync of a 15-year tracker can be -/// resumed across calls rather than blocking on one enormous fetch). -pub async fn run_sync( - store: &crate::storage::SqliteMemoryStore, - connector: &C, - reconcile: bool, - max_pages: usize, -) -> ConnectorResult { - use crate::storage::SourceUpsertOutcome; - - let source_system = connector.source_system().to_string(); - let scope = connector.scope().to_string(); - - let mut report = SyncReport { - source_system: source_system.clone(), - scope: scope.clone(), - ..Default::default() - }; - - // 1. Load checkpoint, apply the overlap window. - let checkpoint = store - .get_connector_cursor(&source_system, &scope) - .map_err(|e| ConnectorError::Transport(e.to_string()))?; - let since = checkpoint - .cursor_updated_at - .map(|c| c - chrono::Duration::seconds(CURSOR_OVERLAP_SECS)); - - // 2. Page forward, upserting each record. - let mut cursor: Option = None; - let mut max_seen = checkpoint.cursor_updated_at; - // Oldest source_updated_at among records that FAILED to upsert this run. We - // must not advance the persisted cursor past this, or the failed record — - // fetched in ascending update order — would fall outside the next run's - // `since` window and never be retried (a silent permanent gap). - let mut oldest_failure: Option> = None; - // Count of genuinely new records (Created). Unchanged re-scans of the - // overlap window must not inflate the running total. - let mut created_this_run = 0i64; - - for _ in 0..max_pages.max(1) { - let page = connector.fetch_updated(since, cursor.clone()).await?; - for record in page.records { - let observed = record.envelope.source_updated_at; - match store.upsert_by_source(record.into_ingest_input()) { - Ok(res) => { - match res.outcome { - SourceUpsertOutcome::Created => { - report.created += 1; - created_this_run += 1; - } - SourceUpsertOutcome::Updated => report.updated += 1, - SourceUpsertOutcome::Unchanged => report.unchanged += 1, - } - if let Some(ts) = observed - && max_seen.map(|m| ts > m).unwrap_or(true) - { - max_seen = Some(ts); - } - } - Err(e) => { - report.warnings.push(format!("upsert failed: {e}")); - if let Some(ts) = observed - && oldest_failure.map(|f| ts < f).unwrap_or(true) - { - oldest_failure = Some(ts); - } - } - } - } - match page.next_cursor { - Some(next) => cursor = Some(next), - None => break, - } - } - - // Clamp the cursor so we never advance past a record that failed this run. - // Subtract one second so the next run's inclusive `since` re-includes it. - if let Some(failed_at) = oldest_failure { - let clamp_to = failed_at - chrono::Duration::seconds(1); - max_seen = Some(match max_seen { - Some(m) if m < clamp_to => m, - _ => clamp_to, - }); - } - - // 3. Optional deletion reconciliation. - let mut reconciled = false; - if reconcile { - match connector.list_live_ids().await { - // CATASTROPHIC-DATA-LOSS GUARD: an empty live-id set would tombstone - // EVERY stored memory for this source (none of them appear in the - // empty list). An empty result almost always means a transient/auth - // failure or an over-narrow scope, not "the source truly has zero - // issues". Treat it like None (cannot safely enumerate) and skip. - Ok(Some(live_ids)) if live_ids.is_empty() => report.warnings.push( - "list_live_ids returned an empty set; skipping reconcile to avoid \ - mass-tombstoning the entire source" - .to_string(), - ), - Ok(Some(live_ids)) => { - match store.reconcile_source_tombstones(&source_system, &scope, &live_ids) { - Ok(r) => { - report.tombstoned = r.tombstoned.len(); - reconciled = true; - } - Err(e) => report.warnings.push(format!("reconcile failed: {e}")), - } - } - Ok(None) => report - .warnings - .push("connector cannot enumerate live ids; skipped reconcile".to_string()), - Err(e) => report.warnings.push(format!("list_live_ids failed: {e}")), - } - } - report.reconciled = reconciled; - report.new_cursor = max_seen; - - // 4. Persist the checkpoint (only after the run). - let now = Utc::now(); - let new_checkpoint = ConnectorCursor { - source_system: source_system.clone(), - scope: scope.clone(), - cursor_updated_at: max_seen, - last_synced_at: Some(now), - last_full_reconcile_at: if reconciled { - Some(now) - } else { - checkpoint.last_full_reconcile_at - }, - // Accumulate only NEW records, so re-scanning the overlap window (which - // reports Unchanged) does not inflate the running total. - records_seen: checkpoint.records_seen + created_this_run, - }; - store - .save_connector_cursor(&new_checkpoint) - .map_err(|e| ConnectorError::Transport(e.to_string()))?; - - Ok(report) -} - -/// Compute a stable content hash over the record's meaning. -/// -/// Stability requirements (so re-syncing an unchanged record is a true no-op): -/// - **key order independent** — callers pass `(field, value)` pairs which we -/// sort before hashing, so map/field ordering never changes the digest; -/// - **volatile fields excluded** — the caller must omit the cursor timestamp, -/// view/comment counts, and ephemeral permission flags (hash the meaning, -/// not the metadata); -/// - **collision-resistant** — BLAKE3 (already a Vestige dependency). -/// -/// Comment/journal arrays should be flattened into the pairs in a stable order -/// (sorted by their own id) by the caller before hashing. -pub fn content_hash(fields: &[(&str, &str)]) -> String { - let mut pairs: Vec<(&str, &str)> = fields.to_vec(); - pairs.sort_by(|a, b| a.0.cmp(b.0).then(a.1.cmp(b.1))); - - let mut hasher = blake3::Hasher::new(); - for (k, v) in pairs { - // Length-prefix each field so ("ab","c") can't collide with ("a","bc"). - hasher.update(&(k.len() as u64).to_le_bytes()); - hasher.update(k.as_bytes()); - hasher.update(&(v.len() as u64).to_le_bytes()); - hasher.update(v.as_bytes()); - } - hasher.finalize().to_hex().to_string() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn content_hash_is_order_independent() { - let a = content_hash(&[ - ("title", "Crash"), - ("body", "stacktrace"), - ("state", "open"), - ]); - let b = content_hash(&[ - ("state", "open"), - ("title", "Crash"), - ("body", "stacktrace"), - ]); - assert_eq!(a, b, "reordering fields must not change the hash"); - } - - #[test] - fn content_hash_changes_with_content() { - let a = content_hash(&[("body", "v1")]); - let b = content_hash(&[("body", "v2")]); - assert_ne!(a, b, "different content must hash differently"); - } - - #[test] - fn content_hash_no_boundary_collision() { - // ("ab","c") vs ("a","bc") must differ thanks to length prefixing. - let a = content_hash(&[("ab", "c")]); - let b = content_hash(&[("a", "bc")]); - assert_ne!(a, b); - } - - #[test] - fn normalized_record_carries_envelope_into_input() { - let rec = NormalizedRecord { - content: "issue body".to_string(), - tags: vec!["github".to_string()], - envelope: SourceEnvelope { - source_system: Some("github".to_string()), - source_id: Some("42".to_string()), - source_url: Some("https://example/42".to_string()), - content_hash: Some("h".to_string()), - ..Default::default() - }, - }; - let input = rec.into_ingest_input(); - assert_eq!(input.content, "issue body"); - assert_eq!(input.source.as_deref(), Some("https://example/42")); - let env = input.source_envelope.unwrap(); - assert!(env.has_key()); - assert_eq!(env.source_id.as_deref(), Some("42")); - } -} diff --git a/crates/vestige-core/src/connectors/redmine.rs b/crates/vestige-core/src/connectors/redmine.rs deleted file mode 100644 index 9ae9d18..0000000 --- a/crates/vestige-core/src/connectors/redmine.rs +++ /dev/null @@ -1,815 +0,0 @@ -//! Redmine connector (#57). -//! -//! Indexes a Redmine project's issues + journals (comments and status/assignment -//! history) into source-aware Vestige memories so an investigative agent can -//! search and reason over years of ticket history **offline**, **semantically**, -//! and **cited back to the canonical issue URL**. Redmine stays the system of -//! record; Vestige indexes, connects, retrieves, and links back. -//! -//! ## Incremental sync (per the connector sync contract) -//! -//! Redmine's REST API has three traps this connector handles explicitly (all -//! confirmed against the official wiki + canonical defects): -//! -//! - **`status_id=*` is mandatory.** The list endpoint returns *open issues -//! only* by default, so without it closing an issue looks like a deletion and -//! closed issues are never synced (Defect #19088). We pass it on both the -//! incremental pull and the reconcile enumeration. -//! - **`include=journals` is silently ignored on the list endpoint.** Journals -//! come back only on the per-issue detail endpoint `GET /issues/{id}.json` -//! (Defect #35242), so each changed issue costs one extra round-trip. -//! - **Filter operators must be hex-encoded** in the compact form -//! (`updated_on=>=…` → `updated_on=%3E%3D…`). We build the query with -//! `reqwest`'s `.query(&[…])` and pass the raw `>=…` value so it is encoded -//! exactly once (no double-encoding). -//! -//! `sort=updated_on:asc` pages forward in cursor order so a mid-run interruption -//! resumes safely; the `since = cursor − overlap` window + the `content_hash` -//! no-op make the re-scan free. Redmine has no deletion feed, so deletions are -//! reconciled out-of-band via [`list_live_ids`](Connector::list_live_ids). - -use chrono::{DateTime, Utc}; -use serde::Deserialize; - -use super::{ - Connector, ConnectorError, ConnectorResult, FetchPage, NormalizedRecord, content_hash, -}; -use crate::memory::SourceEnvelope; - -const USER_AGENT: &str = concat!("vestige-connector/", env!("CARGO_PKG_VERSION")); -const PAGE_LIMIT: u32 = 100; - -/// Configuration for a Redmine connector instance bound to one project. -#[derive(Clone)] -pub struct RedmineConfig { - /// Base URL of the Redmine instance, e.g. `https://redmine.example.com`. - pub base_url: String, - /// Project identifier to scope the sync to. May be the numeric id or the - /// project identifier slug — used as `project_id` and stored as - /// `source_project`. (Note: Redmine's `project_id` list filter wants the - /// numeric id; the slug works as the human-readable scope label.) - pub project: String, - /// API access key. Optional only if the instance allows anonymous REST. - pub api_key: Option, - /// Max journals to fold into one issue memory (defense against huge threads). - pub max_journals: usize, -} - -// Manual Debug that NEVER prints the api_key — a derived Debug would leak the -// credential into any `{:?}` log line or panic message. -impl std::fmt::Debug for RedmineConfig { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("RedmineConfig") - .field("base_url", &self.base_url) - .field("project", &self.project) - .field("api_key", &self.api_key.as_ref().map(|_| "")) - .field("max_journals", &self.max_journals) - .finish() - } -} - -impl RedmineConfig { - pub fn new(base_url: impl Into, project: impl Into) -> Self { - Self { - base_url: base_url.into(), - project: project.into(), - api_key: None, - max_journals: 100, - } - } - - pub fn with_api_key(mut self, key: Option) -> Self { - self.api_key = key; - self - } - - /// Base URL with any trailing slash removed. - fn root(&self) -> String { - self.base_url.trim_end_matches('/').to_string() - } -} - -/// A Redmine connector bound to one project. -pub struct RedmineConnector { - config: RedmineConfig, - scope: String, - client: reqwest::Client, -} - -impl RedmineConnector { - pub fn new(config: RedmineConfig) -> ConnectorResult { - if config.base_url.trim().is_empty() { - return Err(ConnectorError::Config("base_url is required".to_string())); - } - if config.project.trim().is_empty() { - return Err(ConnectorError::Config("project is required".to_string())); - } - // SSRF guard: require http(s) and reject internal/reserved hosts so a - // misconfigured/hostile base_url cannot point the authenticated client at - // localhost, link-local metadata endpoints (169.254.169.254), or private - // ranges. Gated off only when explicitly allowed (for local tests). - match reqwest::Url::parse(&config.root()) { - Ok(url) => { - let scheme = url.scheme(); - if scheme != "http" && scheme != "https" { - return Err(ConnectorError::Config(format!( - "base_url scheme must be http or https, got {scheme}" - ))); - } - if std::env::var("VESTIGE_ALLOW_PRIVATE_CONNECTOR_HOSTS").is_err() { - let host = url.host_str().ok_or_else(|| { - ConnectorError::Config("base_url has no host".to_string()) - })?; - let host_clean = host.trim_start_matches('[').trim_end_matches(']'); - if host_clean.eq_ignore_ascii_case("localhost") { - return Err(ConnectorError::Config( - "base_url host localhost is blocked (SSRF guard)".to_string(), - )); - } - if let Ok(ip) = host_clean.parse::() { - let reserved = match ip { - std::net::IpAddr::V4(v4) => { - v4.is_loopback() || v4.is_private() - || v4.is_link_local() || v4.is_unspecified() - } - std::net::IpAddr::V6(v6) => v6.is_loopback() || v6.is_unspecified(), - }; - if reserved { - return Err(ConnectorError::Config(format!( - "base_url host {ip} is a reserved/internal address (SSRF guard)" - ))); - } - } - } - } - Err(_) => { - return Err(ConnectorError::Config(format!( - "base_url is not a valid URL: {}", - config.base_url - ))); - } - } - let client = reqwest::Client::builder() - .user_agent(USER_AGENT) - .build() - .map_err(|e| ConnectorError::Transport(e.to_string()))?; - let scope = config.project.clone(); - Ok(Self { - config, - scope, - client, - }) - } - - fn auth(&self, req: reqwest::RequestBuilder) -> reqwest::RequestBuilder { - let req = req.header("Accept", "application/json"); - match &self.config.api_key { - // The key goes in the header (not the URL) so it stays out of proxy - // and access logs. - Some(k) => req.header("X-Redmine-API-Key", k), - None => req, - } - } - - fn classify_status(resp: &reqwest::Response) -> Option { - let status = resp.status(); - if status.is_success() { - return None; - } - if status.as_u16() == 429 { - let retry = resp - .headers() - .get("retry-after") - .and_then(|v| v.to_str().ok()) - .and_then(|s| s.parse::().ok()) - .map(std::time::Duration::from_secs); - return Some(ConnectorError::RateLimited(retry)); - } - let message = match status.as_u16() { - // A valid key against an instance with REST disabled 401/403s; make - // that distinguishable from "no results". - 401 | 403 => { - "unauthorized — check REDMINE_API_KEY and that the instance has the REST API enabled (Administration → Settings → API)" - .to_string() - } - _ => status - .canonical_reason() - .unwrap_or("request failed") - .to_string(), - }; - Some(ConnectorError::Source { - status: status.as_u16(), - message, - }) - } - - /// Fetch the journals + relations for one issue (the detail endpoint — - /// journals are not returned on the list endpoint). - async fn fetch_detail(&self, issue_id: u64) -> ConnectorResult { - let url = format!("{}/issues/{}.json", self.config.root(), issue_id); - let resp = self - .auth(self.client.get(&url)) - .query(&[("include", "journals,relations")]) - .send() - .await - .map_err(|e| ConnectorError::Transport(e.to_string()))?; - if let Some(err) = Self::classify_status(&resp) { - return Err(err); - } - let wrapper: IssueWrapper = resp - .json() - .await - .map_err(|e| ConnectorError::Transport(e.to_string()))?; - Ok(wrapper.issue) - } - - /// Fold a raw issue (with journals) into one normalized memory record. - fn normalize(&self, issue: &RawIssue) -> NormalizedRecord { - let status_name = issue.status.as_ref().map(|s| s.name.clone()); - let tracker_name = issue.tracker.as_ref().map(|t| t.name.clone()); - let author = issue.author.as_ref().map(|a| a.name.clone()); - - // Journals sorted by id for a stable order + stable hash. Keep notes - // and field changes so status/assignment history remains searchable. - let mut journals: Vec<&RawJournal> = issue - .journals - .iter() - .filter(|j| { - j.notes - .as_deref() - .map(|n| !n.trim().is_empty()) - .unwrap_or(false) - || !j.details.is_empty() - }) - .collect(); - journals.sort_by_key(|j| j.id); - // Keep the NEWEST max_journals, not the oldest: on a capped thread the - // latest activity is what signals the record changed. Truncating from the - // front (keeping oldest) meant new comments were dropped, the content_hash - // never moved, and the memory never re-indexed with fresh activity. - if journals.len() > self.config.max_journals { - let drop = journals.len() - self.config.max_journals; - journals.drain(0..drop); - } - - // Human-readable content. - let mut content = format!("[{}#{}] {}\n", self.scope, issue.id, issue.subject); - if let Some(s) = &status_name { - content.push_str(&format!("Status: {s}\n")); - } - if let Some(t) = &tracker_name { - content.push_str(&format!("Tracker: {t}\n")); - } - if let Some(desc) = &issue.description - && !desc.trim().is_empty() - { - content.push('\n'); - content.push_str(desc.trim()); - content.push('\n'); - } - for j in &journals { - let who = j.user.as_ref().map(|u| u.name.as_str()).unwrap_or("?"); - let note = j.notes.as_deref().unwrap_or("").trim(); - if !note.is_empty() { - content.push_str(&format!("\n- {who}: {note}")); - } - for detail in &j.details { - content.push_str(&format!( - "\n- {who} changed {}{}: {} -> {}", - detail.property.as_deref().unwrap_or("field"), - detail - .name - .as_deref() - .map(|n| format!(".{n}")) - .unwrap_or_default(), - detail.old_value.as_deref().unwrap_or(""), - detail.new_value.as_deref().unwrap_or("") - )); - } - } - if !issue.relations.is_empty() { - content.push_str("\n\nRelations:"); - let mut relations: Vec<&RawRelation> = issue.relations.iter().collect(); - relations.sort_by_key(|r| r.id); - for relation in relations { - let related = relation.related_issue_id(issue.id); - content.push_str(&format!( - "\n- #{} ({})", - related, - relation.relation_type.as_deref().unwrap_or("relates") - )); - if let Some(delay) = relation.delay { - content.push_str(&format!(", delay {delay}")); - } - } - } - - // Stable content hash — meaning only, never the cursor (`updated_on`) or - // volatile counts. Journals and relations contribute stable fields in id - // order. - let journals_blob = journals - .iter() - .map(|j| { - let details = j - .details - .iter() - .map(|d| { - format!( - "{}:{}:{}:{}", - d.property.as_deref().unwrap_or(""), - d.name.as_deref().unwrap_or(""), - d.old_value.as_deref().unwrap_or(""), - d.new_value.as_deref().unwrap_or("") - ) - }) - .collect::>() - .join("\u{1e}"); - format!( - "{}:{}:{}", - j.id, - j.notes.as_deref().unwrap_or("").trim(), - details - ) - }) - .collect::>() - .join("\u{1f}"); - let relations_blob = { - let mut relations: Vec<&RawRelation> = issue.relations.iter().collect(); - relations.sort_by_key(|r| r.id); - relations - .iter() - .map(|r| { - format!( - "{}:{}:{}:{}", - r.id, - r.issue_id.unwrap_or_default(), - r.issue_to_id.unwrap_or_default(), - r.relation_type.as_deref().unwrap_or("") - ) - }) - .collect::>() - .join("\u{1f}") - }; - let id_str = issue.id.to_string(); - let status_id_str = issue - .status - .as_ref() - .map(|s| s.id.to_string()) - .unwrap_or_default(); - let tracker_id_str = issue - .tracker - .as_ref() - .map(|t| t.id.to_string()) - .unwrap_or_default(); - let done_ratio_str = issue.done_ratio.unwrap_or(0).to_string(); - let desc_str = issue.description.clone().unwrap_or_default(); - let hash = content_hash(&[ - ("id", &id_str), - ("subject", &issue.subject), - ("description", &desc_str), - ("status_id", &status_id_str), - ("tracker_id", &tracker_id_str), - ("done_ratio", &done_ratio_str), - ("journals", &journals_blob), - ("relations", &relations_blob), - ]); - - // Tags, lowercased — `tag_prefix` matching is case-sensitive, and - // Redmine status/tracker names are mixed-case. - let mut tags = vec!["redmine".to_string(), "issue".to_string()]; - if let Some(s) = &status_name { - tags.push(format!("status:{}", s.to_lowercase())); - } - if let Some(t) = &tracker_name { - tags.push(format!("tracker:{}", t.to_lowercase())); - } - if let Some(p) = &issue.priority { - tags.push(format!("priority:{}", p.name.to_lowercase())); - } - - let envelope = SourceEnvelope { - source_system: Some("redmine".to_string()), - source_id: Some(issue.id.to_string()), - source_url: Some(format!("{}/issues/{}", self.config.root(), issue.id)), - source_updated_at: issue - .updated_on - .as_deref() - .and_then(|s| DateTime::parse_from_rfc3339(s).ok()) - .map(|d| d.with_timezone(&Utc)), - content_hash: Some(hash), - synced_at: Some(Utc::now()), - source_project: Some(self.scope.clone()), - source_type: Some("issue".to_string()), - source_author: author, - }; - - NormalizedRecord { - content, - tags, - envelope, - } - } -} - -impl Connector for RedmineConnector { - fn source_system(&self) -> &str { - "redmine" - } - - fn scope(&self) -> &str { - &self.scope - } - - async fn fetch_updated( - &self, - since: Option>, - cursor: Option, - ) -> ConnectorResult { - // The cursor carries the next offset (Redmine pages by offset, not an - // opaque url). First page = offset 0. - let offset: u32 = cursor.as_deref().and_then(|c| c.parse().ok()).unwrap_or(0); - - let url = format!("{}/issues.json", self.config.root()); - let limit_str = PAGE_LIMIT.to_string(); - let offset_str = offset.to_string(); - // Build params; reqwest percent-encodes each value exactly once, so we - // pass the RAW `>=…` operator (it becomes %3E%3D on the wire). Do not - // pre-encode here or it would be double-encoded. - let mut params: Vec<(&str, String)> = vec![ - ("status_id", "*".to_string()), - ("sort", "updated_on:asc".to_string()), - ("project_id", self.config.project.clone()), - ("limit", limit_str), - ("offset", offset_str), - ]; - if let Some(s) = since { - let since_z = s.to_rfc3339_opts(chrono::SecondsFormat::Secs, true); - params.push(("updated_on", format!(">={since_z}"))); - } - - let resp = self - .auth(self.client.get(&url)) - .query(¶ms) - .send() - .await - .map_err(|e| ConnectorError::Transport(e.to_string()))?; - if let Some(err) = Self::classify_status(&resp) { - return Err(err); - } - let page: IssueListResponse = resp - .json() - .await - .map_err(|e| ConnectorError::Transport(e.to_string()))?; - - // Per-issue detail fetch for journals (list endpoint omits them). - // - // A detail-fetch failure must NOT silently fall back to the journal-less - // list summary: that persists a record with incomplete content and the - // WRONG content_hash, and because the offset cursor still advances past - // it, the issue is never revisited — a permanent silent gap. Instead we - // abort the page with the transport error. The driver returns without - // advancing the persisted cursor (fetch_updated is called with `?`), so - // the next run re-fetches this same window; the idempotent upsert makes - // reprocessing safe once the detail endpoint recovers. - let mut records = Vec::new(); - for summary in &page.issues { - let detailed = self.fetch_detail(summary.id).await.map_err(|e| { - ConnectorError::Transport(format!( - "detail fetch failed for issue {}; not advancing cursor to avoid a silent gap: {e}", - summary.id - )) - })?; - records.push(self.normalize(&detailed)); - } - - // Advance the offset cursor until we've walked total_count. - let next_offset = offset + page.issues.len() as u32; - let next_cursor = if (next_offset as u64) < page.total_count && !page.issues.is_empty() { - Some(next_offset.to_string()) - } else { - None - }; - - Ok(FetchPage { - records, - next_cursor, - }) - } - - async fn list_live_ids(&self) -> ConnectorResult>> { - // Enumerate all issue ids (open AND closed) for the reconcile pass. - // status_id=* is mandatory here too, or closed issues read as deleted. - let mut ids = Vec::new(); - // u64 offset (a u32 could wrap on a huge/compromised total_count, turning - // the loop infinite + allocating unboundedly). Also hard-cap pages. - let mut offset: u64 = 0; - const MAX_PAGES: u32 = 10_000; - let mut pages = 0u32; - loop { - let url = format!("{}/issues.json", self.config.root()); - let resp = self - .auth(self.client.get(&url)) - .query(&[ - ("status_id", "*".to_string()), - ("project_id", self.config.project.clone()), - ("limit", PAGE_LIMIT.to_string()), - ("offset", offset.to_string()), - ]) - .send() - .await - .map_err(|e| ConnectorError::Transport(e.to_string()))?; - if let Some(err) = Self::classify_status(&resp) { - return Err(err); - } - let page: IssueListResponse = resp - .json() - .await - .map_err(|e| ConnectorError::Transport(e.to_string()))?; - if page.issues.is_empty() { - break; - } - for issue in &page.issues { - ids.push(issue.id.to_string()); - } - let new_offset = offset + page.issues.len() as u64; - // Defensive: a non-advancing page would loop forever. - if new_offset <= offset { - break; - } - offset = new_offset; - pages += 1; - if offset >= page.total_count || pages >= MAX_PAGES { - break; - } - } - Ok(Some(ids)) - } -} - -// --------------------------------------------------------------------------- -// Raw Redmine API shapes (only the fields we use) -// --------------------------------------------------------------------------- - -#[derive(Debug, Deserialize)] -struct IssueListResponse { - #[serde(default)] - issues: Vec, - #[serde(default)] - total_count: u64, -} - -#[derive(Debug, Deserialize)] -struct IssueWrapper { - issue: RawIssue, -} - -#[derive(Debug, Clone, Deserialize)] -struct RawIssue { - id: u64, - #[serde(default)] - subject: String, - #[serde(default)] - description: Option, - #[serde(default)] - status: Option, - #[serde(default)] - tracker: Option, - #[serde(default)] - priority: Option, - #[serde(default)] - author: Option, - #[serde(default)] - done_ratio: Option, - #[serde(default)] - updated_on: Option, - #[serde(default)] - journals: Vec, - #[serde(default)] - relations: Vec, -} - -/// Redmine `{id, name}` reference (status, tracker, priority, user, …). -#[derive(Debug, Clone, Deserialize)] -struct NamedRef { - #[serde(default)] - id: i64, - #[serde(default)] - name: String, -} - -#[derive(Debug, Clone, Deserialize)] -struct RawJournal { - id: u64, - #[serde(default)] - notes: Option, - #[serde(default)] - user: Option, - #[serde(default)] - details: Vec, -} - -#[derive(Debug, Clone, Deserialize)] -struct RawJournalDetail { - #[serde(default)] - property: Option, - #[serde(default)] - name: Option, - #[serde(default)] - old_value: Option, - #[serde(default)] - new_value: Option, -} - -#[derive(Debug, Clone, Deserialize)] -struct RawRelation { - #[serde(default)] - id: u64, - #[serde(default)] - issue_id: Option, - #[serde(default)] - issue_to_id: Option, - #[serde(default)] - relation_type: Option, - #[serde(default)] - delay: Option, -} - -impl RawRelation { - fn related_issue_id(&self, current_issue_id: u64) -> u64 { - match (self.issue_id, self.issue_to_id) { - (Some(from), Some(to)) if from == current_issue_id => to, - (Some(from), Some(to)) if to == current_issue_id => from, - (_, Some(to)) => to, - (Some(from), _) => from, - _ => 0, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn issue(id: u64, subject: &str, desc: &str, status: (i64, &str)) -> RawIssue { - RawIssue { - id, - subject: subject.to_string(), - description: Some(desc.to_string()), - status: Some(NamedRef { - id: status.0, - name: status.1.to_string(), - }), - tracker: Some(NamedRef { - id: 1, - name: "Bug".to_string(), - }), - priority: Some(NamedRef { - id: 2, - name: "Normal".to_string(), - }), - author: Some(NamedRef { - id: 7, - name: "Jane Dev".to_string(), - }), - done_ratio: Some(0), - updated_on: Some("2026-06-19T00:00:00Z".to_string()), - journals: vec![], - relations: vec![], - } - } - - fn connector() -> RedmineConnector { - RedmineConnector::new(RedmineConfig::new("https://redmine.example.com", "infra")).unwrap() - } - - #[test] - fn rejects_empty_and_bad_config() { - assert!(RedmineConnector::new(RedmineConfig::new("", "p")).is_err()); - assert!(RedmineConnector::new(RedmineConfig::new("https://r.example", "")).is_err()); - assert!(RedmineConnector::new(RedmineConfig::new("not a url", "p")).is_err()); - } - - #[test] - fn normalize_builds_keyed_envelope_with_citation() { - let c = connector(); - let rec = c.normalize(&issue(123, "Disk full", "df -h shows 100%", (1, "New"))); - let env = &rec.envelope; - assert!(env.has_key()); - assert_eq!(env.source_system.as_deref(), Some("redmine")); - assert_eq!(env.source_id.as_deref(), Some("123")); - assert_eq!( - env.source_url.as_deref(), - Some("https://redmine.example.com/issues/123") - ); - assert_eq!(env.source_project.as_deref(), Some("infra")); - assert_eq!(env.source_author.as_deref(), Some("Jane Dev")); - assert!(rec.content.contains("Disk full")); - // Tags lowercased so the case-sensitive tag_prefix filter matches. - assert!(rec.tags.contains(&"status:new".to_string())); - assert!(rec.tags.contains(&"tracker:bug".to_string())); - assert!(rec.tags.contains(&"priority:normal".to_string())); - } - - #[test] - fn status_change_changes_hash() { - let c = connector(); - let new = c - .normalize(&issue(1, "T", "body", (1, "New"))) - .envelope - .content_hash; - let closed = c - .normalize(&issue(1, "T", "body", (5, "Closed"))) - .envelope - .content_hash; - assert_ne!( - new, closed, - "a status change must change the hash → re-embed" - ); - } - - #[test] - fn journals_fold_in_id_order_and_affect_hash() { - let c = connector(); - let mut iss = issue(1, "T", "body", (1, "New")); - iss.journals = vec![ - RawJournal { - id: 20, - notes: Some("second".to_string()), - user: Some(NamedRef { - id: 1, - name: "B".to_string(), - }), - details: vec![], - }, - RawJournal { - id: 10, - notes: Some("first".to_string()), - user: Some(NamedRef { - id: 2, - name: "A".to_string(), - }), - details: vec![], - }, - // Pure empty journal must be dropped, not folded. - RawJournal { - id: 30, - notes: None, - user: Some(NamedRef { - id: 3, - name: "C".to_string(), - }), - details: vec![], - }, - ]; - let rec = c.normalize(&iss); - let first = rec.content.find("first").unwrap(); - let second = rec.content.find("second").unwrap(); - assert!(first < second, "journals fold in id order"); - - let no_journals = c - .normalize(&issue(1, "T", "body", (1, "New"))) - .envelope - .content_hash; - assert_ne!( - rec.envelope.content_hash, no_journals, - "journals must contribute to the hash" - ); - } - - #[test] - fn journal_details_and_relations_are_searchable_and_hashed() { - let c = connector(); - let mut iss = issue(1, "T", "body", (1, "New")); - iss.journals = vec![RawJournal { - id: 1, - notes: None, - user: Some(NamedRef { - id: 2, - name: "A".to_string(), - }), - details: vec![RawJournalDetail { - property: Some("attr".to_string()), - name: Some("status_id".to_string()), - old_value: Some("1".to_string()), - new_value: Some("5".to_string()), - }], - }]; - iss.relations = vec![RawRelation { - id: 9, - issue_id: Some(1), - issue_to_id: Some(42), - relation_type: Some("relates".to_string()), - delay: None, - }]; - - let rec = c.normalize(&iss); - assert!(rec.content.contains("changed attr.status_id: 1 -> 5")); - assert!(rec.content.contains("#42 (relates)")); - - let no_history = c.normalize(&issue(1, "T", "body", (1, "New"))); - assert_ne!( - rec.envelope.content_hash, no_history.envelope.content_hash, - "field-change journals and relations must affect idempotent updates" - ); - } -} diff --git a/crates/vestige-core/src/consolidation/phases.rs b/crates/vestige-core/src/consolidation/phases.rs index 9fe41b7..abf6019 100644 --- a/crates/vestige-core/src/consolidation/phases.rs +++ b/crates/vestige-core/src/consolidation/phases.rs @@ -626,15 +626,13 @@ impl DreamEngine { tag_b: &str, conn_type: CreativeConnectionType, ) -> String { - // char-boundary-safe: &content[..60] panics if a multi-byte UTF-8 char - // straddles byte 60. get(..60) returns None at a non-boundary => fall back. let a_summary = if a.content.len() > 60 { - a.content.get(..60).unwrap_or(&a.content) + &a.content[..60] } else { &a.content }; let b_summary = if b.content.len() > 60 { - b.content.get(..60).unwrap_or(&b.content) + &b.content[..60] } else { &b.content }; @@ -891,7 +889,6 @@ mod tests { embedding_model: None, suppression_count: 0, suppressed_at: None, - source_envelope: None, } } diff --git a/crates/vestige-core/src/consolidation/sleep.rs b/crates/vestige-core/src/consolidation/sleep.rs index 005ba08..fe5e672 100644 --- a/crates/vestige-core/src/consolidation/sleep.rs +++ b/crates/vestige-core/src/consolidation/sleep.rs @@ -192,7 +192,6 @@ impl ConsolidationRun { neighbors_reinforced: 0, activations_computed: 0, w20_optimized: None, - backfilled_causes: 0, } } } diff --git a/crates/vestige-core/src/embedder/fastembed.rs b/crates/vestige-core/src/embedder/fastembed.rs deleted file mode 100644 index a4b079e..0000000 --- a/crates/vestige-core/src/embedder/fastembed.rs +++ /dev/null @@ -1,217 +0,0 @@ -//! `FastembedEmbedder` -- adapts the existing `EmbeddingService` to the -//! `LocalEmbedder` trait. - -#[cfg(feature = "embeddings")] -use crate::embeddings::{EMBEDDING_DIMENSIONS, EmbeddingService}; - -use super::{EmbedderError, EmbedderResult, EmbedderSend}; - -pub struct FastembedEmbedder { - #[cfg(feature = "embeddings")] - inner: EmbeddingService, - cached_hash: std::sync::OnceLock, -} - -impl FastembedEmbedder { - pub fn new() -> Self { - Self { - #[cfg(feature = "embeddings")] - inner: EmbeddingService::new(), - cached_hash: std::sync::OnceLock::new(), - } - } - - fn compute_hash(name: &str, dim: usize) -> String { - let mut hasher = blake3::Hasher::new(); - hasher.update(name.as_bytes()); - hasher.update(&(dim as u64).to_le_bytes()); - // fastembed's ONNX bytes are not directly accessible at runtime; we - // use `(name, dim, vestige-core CARGO_PKG_VERSION)` as the - // signature. If fastembed ever changes its output deterministically - // between minor versions, bumping the crate version triggers a - // mismatch -- which is exactly the drift we want to detect. - hasher.update(env!("CARGO_PKG_VERSION").as_bytes()); - hasher.finalize().to_hex().to_string() - } -} - -impl Default for FastembedEmbedder { - fn default() -> Self { - Self::new() - } -} - -impl EmbedderSend for FastembedEmbedder { - async fn embed(&self, text: &str) -> EmbedderResult> { - #[cfg(feature = "embeddings")] - { - let emb = self - .inner - .embed(text) - .map_err(|e| EmbedderError::EmbedFailed(e.to_string()))?; - Ok(emb.vector) - } - #[cfg(not(feature = "embeddings"))] - { - let _ = text; - Err(EmbedderError::Init( - "embeddings feature not enabled".to_string(), - )) - } - } - - fn model_name(&self) -> &str { - #[cfg(feature = "embeddings")] - { - self.inner.model_name() - } - #[cfg(not(feature = "embeddings"))] - { - "nomic-ai/nomic-embed-text-v1.5" - } - } - - fn dimension(&self) -> usize { - #[cfg(feature = "embeddings")] - { - EMBEDDING_DIMENSIONS - } - #[cfg(not(feature = "embeddings"))] - { - 256 - } - } - - fn model_hash(&self) -> String { - self.cached_hash - .get_or_init(|| Self::compute_hash(self.model_name(), self.dimension())) - .clone() - } - - async fn embed_batch(&self, texts: &[&str]) -> EmbedderResult>> { - #[cfg(feature = "embeddings")] - { - let embs = self - .inner - .embed_batch(texts) - .map_err(|e| EmbedderError::EmbedFailed(e.to_string()))?; - Ok(embs.into_iter().map(|e| e.vector).collect()) - } - #[cfg(not(feature = "embeddings"))] - { - let _ = texts; - Err(EmbedderError::Init( - "embeddings feature not enabled".to_string(), - )) - } - } -} - -// ============================================================================ -// UNIT TESTS -// ============================================================================ - -#[cfg(test)] -mod tests { - use super::*; - - #[cfg(feature = "embeddings")] - fn is_model_unavailable(err: &EmbedderError) -> bool { - let msg = err.to_string(); - msg.contains("Failed to retrieve") - || msg.contains("model files can be downloaded") - || msg.contains("Failed to initialize nomic-ai/nomic-embed-text-v1.5") - } - - #[test] - fn embedder_reports_correct_name() { - let e = FastembedEmbedder::new(); - assert!( - e.model_name().contains("nomic"), - "model name should contain 'nomic'" - ); - } - - #[test] - fn embedder_reports_256_dimension() { - let e = FastembedEmbedder::new(); - assert_eq!(e.dimension(), 256); - } - - #[test] - fn embedder_hash_is_stable() { - let e = FastembedEmbedder::new(); - let h1 = e.model_hash(); - let h2 = e.model_hash(); - assert_eq!(h1, h2, "model_hash must be stable across calls"); - } - - #[test] - fn embedder_hash_includes_crate_version() { - // Compute what the hash should be given the known inputs - let name = FastembedEmbedder::new().model_name().to_string(); - let dim = FastembedEmbedder::new().dimension(); - let expected = FastembedEmbedder::compute_hash(&name, dim); - let got = FastembedEmbedder::new().model_hash(); - assert_eq!(got, expected); - } - - #[test] - fn embedder_signature_matches_accessors() { - let e = FastembedEmbedder::new(); - let sig = e.signature(); - assert_eq!(sig.name, e.model_name()); - assert_eq!(sig.dimension, e.dimension()); - assert_eq!(sig.hash, e.model_hash()); - } - - #[cfg(feature = "embeddings")] - #[test] - fn embedder_embed_smoke() { - let e = FastembedEmbedder::new(); - let rt = tokio::runtime::Runtime::new().unwrap(); - let vec = match rt.block_on(e.embed("hello world")) { - Ok(vec) => vec, - Err(err) if is_model_unavailable(&err) => { - eprintln!("skipping fastembed smoke; model unavailable: {err}"); - return; - } - Err(err) => panic!("embed: {err}"), - }; - assert_eq!(vec.len(), 256); - } - - #[cfg(feature = "embeddings")] - #[test] - fn embedder_embed_batch_matches_sequential() { - let e = FastembedEmbedder::new(); - let rt = tokio::runtime::Runtime::new().unwrap(); - let texts = ["alpha beta", "gamma delta"]; - let batch = match rt.block_on(e.embed_batch(texts.as_ref())) { - Ok(batch) => batch, - Err(err) if is_model_unavailable(&err) => { - eprintln!("skipping fastembed batch smoke; model unavailable: {err}"); - return; - } - Err(err) => panic!("batch: {err}"), - }; - let seq_a = match rt.block_on(e.embed(texts[0])) { - Ok(vec) => vec, - Err(err) if is_model_unavailable(&err) => { - eprintln!("skipping fastembed sequential smoke; model unavailable: {err}"); - return; - } - Err(err) => panic!("seq a: {err}"), - }; - let seq_b = match rt.block_on(e.embed(texts[1])) { - Ok(vec) => vec, - Err(err) if is_model_unavailable(&err) => { - eprintln!("skipping fastembed sequential smoke; model unavailable: {err}"); - return; - } - Err(err) => panic!("seq b: {err}"), - }; - assert_eq!(batch[0], seq_a); - assert_eq!(batch[1], seq_b); - } -} diff --git a/crates/vestige-core/src/embedder/mod.rs b/crates/vestige-core/src/embedder/mod.rs deleted file mode 100644 index c13368b..0000000 --- a/crates/vestige-core/src/embedder/mod.rs +++ /dev/null @@ -1,109 +0,0 @@ -//! Text-to-vector encoding trait. Pluggable per-install. - -use std::future::Future; -use std::pin::Pin; - -mod fastembed; - -pub use fastembed::FastembedEmbedder; - -/// Error returned by every `Embedder` method. -#[non_exhaustive] -#[derive(Debug, thiserror::Error)] -pub enum EmbedderError { - #[error("embedder initialization failed: {0}")] - Init(String), - #[error("embedding generation failed: {0}")] - EmbedFailed(String), - #[error("invalid input: {0}")] - InvalidInput(String), -} - -pub type EmbedderResult = std::result::Result; - -/// Boxed Send future returning an `EmbedderResult`, bound to the lifetime -/// of the borrows captured by the call. Used as the return type of every -/// async method on the dyn-compatible `Embedder` trait below. -pub type BoxedEmbedderFuture<'a, T> = Pin> + Send + 'a>>; - -/// Pluggable embedder. The storage layer NEVER calls fastembed directly; -/// callers compute vectors via this trait and pass them into `MemoryStore`. -/// -/// `LocalEmbedder` is the source-of-truth trait declared with native -/// async-fn-in-trait. `#[trait_variant::make(EmbedderSend: Send)]` derives -/// a Send-bounded variant that backends actually implement (the -/// trait_variant 0.1.x blanket goes variant -> source). The dyn-compatible -/// public surface is the `Embedder` trait declared below, which wraps every -/// async method in `Pin>`. -#[trait_variant::make(EmbedderSend: Send)] -pub trait LocalEmbedder: Sync + 'static { - async fn embed(&self, text: &str) -> EmbedderResult>; - - fn model_name(&self) -> &str; - - fn dimension(&self) -> usize; - - /// Stable blake3 hash of (model_name || dimension || vestige-core crate version). - /// Lowercase hex, 64 chars. - /// - /// Used by `MemoryStore::register_model` to detect silent model drift - /// (e.g. a fastembed minor upgrade that changes vector output). - fn model_hash(&self) -> String; - - async fn embed_batch(&self, texts: &[&str]) -> EmbedderResult>>; - - /// Returns the `ModelSignature` describing this embedder. Convenience - /// wrapper over the three accessors above. - fn signature(&self) -> crate::storage::ModelSignature { - crate::storage::ModelSignature { - name: self.model_name().to_string(), - dimension: self.dimension(), - hash: self.model_hash(), - } - } -} - -/// Dyn-compatible embedder trait. -/// -/// `EmbedderSend` above is the trait users implement; it uses native -/// async-fn-in-trait return types (RPITIT), which gives zero-allocation -/// static dispatch but is not dyn-safe. This trait wraps every async -/// method in `Pin>` so `Box` -/// and `Arc` work for the cognitive module surface and -/// the Phase 1 integration tests. -/// -/// Implementations should not target this trait directly; the blanket -/// `impl Embedder for T` adapts every Send-variant -/// implementation automatically. -pub trait Embedder: Send + Sync + 'static { - fn embed<'a>(&'a self, text: &'a str) -> BoxedEmbedderFuture<'a, Vec>; - fn embed_batch<'a>(&'a self, texts: &'a [&'a str]) -> BoxedEmbedderFuture<'a, Vec>>; - fn model_name(&self) -> &str; - fn dimension(&self) -> usize; - fn model_hash(&self) -> String; - fn signature(&self) -> crate::storage::ModelSignature; -} - -impl Embedder for T -where - T: EmbedderSend, -{ - fn embed<'a>(&'a self, text: &'a str) -> BoxedEmbedderFuture<'a, Vec> { - Box::pin(::embed(self, text)) - } - fn embed_batch<'a>(&'a self, texts: &'a [&'a str]) -> BoxedEmbedderFuture<'a, Vec>> { - Box::pin(::embed_batch(self, texts)) - } - fn model_name(&self) -> &str { - ::model_name(self) - } - fn dimension(&self) -> usize { - ::dimension(self) - } - fn model_hash(&self) -> String { - ::model_hash(self) - } - fn signature(&self) -> crate::storage::ModelSignature { - ::signature(self) - } -} diff --git a/crates/vestige-core/src/embeddings/code.rs b/crates/vestige-core/src/embeddings/code.rs index 458157b..7a72625 100644 --- a/crates/vestige-core/src/embeddings/code.rs +++ b/crates/vestige-core/src/embeddings/code.rs @@ -85,21 +85,13 @@ impl CodeEmbedding { lines.join(" ") } - /// Check if a line is only a comment. - /// - /// Conservative on purpose: the previous version treated any line starting - /// with `#` or `*` as a comment, which deleted real code — C preprocessor - /// directives (`#include`, `#define`), pointer derefs / multiplication - /// (`*ptr = 5;`), CSS `* { ... }`, etc. We only strip unambiguous comment - /// markers; a leading `*` counts only as a block-comment continuation - /// (`* ...`), never a bare `*expr`. + /// Check if a line is only a comment fn is_comment_only(&self, line: &str) -> bool { let trimmed = line.trim(); trimmed.starts_with("//") + || trimmed.starts_with('#') || trimmed.starts_with("/*") - || trimmed == "*" - || trimmed.starts_with("* ") - || trimmed.starts_with("*/") + || trimmed.starts_with('*') } /// Extract semantic chunks from code diff --git a/crates/vestige-core/src/embeddings/local.rs b/crates/vestige-core/src/embeddings/local.rs index 03757a6..3dfc363 100644 --- a/crates/vestige-core/src/embeddings/local.rs +++ b/crates/vestige-core/src/embeddings/local.rs @@ -7,13 +7,7 @@ //! - **Default**: Nomic Embed Text v1.5 (ONNX, 768d → 256d Matryoshka, 8192 context) //! - **Optional**: Nomic Embed Text v2 MoE (Candle, 475M params, 305M active, 8 experts) //! Enable with `nomic-v2` feature flag + `metal` for Apple Silicon acceleration. -//! - **Optional**: Qwen3 Embedding 0.6B (Candle, 1024d → 256d Matryoshka) -//! Enable with `qwen3-embeddings` and `VESTIGE_EMBEDDING_MODEL=qwen3-0.6b`. -#[cfg(feature = "qwen3-embeddings")] -use candle_core::{DType, Device}; -#[cfg(feature = "qwen3-embeddings")] -use fastembed::Qwen3TextEmbedding; use fastembed::{EmbeddingModel, InitOptions, TextEmbedding}; use std::sync::{Mutex, OnceLock}; @@ -37,82 +31,7 @@ pub const BATCH_SIZE: usize = 32; // ============================================================================ /// Result type for model initialization -static EMBEDDING_BACKEND_RESULT: OnceLock, String>> = - OnceLock::new(); - -const NOMIC_V15_MODEL_ID: &str = "nomic-ai/nomic-embed-text-v1.5"; -#[cfg(feature = "qwen3-embeddings")] -const QWEN3_06B_MODEL_ID: &str = "Qwen/Qwen3-Embedding-0.6B"; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum EmbeddingModelSpec { - NomicV15, - #[cfg(feature = "qwen3-embeddings")] - Qwen3Embedding06B, -} - -impl EmbeddingModelSpec { - fn selected() -> Result { - let requested = std::env::var("VESTIGE_EMBEDDING_MODEL") - .ok() - .map(|value| value.trim().to_ascii_lowercase()) - .filter(|value| !value.is_empty()) - .unwrap_or_else(|| "nomic-v1.5".to_string()); - - match requested.as_str() { - "nomic" | "nomic-v1.5" | "nomic-embed-text-v1.5" | NOMIC_V15_MODEL_ID => { - Ok(Self::NomicV15) - } - "qwen3" | "qwen3-0.6b" | "qwen3-embedding-0.6b" | "qwen/qwen3-embedding-0.6b" => { - #[cfg(feature = "qwen3-embeddings")] - { - Ok(Self::Qwen3Embedding06B) - } - #[cfg(not(feature = "qwen3-embeddings"))] - { - Err( - "VESTIGE_EMBEDDING_MODEL requests Qwen3, but vestige-core was not built with the qwen3-embeddings feature" - .to_string(), - ) - } - } - other => Err(format!( - "Unsupported VESTIGE_EMBEDDING_MODEL '{}'. Expected 'nomic-v1.5' or 'qwen3-0.6b'.", - other - )), - } - } - - fn model_name(self) -> &'static str { - match self { - Self::NomicV15 => NOMIC_V15_MODEL_ID, - #[cfg(feature = "qwen3-embeddings")] - Self::Qwen3Embedding06B => QWEN3_06B_MODEL_ID, - } - } -} - -enum EmbeddingBackend { - NomicV15(TextEmbedding), - #[cfg(feature = "qwen3-embeddings")] - Qwen3Embedding06B(Qwen3TextEmbedding), -} - -impl EmbeddingBackend { - fn model_name(&self) -> &'static str { - match self { - Self::NomicV15(_) => NOMIC_V15_MODEL_ID, - #[cfg(feature = "qwen3-embeddings")] - Self::Qwen3Embedding06B(_) => QWEN3_06B_MODEL_ID, - } - } -} - -fn qwen3_format_query(query: &str) -> String { - format!( - "Instruct: Given a web search query, retrieve relevant passages that answer the query\nQuery: {query}" - ) -} +static EMBEDDING_MODEL_RESULT: OnceLock, String>> = OnceLock::new(); /// Get the default cache directory for fastembed models. /// @@ -146,10 +65,10 @@ pub(crate) fn get_cache_dir() -> std::path::PathBuf { std::path::PathBuf::from(".fastembed_cache") } -/// Initialize the global embedding backend selected by `VESTIGE_EMBEDDING_MODEL`. -fn get_backend() -> Result, EmbeddingError> { - let result = EMBEDDING_BACKEND_RESULT.get_or_init(|| { - let spec = EmbeddingModelSpec::selected()?; +/// Initialize the global embedding model +/// Using nomic-embed-text-v1.5 (768d) - 8192 token context, Matryoshka support +fn get_model() -> Result, EmbeddingError> { + let result = EMBEDDING_MODEL_RESULT.get_or_init(|| { // Get cache directory (respects FASTEMBED_CACHE_PATH env var) let cache_dir = get_cache_dir(); @@ -158,72 +77,31 @@ fn get_backend() -> Result, Emb tracing::warn!("Failed to create cache directory {:?}: {}", cache_dir, e); } - match spec { - EmbeddingModelSpec::NomicV15 => { - let options = InitOptions::new(EmbeddingModel::NomicEmbedTextV15) - .with_show_download_progress(true) - .with_cache_dir(cache_dir); + // nomic-embed-text-v1.5: 768 dimensions, 8192 token context + // Matryoshka representation learning, fully open source + let options = InitOptions::new(EmbeddingModel::NomicEmbedTextV15) + .with_show_download_progress(true) + .with_cache_dir(cache_dir); - TextEmbedding::try_new(options) - .map(EmbeddingBackend::NomicV15) - .map(Mutex::new) - .map_err(|e| { - format!( - "Failed to initialize {} embedding model: {}. \ - Ensure ONNX runtime is available and model files can be downloaded.", - spec.model_name(), - e - ) - }) - } - #[cfg(feature = "qwen3-embeddings")] - EmbeddingModelSpec::Qwen3Embedding06B => { - let device = qwen3_device(); - Qwen3TextEmbedding::from_hf( - QWEN3_06B_MODEL_ID, - &device, - DType::F32, - MAX_TEXT_LENGTH, + TextEmbedding::try_new(options) + .map(Mutex::new) + .map_err(|e| { + format!( + "Failed to initialize nomic-embed-text-v1.5 embedding model: {}. \ + Ensure ONNX runtime is available and model files can be downloaded.", + e ) - .map(EmbeddingBackend::Qwen3Embedding06B) - .map(Mutex::new) - .map_err(|e| { - format!( - "Failed to initialize {} embedding model: {}. \ - Ensure Hugging Face model files can be downloaded.", - spec.model_name(), - e - ) - }) - } - } + }) }); match result { - Ok(backend) => backend + Ok(model) => model .lock() .map_err(|e| EmbeddingError::ModelInit(format!("Lock poisoned: {}", e))), Err(err) => Err(EmbeddingError::ModelInit(err.clone())), } } -#[cfg(feature = "qwen3-embeddings")] -fn qwen3_device() -> Device { - #[cfg(feature = "cuda")] - { - if let Ok(device) = Device::new_cuda(0) { - return device; - } - } - #[cfg(feature = "metal")] - { - if let Ok(device) = Device::new_metal(0) { - return device; - } - } - Device::Cpu -} - // ============================================================================ // ERROR TYPES // ============================================================================ @@ -343,43 +221,38 @@ impl EmbeddingService { Self { _unused: () } } - /// Check if the model has already been initialized. - /// - /// This must stay side-effect free: health/status paths call it during - /// startup and must not download or load ONNX models just to report state. + /// Check if the model is ready pub fn is_ready(&self) -> bool { - match EMBEDDING_BACKEND_RESULT.get() { - Some(Ok(backend)) => backend.lock().is_ok(), - Some(Err(e)) => { + match get_model() { + Ok(_) => true, + Err(e) => { tracing::warn!("Embedding model not ready: {}", e); false } - None => false, } } /// Check if the model is ready and return the error if not pub fn check_ready(&self) -> Result<(), EmbeddingError> { - get_backend().map(|_| ()) + get_model().map(|_| ()) } /// Initialize the model (downloads if necessary) pub fn init(&self) -> Result<(), EmbeddingError> { - let _model = get_backend()?; // Ensures model is loaded and returns any init errors + let _model = get_model()?; // Ensures model is loaded and returns any init errors Ok(()) } /// Get the model name pub fn model_name(&self) -> &'static str { - if let Some(Ok(backend)) = EMBEDDING_BACKEND_RESULT.get() - && let Ok(backend) = backend.lock() + #[cfg(feature = "nomic-v2")] { - return backend.model_name(); + "nomic-ai/nomic-embed-text-v2-moe" + } + #[cfg(not(feature = "nomic-v2"))] + { + "nomic-ai/nomic-embed-text-v1.5" } - - EmbeddingModelSpec::selected() - .unwrap_or(EmbeddingModelSpec::NomicV15) - .model_name() } /// Get the embedding dimensions @@ -395,7 +268,7 @@ impl EmbeddingService { )); } - let mut backend = get_backend()?; + let mut model = get_model()?; // Truncate if too long (char-boundary safe) let text = if text.len() > MAX_TEXT_LENGTH { @@ -408,15 +281,9 @@ impl EmbeddingService { text }; - let embeddings = match &mut *backend { - EmbeddingBackend::NomicV15(model) => model - .embed(vec![text], None) - .map_err(|e| EmbeddingError::EmbeddingFailed(e.to_string()))?, - #[cfg(feature = "qwen3-embeddings")] - EmbeddingBackend::Qwen3Embedding06B(model) => model - .embed(&[text]) - .map_err(|e| EmbeddingError::EmbeddingFailed(e.to_string()))?, - }; + let embeddings = model + .embed(vec![text], None) + .map_err(|e| EmbeddingError::EmbeddingFailed(e.to_string()))?; if embeddings.is_empty() { return Err(EmbeddingError::EmbeddingFailed( @@ -427,26 +294,13 @@ impl EmbeddingService { Ok(Embedding::new(matryoshka_truncate(embeddings[0].clone()))) } - /// Generate an embedding for retrieval queries. - /// - /// Qwen3 uses instruction-formatted queries against raw document embeddings; - /// Nomic remains symmetric and receives the query unchanged. - pub fn embed_query(&self, query: &str) -> Result { - if self.model_name().to_ascii_lowercase().contains("qwen3") { - let formatted = qwen3_format_query(query); - self.embed(&formatted) - } else { - self.embed(query) - } - } - /// Generate embeddings for multiple texts (batch processing) pub fn embed_batch(&self, texts: &[&str]) -> Result, EmbeddingError> { if texts.is_empty() { return Ok(vec![]); } - let mut backend = get_backend()?; + let mut model = get_model()?; let mut all_embeddings = Vec::with_capacity(texts.len()); // Process in batches for efficiency @@ -466,15 +320,9 @@ impl EmbeddingService { }) .collect(); - let embeddings = match &mut *backend { - EmbeddingBackend::NomicV15(model) => model - .embed(truncated, None) - .map_err(|e| EmbeddingError::EmbeddingFailed(e.to_string()))?, - #[cfg(feature = "qwen3-embeddings")] - EmbeddingBackend::Qwen3Embedding06B(model) => model - .embed(&truncated) - .map_err(|e| EmbeddingError::EmbeddingFailed(e.to_string()))?, - }; + let embeddings = model + .embed(truncated, None) + .map_err(|e| EmbeddingError::EmbeddingFailed(e.to_string()))?; for emb in embeddings { all_embeddings.push(Embedding::new(matryoshka_truncate(emb))); @@ -633,14 +481,6 @@ mod tests { } } - #[test] - fn test_qwen3_query_format() { - let formatted = qwen3_format_query("rust memory portability"); - assert!(formatted.starts_with("Instruct:")); - assert!(formatted.contains("retrieve relevant passages")); - assert!(formatted.ends_with("Query: rust memory portability")); - } - #[test] fn test_embedding_normalize() { let mut emb = Embedding::new(vec![3.0, 4.0]); diff --git a/crates/vestige-core/src/embeddings/mod.rs b/crates/vestige-core/src/embeddings/mod.rs index d38497b..5d89c10 100644 --- a/crates/vestige-core/src/embeddings/mod.rs +++ b/crates/vestige-core/src/embeddings/mod.rs @@ -13,7 +13,6 @@ mod code; mod hybrid; mod local; -#[cfg(feature = "vector-search")] pub(crate) use local::get_cache_dir; pub use local::{ BATCH_SIZE, EMBEDDING_DIMENSIONS, Embedding, EmbeddingError, EmbeddingService, MAX_TEXT_LENGTH, diff --git a/crates/vestige-core/src/fts.rs b/crates/vestige-core/src/fts.rs index e3de7bc..eae8ed8 100644 --- a/crates/vestige-core/src/fts.rs +++ b/crates/vestige-core/src/fts.rs @@ -22,24 +22,31 @@ pub fn sanitize_fts5_terms(query: &str) -> Option { sanitized = sanitized .chars() .map(|c| match c { - '*' | ':' | '^' | '-' | '"' | '(' | ')' | '{' | '}' | '[' | ']' | '.' | '/' | '\\' - | '=' | '@' => ' ', + '*' | ':' | '^' | '-' | '"' | '(' | ')' | '{' | '}' | '[' | ']' => ' ', _ => c, }) .collect(); - // Drop any token that IS a bare FTS5 boolean operator (AND/OR/NOT/NEAR), - // case-insensitively. Token-level filtering is complete and repetition-proof. - // The previous string-replace approach was single-pass and non-overlapping, - // so a doubled operator ("foo AND AND AND") left a bare operator at the edge - // or interior that leaked into the raw MATCH — either aborting with an FTS5 - // syntax error, or silently flipping implicit-AND to boolean-OR - // ("foo OR bar"). Filtering tokens cannot leak an operator regardless of how - // many are chained. - let terms: Vec<&str> = sanitized - .split_whitespace() - .filter(|t| !FTS5_OPERATORS.iter().any(|op| t.eq_ignore_ascii_case(op))) - .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; } @@ -47,43 +54,6 @@ pub fn sanitize_fts5_terms(query: &str) -> Option { Some(terms.join(" ")) } -/// Build a RECALL-friendly FTS5 query that matches rows containing ANY of the -/// query's tokens, each quoted as a phrase literal so punctuation/operators are -/// neutralized. Produces e.g. `"500" OR "internal" OR "server" OR "error"`. -/// -/// This is the correct default for natural-language similarity search: implicit -/// AND (the old behavior) requires every word — including "on"/"the" — to appear, -/// which silently drops near-matches; wrapping the whole string in one phrase -/// (the prior `sanitize_fts5_query`) requires the tokens to be adjacent and in -/// order, which drops nearly everything. OR + `ORDER BY rank` (BM25) ranks the -/// row sharing the most distinctive tokens first — true lexical resemblance. -/// -/// Per https://sqlite.org/fts5.html an embedded `"` is escaped by doubling it. -/// -/// Tokenization MUST mirror the index's `tokenize='porter ascii'` (migration V7): -/// the `ascii` tokenizer treats every non-ASCII-alphanumeric byte as a separator, -/// including `_` and any non-ASCII letter. So we split on `!is_ascii_alphanumeric` -/// — otherwise a query token like `API_TIMEOUT` or `café` becomes a single phrase -/// (`"api_timeout"` / `"café"`) that can NEVER match the index (which stored them -/// as `api`+`timeout` / `caf`). Per-token length is capped at 64 (the ascii -/// tokenizer's effective max token length) and token count at 64 to bound the -/// OR-chain. ASCII lowercasing mirrors the tokenizer's case-folding. -pub fn sanitize_fts5_or_query(query: &str) -> Option { - let limited: String = query.chars().take(1000).collect(); - let q: String = limited - .split(|c: char| !c.is_ascii_alphanumeric()) - .filter(|t| !t.is_empty()) - .take(64) // bound the OR-chain length (DoS hardening) - .map(|t| { - // mirror the ascii tokenizer: lowercase, cap at its max token length - let tok: String = t.chars().take(64).collect::().to_ascii_lowercase(); - format!("\"{}\"", tok.replace('"', "\"\"")) - }) - .collect::>() - .join(" OR "); - if q.is_empty() { None } else { Some(q) } -} - /// Sanitize input for FTS5 MATCH queries /// /// Prevents: @@ -98,13 +68,11 @@ pub fn sanitize_fts5_query(query: &str) -> String { // Remove FTS5 special characters and operators let mut sanitized = limited.to_string(); - // Remove special characters: * : ^ - " ( ) and common identifier/path - // punctuation that FTS5 otherwise treats as syntax. + // Remove special characters: * : ^ - " ( ) sanitized = sanitized .chars() .map(|c| match c { - '*' | ':' | '^' | '-' | '"' | '(' | ')' | '{' | '}' | '[' | ']' | '.' | '/' | '\\' - | '=' | '@' => ' ', + '*' | ':' | '^' | '-' | '"' | '(' | ')' | '{' | '}' | '[' | ']' => ' ', _ => c, }) .collect(); @@ -180,75 +148,4 @@ mod tests { let sanitized = sanitize_fts5_query(&long_query); assert!(sanitized.len() <= 1004); } - - // --- sanitize_fts5_or_query (rotation-audit-hardened) ------------------- - - #[test] - fn or_query_splits_like_ascii_tokenizer() { - // The index uses tokenize='porter ascii': '_' and non-ASCII are separators. - // API_TIMEOUT must become two tokens, lowercased — NOT one phrase that - // could never match the index. (Consensus finding, DeepSeek + MiniMax.) - let q = sanitize_fts5_or_query("API_TIMEOUT failed").unwrap(); - assert_eq!(q, "\"api\" OR \"timeout\" OR \"failed\""); - } - - #[test] - fn or_query_non_ascii_is_separated() { - // café -> the ascii tokenizer indexes "caf"; our query must not emit "café". - let q = sanitize_fts5_or_query("café").unwrap(); - assert_eq!(q, "\"caf\""); - } - - #[test] - fn or_query_neutralizes_fts5_operators_and_injection() { - // Operators/columns/wildcards are all separators -> stripped, then quoted. - let q = sanitize_fts5_or_query("title:secret OR a* -b \"x\"").unwrap(); - // every token is a quoted phrase literal; no bare operator survives except - // our own joining OR. An embedded quote is doubled. - assert!(q.contains("\"title\"")); - assert!(q.contains("\"secret\"")); - assert!(!q.contains("title:")); - assert!(!q.contains("a*")); - assert!(!q.contains("-b")); - } - - #[test] - fn or_query_empty_and_punctuation_only() { - assert_eq!(sanitize_fts5_or_query(""), None); - assert_eq!(sanitize_fts5_or_query(" "), None); - assert_eq!(sanitize_fts5_or_query(":-*^()"), None); - } - - #[test] - fn or_query_bounds_token_count_and_length() { - // DoS hardening: <=64 arms, each token <=64 chars. - let many = (0..500).map(|i| format!("t{i}")).collect::>().join(" "); - let q = sanitize_fts5_or_query(&many).unwrap(); - assert!(q.matches(" OR ").count() <= 63, "OR-chain must be bounded"); - let longtok = "a".repeat(200); - let q2 = sanitize_fts5_or_query(&longtok).unwrap(); - assert!(q2.len() <= 66, "single token capped at 64 + quotes, got {}", q2.len()); - } - - #[test] - fn terms_strips_all_bare_operators_even_when_doubled() { - // Regression: doubled/chained operators must never leak a bare operator - // into the raw MATCH (which aborts with a syntax error or silently flips - // implicit-AND to boolean-OR). - for op in FTS5_OPERATORS { - let doubled = format!("foo {op} {op} {op} bar"); - let out = sanitize_fts5_terms(&doubled).unwrap(); - for tok in out.split_whitespace() { - assert!( - !FTS5_OPERATORS.iter().any(|o| tok.eq_ignore_ascii_case(o)), - "operator {op} leaked into output {out:?}" - ); - } - assert_eq!(out, "foo bar", "only real terms should survive: {out:?}"); - } - // Lowercase + leading/trailing operators are stripped too. - assert_eq!(sanitize_fts5_terms("or foo and bar or").unwrap(), "foo bar"); - // Operator-only input yields nothing. - assert_eq!(sanitize_fts5_terms("AND OR NOT").map(|_| ()), None); - } } diff --git a/crates/vestige-core/src/lib.rs b/crates/vestige-core/src/lib.rs index f6f12b4..306b2d2 100644 --- a/crates/vestige-core/src/lib.rs +++ b/crates/vestige-core/src/lib.rs @@ -80,20 +80,12 @@ // MODULES // ============================================================================ -/// Optional `vestige.toml` configuration (Phase 2: Configurable Output). -pub mod config; -pub mod connectors; pub mod consolidation; -pub mod embedder; pub mod fsrs; pub mod fts; pub mod memory; pub mod storage; -/// Agent Black Box, Memory Receipts & Memory PRs — the cognitive flight -/// recorder, immune system, and reviewable-diff model for agent memory. -pub mod trace; - #[cfg(feature = "embeddings")] #[cfg_attr(docsrs, doc(cfg(feature = "embeddings")))] pub mod embeddings; @@ -135,12 +127,9 @@ pub use memory::{ MemorySystem, NodeType, RecallInput, - SchemaIntrospection, SearchMode, SearchResult, SimilarityResult, - SourceEnvelope, - TableIntrospection, TemporalRange, }; @@ -161,64 +150,10 @@ pub use fsrs::{ retrievability_with_decay, }; -// Configuration (vestige.toml output profiles / defaults) -pub use config::{CONFIG_FILE, OutputConfig, OutputDefaults, OutputProfile, VestigeConfig}; - -// Agent Black Box / Receipts / Memory PRs (the cognitive flight recorder) -pub use trace::{ - classify_write, DecayRisk, MemoryPr, MemoryPrAction, MemoryPrKind, MemoryPrStatus, - MemoryTraceEvent, Receipt, ReceiptMutation, ReviewMode, RiskClass, RiskSignal, SuppressReason, - SuppressedReceiptEntry, WriteContext, WriteSource, HIGH_TRUST_FLOOR, LOW_CONFIDENCE_FLOOR, -}; - // Storage layer pub use storage::{ - ClassificationResult, - CompositionEventRecord, - CompositionMemberRecord, - CompositionNeighborRecord, - CompositionOutcomeRecord, - ConnectionRecord, - ConnectorCursor, - ConsolidationHistoryRecord, - Domain, - DreamHistoryRecord, - HealthStatus, - InsightRecord, - IntentionRecord, - LocalMemoryStore, - MemoryEdge, - MemoryRecord, - MemoryStore, - MemoryStoreError, - MemoryStoreResult, - ModelSignature, - NeverComposedCandidate, - PORTABLE_ARCHIVE_FORMAT, - PortableArchive, - PortableImportMode, - PortableImportReport, - PortableSyncReport, - ReconcileReport, - Result, - SchedulingState, - SearchQuery, - AgentRunSummary, - SmartIngestResult, - SourceUpsertOutcome, - SourceUpsertResult, - SqliteMemoryStore, - StateTransitionRecord, - Storage, - StorageError, - StoreStats, - // Note: storage::SearchResult is intentionally not re-exported here to avoid - // collision with memory::SearchResult. Use vestige_core::storage::SearchResult directly. -}; - -// Embedder trait and implementations -pub use embedder::{ - Embedder, EmbedderError, EmbedderResult, EmbedderSend, FastembedEmbedder, LocalEmbedder, + ConnectionRecord, ConsolidationHistoryRecord, DreamHistoryRecord, InsightRecord, + IntentionRecord, Result, SmartIngestResult, StateTransitionRecord, Storage, StorageError, }; // Consolidation (sleep-inspired memory processing) @@ -277,9 +212,6 @@ pub use advanced::{ LabileState, Language, MaintenanceType, - // Merge / Supersede controls (Phase 3) - MatchClass, - MatchSignals, // Memory chains MemoryChainBuilder, // Memory compression @@ -290,15 +222,10 @@ pub use advanced::{ MemoryPath, MemoryReplay, MemorySnapshot, - MergeCandidate, - MergeOperation, - MergePlan, - MergePolicy, MergeStrategy, Modification, Pattern, PatternType, - PlanKind, PredictedMemory, PredictionContext, PredictionErrorConfig, diff --git a/crates/vestige-core/src/memory/mod.rs b/crates/vestige-core/src/memory/mod.rs index ab11ff7..e7c61d4 100644 --- a/crates/vestige-core/src/memory/mod.rs +++ b/crates/vestige-core/src/memory/mod.rs @@ -10,7 +10,7 @@ mod node; mod strength; mod temporal; -pub use node::{IngestInput, KnowledgeNode, NodeType, RecallInput, SearchMode, SourceEnvelope}; +pub use node::{IngestInput, KnowledgeNode, NodeType, RecallInput, SearchMode}; pub use strength::{DualStrength, StrengthDecay}; pub use temporal::{TemporalRange, TemporalValidity}; @@ -209,12 +209,9 @@ impl KnowledgeEdge { } } - /// Check if the edge is currently valid (within its [valid_from, valid_until) - /// window). Delegates to the bi-temporal check so a not-yet-active edge - /// (future valid_from) or an expired one is correctly reported invalid — - /// the old version only checked valid_until and ignored valid_from. + /// Check if the edge is currently valid pub fn is_valid(&self) -> bool { - self.was_valid_at(chrono::Utc::now()) + self.valid_until.is_none() } /// Check if the edge was valid at a given time @@ -250,14 +247,8 @@ pub struct MemoryStats { pub newest_memory: Option>, /// Number of nodes with semantic embeddings pub nodes_with_embeddings: i64, - /// Number of nodes with embeddings generated by the active embedding model family - pub nodes_with_active_embeddings: i64, - /// Number of nodes whose stored embeddings belong to a different model family - pub nodes_with_mismatched_embeddings: i64, /// Embedding model used (if any) pub embedding_model: Option, - /// Embedding model family currently configured for new queries and writes - pub active_embedding_model: Option, } impl Default for MemoryStats { @@ -271,67 +262,11 @@ impl Default for MemoryStats { oldest_memory: None, newest_memory: None, nodes_with_embeddings: 0, - nodes_with_active_embeddings: 0, - nodes_with_mismatched_embeddings: 0, embedding_model: None, - active_embedding_model: None, } } } -// ============================================================================ -// SCHEMA INTROSPECTION (v2.1.24+: surfaces DB shape to MCP consumers) -// ============================================================================ - -/// A single SQLite table's introspected shape: name, row count, column list. -/// -/// Returned as part of `SchemaIntrospection` from `Storage::schema_introspection()`. -/// Consumers needing more depth (e.g. per-column NULL counts) should request -/// targeted methods rather than expecting this struct to grow unboundedly — -/// the row + column shape covered here is the 80% case for audit / migration -/// guard scripts. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct TableIntrospection { - /// SQLite table name. - pub name: String, - /// Row count. - pub rows: i64, - /// Column names in declaration order. - pub columns: Vec, -} - -/// Result of `Storage::schema_introspection()`. Snapshots the schema version, -/// migration timestamp, and a row/column view of every user-data table. -/// -/// Motivation: external consumers (audit scripts, migration guards, downstream -/// upgrade scripts) currently must read SQLite directly to learn the schema -/// version and table shape, which couples them to internal layout. This struct -/// gives them a first-class MCP-callable surface. The list of tables walked is -/// intentionally the same canonical set used elsewhere in storage (the user- -/// data tables) so the surface stays stable across migrations. -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -#[serde(rename_all = "camelCase")] -pub struct SchemaIntrospection { - /// Current schema version (highest applied migration; matches the - /// `schema_version` table's MAX(version)). - pub schema_version: u32, - /// When the current schema version was applied (RFC3339), if known. - pub schema_version_applied_at: Option>, - /// Per-table introspection rows. - pub tables: Vec, - /// Total number of nodes whose `embeddings.embedding` is NULL (i.e., have - /// no embedding row). Convenience field for embedding-coverage audits; - /// equivalent to (knowledge_nodes.rows − rows in `embeddings` joined to - /// knowledge_nodes), so consumers don't have to compute it themselves. - pub embedding_null_count: i64, - /// Active embedding model name (mirrors `MemoryStats.active_embedding_model`). - /// Useful when an audit script wants schema_version + active model in one call. - pub active_embedding_model: Option, - /// Embedding dimensions for the active model, if known. - pub active_embedding_dimensions: Option, -} - // ============================================================================ // CONSOLIDATION RESULT // ============================================================================ @@ -362,10 +297,6 @@ pub struct ConsolidationResult { pub activations_computed: i64, /// Personalized w20 if optimized this cycle pub w20_optimized: Option, - /// Retroactive Salience Backfill: number of quiet earlier *causes* promoted - /// because a recent salient failure reached backward and surfaced them - /// (root-cause memories a semantic search would have missed). - pub backfilled_causes: i64, } // ============================================================================ diff --git a/crates/vestige-core/src/memory/node.rs b/crates/vestige-core/src/memory/node.rs index c400afc..9785387 100644 --- a/crates/vestige-core/src/memory/node.rs +++ b/crates/vestige-core/src/memory/node.rs @@ -79,91 +79,6 @@ impl std::fmt::Display for NodeType { } } -// ============================================================================ -// SOURCE ENVELOPE (#57) -// ============================================================================ - -/// Structured provenance for a memory that mirrors a record in an external -/// system of record (a Redmine issue, a GitHub Issue, a Jira ticket, a support -/// thread, …). -/// -/// The product boundary (#57): the external system stays canonical. Vestige -/// **indexes, connects, retrieves, and cites back**; it does not replace the -/// ticket tracker. This envelope carries exactly the fields a connector needs -/// to do that without leaking stale data: -/// -/// - `(source_system, source_id)` is the **idempotency key**. Re-running a sync -/// upserts the same logical record instead of duplicating it. -/// - `content_hash` is the **change detector**. If a re-fetched record hashes -/// to the stored value, the upsert is a no-op (only `synced_at` advances), -/// so an incremental re-scan never churns the index or the embedding model. -/// - `source_url` is the **citation**. Search results link back to the -/// canonical record so the agent can follow it for authoritative detail. -/// - `source_updated_at` is the **cursor field** the connector checkpoints on. -/// -/// Every field is optional at the type level so partial connectors and manual -/// imports can populate only what they have, but a real connector should always -/// set `source_system`, `source_id`, and `content_hash`. -#[non_exhaustive] -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SourceEnvelope { - /// External system this record came from, e.g. `redmine`, `github`, `jira`. - /// Namespaces `source_id` so two systems can share numeric ids. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub source_system: Option, - /// Stable native id in the source system (Redmine issue id, GitHub issue - /// number/node id, …). Combined with `source_system` it is the upsert key. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub source_id: Option, - /// Canonical URL back to the record so retrieval can cite the source. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub source_url: Option, - /// When the source record was last updated upstream (the connector cursor - /// field — Redmine `updated_on`, GitHub `updated_at`). RFC 3339. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub source_updated_at: Option>, - /// Stable hash of the normalized record content. Idempotency / change key. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub content_hash: Option, - /// When the connector last observed this record live. Drives tombstone - /// reconciliation (a record not seen in a full reconcile pass is gone). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub synced_at: Option>, - /// Project / repo / space the record belongs to (Redmine project, GitHub - /// `owner/repo`). Used for scoped sync and search filters. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub source_project: Option, - /// Record type within the source (`issue`, `comment`, `journal`, …). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub source_type: Option, - /// Author / reporter of the record in the source system. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub source_author: Option, -} - -impl SourceEnvelope { - /// True once the two fields a connector needs for idempotent upsert are - /// present. Manual imports that only set `source_url` are not "keyed". - pub fn has_key(&self) -> bool { - self.source_system.is_some() && self.source_id.is_some() - } - - /// True if every field is unset — used to collapse an all-`None` envelope - /// back to `None` on the node so legacy rows stay clean. - pub fn is_empty(&self) -> bool { - self.source_system.is_none() - && self.source_id.is_none() - && self.source_url.is_none() - && self.source_updated_at.is_none() - && self.content_hash.is_none() - && self.synced_at.is_none() - && self.source_project.is_none() - && self.source_type.is_none() - && self.source_author.is_none() - } -} - // ============================================================================ // KNOWLEDGE NODE // ============================================================================ @@ -273,15 +188,6 @@ pub struct KnowledgeNode { /// Timestamp of the most recent suppression (for 24h labile window). #[serde(skip_serializing_if = "Option::is_none")] pub suppressed_at: Option>, - - // ========== Source Envelope (#57, external-source connectors) ========== - /// Structured provenance for memories ingested from an external system - /// (Redmine, GitHub Issues, Jira, …). `None` for memories created directly - /// by an agent or the user — the legacy free-form `source` string above - /// remains the human-readable label; this envelope is the machine-readable, - /// idempotency- and citation-bearing record. See [`SourceEnvelope`]. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub source_envelope: Option, } impl Default for KnowledgeNode { @@ -318,7 +224,6 @@ impl Default for KnowledgeNode { embedding_model: None, suppression_count: 0, suppressed_at: None, - source_envelope: None, } } } @@ -386,11 +291,6 @@ pub struct IngestInput { /// When this knowledge stops being valid #[serde(skip_serializing_if = "Option::is_none")] pub valid_until: Option>, - /// Structured provenance for connector-ingested records (#57). When set - /// with a `(source_system, source_id)` key, callers should route through - /// `upsert_by_source` for idempotent sync rather than plain `ingest`. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub source_envelope: Option, } impl Default for IngestInput { @@ -404,7 +304,6 @@ impl Default for IngestInput { tags: vec![], valid_from: None, valid_until: None, - source_envelope: None, } } } diff --git a/crates/vestige-core/src/memory/temporal.rs b/crates/vestige-core/src/memory/temporal.rs index 166bb06..bdf8965 100644 --- a/crates/vestige-core/src/memory/temporal.rs +++ b/crates/vestige-core/src/memory/temporal.rs @@ -245,67 +245,4 @@ mod tests { assert!(!TemporalValidity::Past.is_valid()); assert!(!TemporalValidity::Future.is_valid()); } - - #[test] - fn test_range_from() { - let now = Utc::now(); - let start = now + Duration::days(10); - let range = TemporalRange::from(start); - assert_eq!(range.start, Some(start)); - assert_eq!(range.end, None); - assert!(range.contains(start)); - assert!(!range.contains(start - Duration::days(1))); - assert!(range.contains(start + Duration::days(1000))); - assert_eq!(range.duration(), None); - } - - #[test] - fn test_range_until() { - let now = Utc::now(); - let end = now + Duration::days(20); - let range = TemporalRange::until(end); - assert_eq!(range.start, None); - assert_eq!(range.end, Some(end)); - assert!(range.contains(end)); - assert!(!range.contains(end + Duration::days(1))); - assert!(range.contains(now - Duration::days(100))); - assert_eq!(range.duration(), None); - } - - #[test] - fn test_range_all() { - let now = Utc::now(); - let range = TemporalRange::all(); - assert_eq!(range.start, None); - assert_eq!(range.end, None); - assert!(range.contains(now)); - assert!(range.contains(now + Duration::days(100_000))); - assert_eq!(range.duration(), None); - } - - #[test] - fn test_range_duration() { - let now = Utc::now(); - let start = now + Duration::days(5); - let end = now + Duration::days(15); - let range = TemporalRange::between(start, end); - assert_eq!(range.duration(), Some(Duration::days(10))); - let from_range = TemporalRange::from(start); - assert_eq!(from_range.duration(), None); - let until_range = TemporalRange::until(end); - assert_eq!(until_range.duration(), None); - } - - #[test] - fn test_range_default() { - let now = Utc::now(); - let default_range = TemporalRange::default(); - let all_range = TemporalRange::all(); - assert_eq!(default_range.start, None); - assert_eq!(default_range.end, None); - assert_eq!(default_range.start, all_range.start); - assert_eq!(default_range.end, all_range.end); - assert!(default_range.contains(now)); - assert_eq!(default_range.contains(now), all_range.contains(now)); - } } diff --git a/crates/vestige-core/src/neuroscience/emotional_memory.rs b/crates/vestige-core/src/neuroscience/emotional_memory.rs index 63dab5e..2f7dc86 100644 --- a/crates/vestige-core/src/neuroscience/emotional_memory.rs +++ b/crates/vestige-core/src/neuroscience/emotional_memory.rs @@ -302,9 +302,6 @@ impl EmotionalMemory { arousal_score: f64, ) -> EmotionalEvaluation { let mut eval = self.evaluate_content(content); - // evaluate_content already counted a flashbulb if IT detected one. Capture - // that so we can reconcile after the importance-based override below. - let inner_flashbulb = eval.is_flashbulb; // Override flashbulb detection with real importance scores eval.is_flashbulb = novelty_score >= FLASHBULB_NOVELTY_THRESHOLD @@ -313,12 +310,8 @@ impl EmotionalMemory { // Blend arousal from lexicon with importance arousal eval.arousal = (eval.arousal * 0.4 + arousal_score * 0.6).clamp(0.0, 1.0); - // Reconcile the counter to the FINAL decision (the old code only ever - // incremented once, when the counter happened to be 0 — undercounting). - match (inner_flashbulb, eval.is_flashbulb) { - (false, true) => self.flashbulbs_detected += 1, // override promoted it - (true, false) => self.flashbulbs_detected = self.flashbulbs_detected.saturating_sub(1), // override demoted the inner count - _ => {} // unchanged: inner count already correct + if eval.is_flashbulb && self.flashbulbs_detected == 0 { + self.flashbulbs_detected += 1; } eval diff --git a/crates/vestige-core/src/neuroscience/hippocampal_index.rs b/crates/vestige-core/src/neuroscience/hippocampal_index.rs index e66f074..5e1a367 100644 --- a/crates/vestige-core/src/neuroscience/hippocampal_index.rs +++ b/crates/vestige-core/src/neuroscience/hippocampal_index.rs @@ -1146,14 +1146,7 @@ impl ContentStore { } } - // insert() returns the previous value when the key already existed; - // subtract its size so the counter reflects a REPLACE, not an ADD. - // Without this the counter drifts monotonically upward on every - // overwrite and evicts far too aggressively. - let replaced = cache.insert(key.to_string(), data.to_vec()); - if let Some(old) = replaced { - *size = size.saturating_sub(old.len()); - } + cache.insert(key.to_string(), data.to_vec()); *size += data_size; } } @@ -1299,13 +1292,6 @@ impl HippocampalIndex { } /// Index a new memory - /// - /// If `memory_id` is already indexed (a smart_ingest update/reinforce/replace - /// re-indexes the same node), the existing entry's barcode, association links, - /// and access history are PRESERVED — only the content-derived fields (preview, - /// semantic summary) are refreshed. Previously this always minted a new barcode - /// and inserted a fresh `MemoryIndex`, silently dropping every accumulated - /// association and the node's history on each re-index. pub fn index_memory( &self, memory_id: &str, @@ -1314,27 +1300,7 @@ impl HippocampalIndex { created_at: DateTime, semantic_embedding: Option>, ) -> Result { - let preview: String = content.chars().take(100).collect(); - let new_summary = semantic_embedding - .as_ref() - .map(|e| self.compress_embedding(e)); - - // Re-index path: refresh content fields in place, keep barcode + links. - { - let mut indices = self - .indices - .write() - .map_err(|e| HippocampalIndexError::LockError(e.to_string()))?; - if let Some(existing) = indices.get_mut(memory_id) { - existing.preview = preview; - if let Some(summary) = new_summary { - existing.semantic_summary = summary; - } - return Ok(existing.barcode); - } - } - - // First-time index: mint a barcode and create a fresh entry. + // Generate barcode let barcode = { let mut generator = self .barcode_generator @@ -1343,6 +1309,10 @@ impl HippocampalIndex { generator.generate(content, created_at) }; + // Create preview + let preview: String = content.chars().take(100).collect(); + + // Create index entry let mut index = MemoryIndex::new( barcode, memory_id.to_string(), @@ -1351,7 +1321,9 @@ impl HippocampalIndex { preview, ); - if let Some(summary) = new_summary { + // Compress embedding if provided + if let Some(embedding) = semantic_embedding { + let summary = self.compress_embedding(&embedding); index.semantic_summary = summary; } @@ -2004,22 +1976,15 @@ impl HippocampalIndex { return Ok(0); } - // Snapshot the (id, summary) pairs under the read lock, then DROP the lock - // before the O(n) cosine scan. Holding the read lock across the whole scan - // blocks all writers (index_memory/add_association/migrate_node) for its - // full duration on a large index. - let source_summary = source.semantic_summary.clone(); - let pairs: Vec<(String, Vec)> = indices - .iter() - .filter(|(id, index)| id.as_str() != memory_id && !index.semantic_summary.is_empty()) - .map(|(id, index)| (id.clone(), index.semantic_summary.clone())) - .collect(); - drop(indices); // release read lock BEFORE the expensive scan - - // Find similar memories (lock-free) + // Find similar memories let mut candidates: Vec<(String, f32)> = Vec::new(); - for (id, summary) in &pairs { - let similarity = self.cosine_similarity(&source_summary, summary); + for (id, index) in indices.iter() { + if id == memory_id || index.semantic_summary.is_empty() { + continue; + } + + let similarity = + self.cosine_similarity(&source.semantic_summary, &index.semantic_summary); if similarity >= similarity_threshold { candidates.push((id.clone(), similarity)); } @@ -2029,6 +1994,8 @@ impl HippocampalIndex { candidates.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); candidates.truncate(max_associations); + drop(indices); // Release read lock + // Add associations let mut added = 0; for (target_id, strength) in candidates { diff --git a/crates/vestige-core/src/neuroscience/importance_signals.rs b/crates/vestige-core/src/neuroscience/importance_signals.rs index 69ba428..26da186 100644 --- a/crates/vestige-core/src/neuroscience/importance_signals.rs +++ b/crates/vestige-core/src/neuroscience/importance_signals.rs @@ -366,12 +366,9 @@ impl PredictionModel { *total += 1; } - // Prune if too large, then reconcile total_count to the surviving - // sum. The old code left total_count inflated after pruning/decay, - // which drove computed novelty (count/total) toward zero over time. + // Prune if too large if patterns.len() > MAX_PREDICTION_PATTERNS { self.apply_decay(&mut patterns); - *total = patterns.values().map(|c| *c as u64).sum(); } } } @@ -1365,12 +1362,7 @@ impl RewardPattern { fn matches(&self, tags: &[String]) -> bool { let tag_set: HashSet<_> = tags.iter().cloned().collect(); let overlap = self.tags.intersection(&tag_set).count(); - // Require at least one shared tag AND at least half of the smaller set to - // overlap. The previous integer-division threshold - // (`min(a,b).max(1) / 2`) evaluated to 0 for small sets, so `overlap >= 0` - // matched EVERY pattern — including disjoint or empty tag sets. - let smaller = self.tags.len().min(tag_set.len()); - overlap >= 1 && overlap * 2 >= smaller + overlap >= self.tags.len().min(tag_set.len()).max(1) / 2 } fn update(&mut self, reward: f64) { diff --git a/crates/vestige-core/src/neuroscience/predictive_retrieval.rs b/crates/vestige-core/src/neuroscience/predictive_retrieval.rs index bb26ec3..77934e1 100644 --- a/crates/vestige-core/src/neuroscience/predictive_retrieval.rs +++ b/crates/vestige-core/src/neuroscience/predictive_retrieval.rs @@ -844,17 +844,6 @@ impl PredictiveMemory { .write() .map_err(|e| PredictiveMemoryError::LockPoisoned(e.to_string()))?; - // Bound the metadata cache like every sibling cache in this module. - // Without a cap it grows unbounded (one entry per memory ever ingested), - // a slow leak in a long-running process. This is a best-effort prediction - // cache, so evicting an arbitrary entry when over the limit is fine. - const MAX_METADATA_ENTRIES: usize = 10_000; - if metadata.len() >= MAX_METADATA_ENTRIES - && !metadata.contains_key(memory_id) - && let Some(evict) = metadata.keys().next().cloned() - { - metadata.remove(&evict); - } metadata.insert( memory_id.to_string(), (content_preview.to_string(), tags.to_vec()), @@ -943,19 +932,12 @@ impl PredictiveMemory { /// Get proactive suggestions ("You might also need...") pub fn get_proactive_suggestions(&self, min_confidence: f64) -> Result> { - // Clone the context and DROP the read guard before calling - // predict_needed_memories, which re-acquires user_model.read(). A - // re-entrant read on the same thread can deadlock under std::sync::RwLock - // when a writer is queued between the two acquisitions. - let session_context = { - let model = self - .user_model - .read() - .map_err(|e| PredictiveMemoryError::LockPoisoned(e.to_string()))?; - model.session_context.clone() - }; + let model = self + .user_model + .read() + .map_err(|e| PredictiveMemoryError::LockPoisoned(e.to_string()))?; - let predictions = self.predict_needed_memories(&session_context)?; + let predictions = self.predict_needed_memories(&model.session_context)?; Ok(predictions .into_iter() @@ -1302,26 +1284,13 @@ impl PredictiveMemory { fn merge_predictions(&self, predictions: Vec) -> Vec { let mut merged: HashMap = HashMap::new(); - let min_conf = self.config.min_confidence; for pred in predictions { merged .entry(pred.memory_id.clone()) .and_modify(|existing| { - // Combine confidence scores: take the max, with a small boost - // for multiple corroborating signals. The boost must NOT - // manufacture a threshold crossing — two individually - // sub-min_confidence signals should stay sub-threshold rather - // than combining (via *1.1) into a passing score. So the boost - // is capped at min_confidence when the strongest signal is - // itself below it. - let best = existing.confidence.max(pred.confidence); - let boosted = (best * 1.1).min(1.0); - existing.confidence = if best < min_conf { - boosted.min(min_conf - f64::EPSILON).max(best) - } else { - boosted - }; + // Combine confidence scores (taking max, with a small boost for multiple signals) + existing.confidence = (existing.confidence.max(pred.confidence) * 1.1).min(1.0); }) .or_insert(pred); } diff --git a/crates/vestige-core/src/neuroscience/prospective_memory.rs b/crates/vestige-core/src/neuroscience/prospective_memory.rs index 6793780..133f17d 100644 --- a/crates/vestige-core/src/neuroscience/prospective_memory.rs +++ b/crates/vestige-core/src/neuroscience/prospective_memory.rs @@ -412,35 +412,6 @@ impl IntentionTrigger { } } - /// Advance a recurring trigger to its next occurrence after it fires, so it - /// re-arms instead of staying perpetually triggered. Returns true if the - /// trigger is recurring and was advanced (the intention should return to - /// Active), false for one-shot triggers (which stay fulfilled/triggered). - /// Recurs into Compound so a recurring sub-trigger inside a compound re-arms. - pub fn re_arm(&mut self, now: DateTime) -> bool { - match self { - Self::Recurring { - recurrence, - next_occurrence, - .. - } => { - // Advance from the later of "now" and the slot that just fired, - // so we never re-arm into an already-past occurrence. - let from = next_occurrence.map(|t| t.max(now)).unwrap_or(now); - *next_occurrence = Some(recurrence.next_occurrence(from)); - true - } - Self::Compound { all_of, any_of } => { - let mut any = false; - for t in all_of.iter_mut().chain(any_of.iter_mut()) { - any |= t.re_arm(now); - } - any - } - _ => false, - } - } - /// Get a human-readable description of the trigger pub fn description(&self) -> String { match self { @@ -737,18 +708,9 @@ impl Intention { /// Mark as triggered pub fn mark_triggered(&mut self) { - let now = Utc::now(); + self.status = IntentionStatus::Triggered; self.reminder_count += 1; - self.last_reminded_at = Some(now); - // A recurring intention re-arms to its next occurrence and returns to - // Active so it fires again; a one-shot intention stays Triggered. - // Previously recurring intentions never advanced next_occurrence, so they - // fired once and never again (or stayed perpetually triggered). - if self.trigger.re_arm(now) { - self.status = IntentionStatus::Active; - } else { - self.status = IntentionStatus::Triggered; - } + self.last_reminded_at = Some(Utc::now()); } /// Mark as fulfilled @@ -1026,22 +988,10 @@ impl IntentionParser { } } - // Check for "at X" time pattern. Find " at " case-insensitively but with a - // byte index that is valid in `original`: to_lowercase() can change byte - // length (e.g. 'ẞ' U+1E9E 3 bytes -> 'ß' U+00DF 2 bytes), so an index into - // `text_lower` can land mid-char in `original` and panic when used to slice - // it. Scanning `original`'s own char indices keeps the split boundary valid. - // Match on raw bytes to stay boundary-safe: " at " is pure ASCII, so a - // byte-window match is guaranteed to fall on char boundaries in `original`. - let bytes = original.as_bytes(); - let at_idx = bytes.windows(4).position(|w| { - (w[0] == b' ') - && (w[1] == b'a' || w[1] == b'A') - && (w[2] == b't' || w[2] == b'T') - && (w[3] == b' ') - }); - if let Some(idx) = at_idx { - let parts: Vec<&str> = vec![&original[..idx], &original[idx + 4..]]; + // Check for "at X" time pattern + if text_lower.contains(" at ") { + // For now, treat as a simple event trigger + let parts: Vec<&str> = original.splitn(2, " at ").collect(); if parts.len() == 2 { let part0_lower = parts[0].to_lowercase(); let content: String = if part0_lower.starts_with("remind me to ") { @@ -1329,7 +1279,6 @@ impl ProspectiveMemory { } // Check if triggered - let mut just_triggered = false; if intention .trigger .is_triggered(context, &context.recent_events) @@ -1337,24 +1286,18 @@ impl ProspectiveMemory { { intention.mark_triggered(); triggered.push(intention.clone()); - just_triggered = true; } // Check for deadline escalation if self.config.enable_escalation { - // try_hours avoids the panic Duration::hours has on an - // out-of-range (user-configurable) value; fall back to a large - // but safe default if the config is absurd. - let threshold = Duration::try_hours(self.config.escalation_threshold_hours) - .unwrap_or_else(|| Duration::hours(24)); + let threshold = Duration::hours(self.config.escalation_threshold_hours); if intention.is_deadline_approaching(threshold) { // Priority will be automatically escalated via effective_priority() } } - // Auto-expire overdue intentions — but never clobber one we JUST - // triggered this iteration (it should fire before it expires). - if self.config.auto_expire && intention.is_overdue() && !just_triggered { + // Auto-expire overdue intentions + if self.config.auto_expire && intention.is_overdue() { intention.status = IntentionStatus::Expired; } } @@ -1403,11 +1346,9 @@ impl ProspectiveMemory { history.push_back(fulfilled_intention); - // Maintain history size. try_days avoids the panic on an - // out-of-range user-configurable retention value. - let retention_window = Duration::try_days(self.config.completed_retention_days) - .unwrap_or_else(|| Duration::days(30)); - let retention_cutoff = Utc::now() - retention_window; + // Maintain history size + let retention_cutoff = + Utc::now() - Duration::days(self.config.completed_retention_days); while history .front() .map(|i| i.fulfilled_at.unwrap_or(i.created_at) < retention_cutoff) @@ -1734,33 +1675,6 @@ mod tests { assert!((next - now) == Duration::hours(2)); } - #[test] - fn test_recurring_intention_re_arms_after_firing() { - // Regression (#124): a recurring intention must re-arm (advance - // next_occurrence + return to Active) each time it fires, not fire once - // and stay Triggered forever. - let now = Utc::now(); - let trigger = IntentionTrigger::Recurring { - base: Box::new(IntentionTrigger::at_time(now - Duration::minutes(1))), - recurrence: RecurrencePattern::EveryHours(2), - next_occurrence: Some(now - Duration::minutes(1)), // already due - }; - let mut intention = Intention::new("recurring task", trigger); - - // Due now, so it triggers. - assert!(intention.trigger.is_triggered(&Context::default(), &[])); - intention.mark_triggered(); - - // After firing it must be Active again (re-armed), not stuck Triggered. - assert_eq!(intention.status, IntentionStatus::Active); - // And next_occurrence must have advanced into the future so it is no - // longer immediately due. - assert!( - !intention.trigger.is_triggered(&Context::default(), &[]), - "recurring trigger must not stay perpetually due after re-arming" - ); - } - #[test] fn test_stats() { let pm = ProspectiveMemory::new(); diff --git a/crates/vestige-core/src/neuroscience/synaptic_tagging.rs b/crates/vestige-core/src/neuroscience/synaptic_tagging.rs index 0bd3339..c5e9ca1 100644 --- a/crates/vestige-core/src/neuroscience/synaptic_tagging.rs +++ b/crates/vestige-core/src/neuroscience/synaptic_tagging.rs @@ -369,25 +369,14 @@ impl CaptureWindow { } } - /// Get the start of the capture window. - /// - /// Uses checked Duration + subtraction: `Duration::minutes` panics for an - /// out-of-range magnitude and the subtraction can overflow the DateTime - /// range, so an unbounded caller-supplied `backward_hours` (via the - /// `trigger_importance` MCP tool) would otherwise abort the server. On - /// overflow we saturate to the minimum representable time. + /// Get the start of the capture window pub fn window_start(&self, event_time: DateTime) -> DateTime { - Duration::try_minutes((self.backward_hours * 60.0) as i64) - .and_then(|d| event_time.checked_sub_signed(d)) - .unwrap_or(DateTime::::MIN_UTC) + event_time - Duration::minutes((self.backward_hours * 60.0) as i64) } - /// Get the end of the capture window. See `window_start` for the overflow - /// rationale; on overflow we saturate to the maximum representable time. + /// Get the end of the capture window pub fn window_end(&self, event_time: DateTime) -> DateTime { - Duration::try_minutes((self.forward_hours * 60.0) as i64) - .and_then(|d| event_time.checked_add_signed(d)) - .unwrap_or(DateTime::::MAX_UTC) + event_time + Duration::minutes((self.forward_hours * 60.0) as i64) } /// Check if a time is within the capture window diff --git a/crates/vestige-core/src/search/vector.rs b/crates/vestige-core/src/search/vector.rs index 61050d1..069dd9a 100644 --- a/crates/vestige-core/src/search/vector.rs +++ b/crates/vestige-core/src/search/vector.rs @@ -137,7 +137,7 @@ impl VectorIndex { let options = IndexOptions { dimensions: config.dimensions, metric: config.metric, - quantization: ScalarKind::F32, + quantization: ScalarKind::I8, connectivity: config.connectivity, expansion_add: config.expansion_add, expansion_search: config.expansion_search, @@ -325,7 +325,7 @@ impl VectorIndex { let options = IndexOptions { dimensions: config.dimensions, metric: config.metric, - quantization: ScalarKind::F32, + quantization: ScalarKind::I8, connectivity: config.connectivity, expansion_add: config.expansion_add, expansion_search: config.expansion_search, diff --git a/crates/vestige-core/src/storage/cloud_crypto.rs b/crates/vestige-core/src/storage/cloud_crypto.rs deleted file mode 100644 index 5d88045..0000000 --- a/crates/vestige-core/src/storage/cloud_crypto.rs +++ /dev/null @@ -1,164 +0,0 @@ -//! Zero-knowledge client-side encryption for Vestige Cloud sync. -//! -//! The portable archive is encrypted on the client **before** it is uploaded -//! and decrypted **after** it is downloaded, so the hosted service only ever -//! stores ciphertext. The encryption passphrase is supplied by the user -//! (`VESTIGE_CLOUD_ENCRYPTION_KEY`) and is **never** sent to the server — it is -//! independent of the bearer sync key. This is what makes the "we hold no keys" -//! guarantee literally true: a server breach yields only random noise. -//! -//! Construction: -//! - KDF: Argon2id over (passphrase, random 16-byte salt) → 32-byte key. -//! - AEAD: XChaCha20-Poly1305 (192-bit nonce) over the archive bytes. -//! - Envelope (all non-secret framing prepended to the ciphertext): -//! `MAGIC(8) | VERSION(1) | salt(16) | nonce(24) | ciphertext+tag` -//! -//! Tradeoff (by design, and a selling point): if the user loses the passphrase, -//! the synced data is unrecoverable. We cannot reset it — we never have it. - -use argon2::Argon2; -use chacha20poly1305::aead::rand_core::RngCore; -use chacha20poly1305::aead::{Aead, AeadCore, KeyInit, OsRng}; -use chacha20poly1305::{Key, XChaCha20Poly1305, XNonce}; - -use super::sqlite::{Result, StorageError}; - -/// Magic marker identifying a Vestige zero-knowledge envelope. -const MAGIC: &[u8; 8] = b"VSTGENC1"; -const VERSION: u8 = 1; -const SALT_LEN: usize = 16; -const NONCE_LEN: usize = 24; // XChaCha20-Poly1305 nonce is 192-bit. -const KEY_LEN: usize = 32; -const HEADER_LEN: usize = MAGIC.len() + 1 + SALT_LEN + NONCE_LEN; - -/// Derive a 32-byte key from the passphrase and salt using Argon2id (defaults). -fn derive_key(passphrase: &[u8], salt: &[u8]) -> Result<[u8; KEY_LEN]> { - let mut key = [0u8; KEY_LEN]; - Argon2::default() - .hash_password_into(passphrase, salt, &mut key) - .map_err(|e| StorageError::Init(format!("key derivation failed: {e}")))?; - Ok(key) -} - -/// Encrypt `plaintext` under `passphrase`, returning the self-describing envelope. -/// -/// A fresh random salt and nonce are generated per call, so re-encrypting the -/// same archive yields different ciphertext (no deterministic leakage). -pub fn encrypt(passphrase: &str, plaintext: &[u8]) -> Result> { - let mut salt = [0u8; SALT_LEN]; - OsRng.fill_bytes(&mut salt); - - let key_bytes = derive_key(passphrase.as_bytes(), &salt)?; - let cipher = XChaCha20Poly1305::new(Key::from_slice(&key_bytes)); - let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng); - - let ciphertext = cipher - .encrypt(&nonce, plaintext) - .map_err(|_| StorageError::Init("encryption failed".to_string()))?; - - let mut out = Vec::with_capacity(HEADER_LEN + ciphertext.len()); - out.extend_from_slice(MAGIC); - out.push(VERSION); - out.extend_from_slice(&salt); - out.extend_from_slice(nonce.as_slice()); - out.extend_from_slice(&ciphertext); - Ok(out) -} - -/// True if `bytes` start with the Vestige encryption magic. Lets the sync -/// backend distinguish an encrypted envelope from a legacy plaintext archive. -pub fn is_encrypted(bytes: &[u8]) -> bool { - bytes.len() >= MAGIC.len() && &bytes[..MAGIC.len()] == MAGIC -} - -/// Decrypt a Vestige envelope under `passphrase`. Fails on a wrong passphrase or -/// any tampering (the Poly1305 tag is verified). -pub fn decrypt(passphrase: &str, envelope: &[u8]) -> Result> { - if envelope.len() < HEADER_LEN { - return Err(StorageError::Init( - "cloud archive is too short to be a valid encrypted envelope".to_string(), - )); - } - if &envelope[..MAGIC.len()] != MAGIC { - return Err(StorageError::Init( - "cloud archive is not a Vestige encrypted envelope".to_string(), - )); - } - let version = envelope[MAGIC.len()]; - if version != VERSION { - return Err(StorageError::Init(format!( - "unsupported cloud encryption version {version}" - ))); - } - - let salt_start = MAGIC.len() + 1; - let nonce_start = salt_start + SALT_LEN; - let ct_start = nonce_start + NONCE_LEN; - let salt = &envelope[salt_start..nonce_start]; - let nonce = XNonce::from_slice(&envelope[nonce_start..ct_start]); - let ciphertext = &envelope[ct_start..]; - - let key_bytes = derive_key(passphrase.as_bytes(), salt)?; - let cipher = XChaCha20Poly1305::new(Key::from_slice(&key_bytes)); - - cipher.decrypt(nonce, ciphertext).map_err(|_| { - StorageError::Init( - "cloud decryption failed: wrong VESTIGE_CLOUD_ENCRYPTION_KEY or corrupted data" - .to_string(), - ) - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn round_trip() { - let pass = "correct horse battery staple"; - let msg = b"the entire cognitive graph, in plaintext, before upload"; - let env = encrypt(pass, msg).unwrap(); - assert!(is_encrypted(&env)); - assert_ne!(&env[..], &msg[..], "envelope must not contain plaintext"); - let back = decrypt(pass, &env).unwrap(); - assert_eq!(back, msg); - } - - #[test] - fn wrong_passphrase_fails() { - let env = encrypt("right-pass", b"secret").unwrap(); - assert!(decrypt("wrong-pass", &env).is_err()); - } - - #[test] - fn tamper_is_detected() { - let mut env = encrypt("pass", b"important memory").unwrap(); - // Flip a byte in the ciphertext region. - let last = env.len() - 1; - env[last] ^= 0xff; - assert!(decrypt("pass", &env).is_err(), "AEAD must reject tampering"); - } - - #[test] - fn ciphertext_is_nondeterministic() { - // Same input encrypted twice → different envelopes (random salt+nonce). - let a = encrypt("p", b"x").unwrap(); - let b = encrypt("p", b"x").unwrap(); - assert_ne!(a, b); - // Both still decrypt correctly. - assert_eq!(decrypt("p", &a).unwrap(), b"x"); - assert_eq!(decrypt("p", &b).unwrap(), b"x"); - } - - #[test] - fn plaintext_is_not_misdetected_as_envelope() { - assert!(!is_encrypted(b"{\"archiveFormat\":\"vestige.portable.v1\"}")); - assert!(!is_encrypted(b"")); - } - - #[test] - fn rejects_short_or_foreign_envelope() { - assert!(decrypt("p", b"too short").is_err()); - assert!(decrypt("p", &[0u8; 100]).is_err()); - } -} diff --git a/crates/vestige-core/src/storage/cloud_sync.rs b/crates/vestige-core/src/storage/cloud_sync.rs deleted file mode 100644 index e30c525..0000000 --- a/crates/vestige-core/src/storage/cloud_sync.rs +++ /dev/null @@ -1,391 +0,0 @@ -//! Hosted managed-sync backend (Vestige Cloud). -//! -//! This module is only compiled with the `cloud-sync` feature. It provides -//! [`HttpPortableSyncBackend`], an HTTP implementation of the -//! [`PortableSyncBackend`](super::sqlite::PortableSyncBackend) trait that -//! pull-merge-pushes the portable archive to a hosted blob endpoint. -//! -//! The merge/conflict engine is unchanged: this backend only moves bytes. The -//! authoritative `key -> namespace` mapping and per-user isolation live in the -//! hosted service; the client just presents an opaque sync key as a bearer -//! token. The default local-first build never links an HTTP client. -//! -//! ## Concurrency -//! -//! Two devices can each pull → merge → push. To avoid a lost update in the -//! GET↔PUT window, the backend uses optimistic concurrency: it captures the -//! object `ETag` on read and sends it as `If-Match` on write. The generic -//! [`sync_portable_archive`](super::sqlite::SqliteMemoryStore::sync_portable_archive) -//! driver calls `read_archive` then `write_archive` exactly once, so the ETag -//! captured during the pull is the precondition for the push. A -//! `412 Precondition Failed` means another device wrote in between; the caller -//! re-runs sync (the merge is idempotent and converges by `updated_at`). - -use std::cell::RefCell; -use std::time::Duration; - -use reqwest::blocking::Client; -use reqwest::header::{AUTHORIZATION, ETAG, IF_MATCH}; -use reqwest::StatusCode; - -use super::portable::PortableArchive; -use super::sqlite::{PortableSyncBackend, Result, StorageError}; - -/// Default request timeout for cloud sync HTTP calls. -const REQUEST_TIMEOUT: Duration = Duration::from_secs(60); - -/// Blob path on the hosted service. One opaque blob per sync key (the service -/// derives the namespace from the key), so the client uses a fixed path. -const BLOB_PATH: &str = "/v1/blob"; - -/// HTTP-backed portable sync backend for Vestige Cloud. -/// -/// Mirrors the shape of -/// [`FilePortableSyncBackend`](super::sqlite::FilePortableSyncBackend) but reads -/// and writes the archive over HTTPS with a per-user bearer key. -#[derive(Debug)] -pub struct HttpPortableSyncBackend { - /// Base endpoint, e.g. `https://sync.vestige.dev`. No trailing slash. - endpoint: String, - /// Per-user sync key, presented as `Authorization: Bearer `. - sync_key: String, - /// Optional zero-knowledge passphrase. When set, the archive is encrypted - /// before upload and decrypted after download — the server never sees - /// plaintext, and this passphrase is never sent to the server. - encryption_key: Option, - /// Blocking HTTP client (the trait is synchronous). - client: Client, - /// ETag captured on the most recent successful read, used as the `If-Match` - /// precondition on the next write. `None` until the first read, or when the - /// remote had no archive yet. - last_etag: RefCell>, -} - -impl HttpPortableSyncBackend { - /// Build a cloud sync backend for `endpoint` authenticated with `sync_key`, - /// with no client-side encryption (plaintext upload). - /// - /// A trailing slash on `endpoint` is trimmed so URL joining is predictable. - pub fn new(endpoint: impl Into, sync_key: impl Into) -> Result { - Self::new_with_encryption(endpoint, sync_key, None) - } - - /// Build a cloud sync backend with optional zero-knowledge encryption. - /// - /// When `encryption_key` is `Some`, the portable archive is encrypted with - /// XChaCha20-Poly1305 (Argon2id-derived key) before upload and decrypted on - /// download. The passphrase never leaves this process. - pub fn new_with_encryption( - endpoint: impl Into, - sync_key: impl Into, - encryption_key: Option, - ) -> Result { - let endpoint = endpoint.into().trim_end_matches('/').to_string(); - let sync_key = sync_key.into(); - if endpoint.is_empty() { - return Err(StorageError::Init( - "cloud sync endpoint is empty (set VESTIGE_CLOUD_ENDPOINT)".to_string(), - )); - } - if sync_key.is_empty() { - return Err(StorageError::Init( - "cloud sync key is empty (set VESTIGE_CLOUD_SYNC_KEY)".to_string(), - )); - } - let encryption_key = encryption_key.filter(|k| !k.is_empty()); - let client = Client::builder() - .timeout(REQUEST_TIMEOUT) - .user_agent(concat!("vestige-cloud-sync/", env!("CARGO_PKG_VERSION"))) - .build() - .map_err(|e| StorageError::Init(format!("failed to build HTTP client: {e}")))?; - Ok(Self { - endpoint, - sync_key, - encryption_key, - client, - last_etag: RefCell::new(None), - }) - } - - /// Whether this backend encrypts client-side (zero-knowledge). - pub fn is_encrypted(&self) -> bool { - self.encryption_key.is_some() - } - - /// Full blob URL for this backend. - fn blob_url(&self) -> String { - format!("{}{}", self.endpoint, BLOB_PATH) - } -} - -impl PortableSyncBackend for HttpPortableSyncBackend { - fn label(&self) -> String { - format!("cloud:{}", self.endpoint) - } - - fn read_archive(&self) -> Result> { - let resp = self - .client - .get(self.blob_url()) - .header(AUTHORIZATION, format!("Bearer {}", self.sync_key)) - .send() - .map_err(|e| StorageError::Init(format!("cloud sync read failed: {e}")))?; - - match resp.status() { - StatusCode::NOT_FOUND => { - // No remote archive yet — first sync for this key. - *self.last_etag.borrow_mut() = None; - Ok(None) - } - StatusCode::OK => { - // Capture the ETag for the matching If-Match write. - let etag = resp - .headers() - .get(ETAG) - .and_then(|v| v.to_str().ok()) - .map(|s| s.to_string()); - *self.last_etag.borrow_mut() = etag; - - let bytes = resp - .bytes() - .map_err(|e| StorageError::Init(format!("cloud sync read body failed: {e}")))?; - - // Decrypt if this is a zero-knowledge envelope. If a passphrase - // is configured but the remote is still plaintext (legacy), parse - // it directly — the next push will encrypt it (transparent upgrade). - let plaintext: std::borrow::Cow<'_, [u8]> = - if super::cloud_crypto::is_encrypted(&bytes) { - let pass = self.encryption_key.as_deref().ok_or_else(|| { - StorageError::Init( - "remote archive is encrypted but VESTIGE_CLOUD_ENCRYPTION_KEY is \ - not set on this device" - .to_string(), - ) - })?; - std::borrow::Cow::Owned(super::cloud_crypto::decrypt(pass, &bytes)?) - } else { - std::borrow::Cow::Borrowed(&bytes) - }; - - let archive: PortableArchive = - serde_json::from_slice(&plaintext).map_err(|e| { - StorageError::Init(format!("failed to parse cloud sync archive: {e}")) - })?; - Ok(Some(archive)) - } - StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => Err(StorageError::Init( - "cloud sync rejected the sync key (401/403). Check your subscription and \ - VESTIGE_CLOUD_SYNC_KEY." - .to_string(), - )), - other => Err(StorageError::Init(format!( - "cloud sync read returned unexpected status {other}" - ))), - } - } - - fn write_archive(&self, archive: &PortableArchive) -> Result<()> { - let plaintext = serde_json::to_vec(archive) - .map_err(|e| StorageError::Init(format!("failed to serialize archive: {e}")))?; - - // Zero-knowledge: encrypt before upload when a passphrase is set, so the - // server only ever stores ciphertext. Content type reflects the payload. - let (body, content_type) = match self.encryption_key.as_deref() { - Some(pass) => ( - super::cloud_crypto::encrypt(pass, &plaintext)?, - "application/octet-stream", - ), - None => (plaintext, "application/json"), - }; - - let mut req = self - .client - .put(self.blob_url()) - .header(AUTHORIZATION, format!("Bearer {}", self.sync_key)) - .header(reqwest::header::CONTENT_TYPE, content_type) - .body(body); - - // Optimistic concurrency: only overwrite the object we pulled. If the - // remote had no archive, require that it still doesn't exist (`If-Match: *` - // would require existence, so we omit the header to allow first create). - if let Some(etag) = self.last_etag.borrow_mut().take() { - req = req.header(IF_MATCH, etag); - } - - let resp = req - .send() - .map_err(|e| StorageError::Init(format!("cloud sync write failed: {e}")))?; - - match resp.status() { - StatusCode::OK | StatusCode::CREATED | StatusCode::NO_CONTENT => Ok(()), - StatusCode::PRECONDITION_FAILED => Err(StorageError::Init( - "cloud sync conflict: another device updated your memory in between. \ - Run `vestige sync --cloud` again to merge and retry." - .to_string(), - )), - StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => Err(StorageError::Init( - "cloud sync rejected the sync key (401/403). Check your subscription and \ - VESTIGE_CLOUD_SYNC_KEY." - .to_string(), - )), - StatusCode::PAYLOAD_TOO_LARGE => Err(StorageError::Init( - "cloud sync archive too large for the hosted plan limit".to_string(), - )), - other => Err(StorageError::Init(format!( - "cloud sync write returned unexpected status {other}" - ))), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use super::super::portable::{PortableArchive, PortableTable, PORTABLE_ARCHIVE_FORMAT}; - use std::io::{Read, Write}; - use std::net::TcpListener; - use std::sync::mpsc; - use std::thread; - - fn sample_archive() -> PortableArchive { - PortableArchive { - archive_format: PORTABLE_ARCHIVE_FORMAT.to_string(), - vestige_version: "test".to_string(), - schema_version: 1, - exported_at: chrono::Utc::now(), - mode: "exact".to_string(), - tables: vec![PortableTable { - name: "knowledge_nodes".to_string(), - columns: vec!["id".to_string()], - rows: vec![], - }], - } - } - - /// A captured request the mock observed, surfaced to the test thread. - #[derive(Debug, Default, Clone)] - struct CapturedRequest { - method: String, - authorization: Option, - if_match: Option, - } - - /// Minimal one-shot HTTP mock. `responder` builds the raw HTTP response - /// string for the request line + headers it parsed. Returns the bound base - /// URL and a receiver for the captured request. - fn spawn_mock(responder: F) -> (String, mpsc::Receiver) - where - F: Fn(&CapturedRequest) -> String + Send + 'static, - { - let listener = TcpListener::bind("127.0.0.1:0").expect("bind mock"); - let addr = listener.local_addr().expect("addr"); - let (tx, rx) = mpsc::channel(); - thread::spawn(move || { - if let Ok((mut stream, _)) = listener.accept() { - let mut buf = [0u8; 8192]; - let n = stream.read(&mut buf).unwrap_or(0); - let text = String::from_utf8_lossy(&buf[..n]); - let mut cap = CapturedRequest::default(); - for (i, line) in text.lines().enumerate() { - if i == 0 { - cap.method = line.split_whitespace().next().unwrap_or("").to_string(); - } else if let Some(v) = line.strip_prefix("authorization: ") { - cap.authorization = Some(v.trim().to_string()); - } else if let Some(v) = line.strip_prefix("if-match: ") { - cap.if_match = Some(v.trim().to_string()); - } - } - let response = responder(&cap); - let _ = stream.write_all(response.as_bytes()); - let _ = stream.flush(); - let _ = tx.send(cap); - } - }); - (format!("http://{addr}"), rx) - } - - fn http_response(status: &str, extra_headers: &str, body: &str) -> String { - format!( - "HTTP/1.1 {status}\r\nContent-Length: {}\r\n{extra_headers}Connection: close\r\n\r\n{body}", - body.len() - ) - } - - #[test] - fn new_rejects_empty_endpoint_and_key() { - assert!(HttpPortableSyncBackend::new("", "key").is_err()); - assert!(HttpPortableSyncBackend::new("https://x", "").is_err()); - assert!(HttpPortableSyncBackend::new("https://x", "key").is_ok()); - } - - #[test] - fn endpoint_trailing_slash_trimmed() { - let be = HttpPortableSyncBackend::new("https://sync.example/", "k").unwrap(); - assert_eq!(be.blob_url(), "https://sync.example/v1/blob"); - } - - #[test] - fn read_404_returns_none() { - let (base, rx) = spawn_mock(|_| http_response("404 Not Found", "", "")); - let be = HttpPortableSyncBackend::new(base, "secret").unwrap(); - let got = be.read_archive().expect("read ok"); - assert!(got.is_none()); - let cap = rx.recv().unwrap(); - assert_eq!(cap.method, "GET"); - assert_eq!(cap.authorization.as_deref(), Some("Bearer secret")); - } - - #[test] - fn read_200_parses_and_captures_etag() { - let archive = sample_archive(); - let body = serde_json::to_string(&archive).unwrap(); - let (base, _rx) = spawn_mock(move |_| { - http_response("200 OK", "ETag: \"v1-abc\"\r\n", &body) - }); - let be = HttpPortableSyncBackend::new(base, "secret").unwrap(); - let got = be.read_archive().expect("read ok").expect("some archive"); - assert_eq!(got.archive_format, PORTABLE_ARCHIVE_FORMAT); - // ETag captured for the next If-Match write. - assert_eq!(be.last_etag.borrow().as_deref(), Some("\"v1-abc\"")); - } - - #[test] - fn read_401_is_error() { - let (base, _rx) = spawn_mock(|_| http_response("401 Unauthorized", "", "")); - let be = HttpPortableSyncBackend::new(base, "bad").unwrap(); - assert!(be.read_archive().is_err()); - } - - #[test] - fn write_sends_if_match_when_etag_present() { - // Seed an etag as if a prior read happened. - let (base, rx) = spawn_mock(|_| http_response("200 OK", "", "")); - let be = HttpPortableSyncBackend::new(base, "secret").unwrap(); - *be.last_etag.borrow_mut() = Some("\"v1-abc\"".to_string()); - be.write_archive(&sample_archive()).expect("write ok"); - let cap = rx.recv().unwrap(); - assert_eq!(cap.method, "PUT"); - assert_eq!(cap.authorization.as_deref(), Some("Bearer secret")); - assert_eq!(cap.if_match.as_deref(), Some("\"v1-abc\"")); - } - - #[test] - fn write_omits_if_match_for_first_create() { - let (base, rx) = spawn_mock(|_| http_response("201 Created", "", "")); - let be = HttpPortableSyncBackend::new(base, "secret").unwrap(); - // No prior read → no etag → no If-Match (allow create). - be.write_archive(&sample_archive()).expect("write ok"); - let cap = rx.recv().unwrap(); - assert_eq!(cap.method, "PUT"); - assert!(cap.if_match.is_none()); - } - - #[test] - fn write_412_is_conflict_error() { - let (base, _rx) = spawn_mock(|_| http_response("412 Precondition Failed", "", "")); - let be = HttpPortableSyncBackend::new(base, "secret").unwrap(); - *be.last_etag.borrow_mut() = Some("\"stale\"".to_string()); - let err = be.write_archive(&sample_archive()).unwrap_err(); - assert!(err.to_string().contains("conflict")); - } -} diff --git a/crates/vestige-core/src/storage/memory_store.rs b/crates/vestige-core/src/storage/memory_store.rs deleted file mode 100644 index 010ee97..0000000 --- a/crates/vestige-core/src/storage/memory_store.rs +++ /dev/null @@ -1,516 +0,0 @@ -//! Backend-agnostic memory store trait. -//! -//! This is the single abstraction every cognitive module sits above. It is -//! intentionally flat: one trait, ~25 methods, no sub-traits. - -use std::collections::HashMap; -use std::future::Future; -use std::pin::Pin; - -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -// ---------------------------------------------------------------------------- -// ERROR -// ---------------------------------------------------------------------------- - -/// Error returned by every `LocalMemoryStore` / `MemoryStore` method. -#[non_exhaustive] -#[derive(Debug, thiserror::Error)] -pub enum MemoryStoreError { - #[error("not found: {0}")] - NotFound(String), - - #[error("backend error: {0}")] - Backend(String), - - #[error( - "embedding model mismatch: store registered {registered_name} (dim {registered_dim}, \ - hash {registered_hash}), embedder is {actual_name} (dim {actual_dim}, hash {actual_hash})" - )] - ModelMismatch { - registered_name: String, - registered_dim: usize, - registered_hash: String, - actual_name: String, - actual_dim: usize, - actual_hash: String, - }, - - #[error("invalid input: {0}")] - InvalidInput(String), - - #[error("initialization error: {0}")] - Init(String), -} - -impl From for MemoryStoreError { - fn from(e: crate::storage::StorageError) -> Self { - use crate::storage::StorageError as S; - match e { - S::NotFound(s) => MemoryStoreError::NotFound(s), - S::Database(e) => MemoryStoreError::Backend(e.to_string()), - S::Io(e) => MemoryStoreError::Backend(e.to_string()), - S::InvalidTimestamp(s) => MemoryStoreError::Backend(format!("invalid timestamp: {s}")), - S::Init(s) => MemoryStoreError::Init(s), - } - } -} - -pub type MemoryStoreResult = std::result::Result; - -// ---------------------------------------------------------------------------- -// DATA TYPES -// ---------------------------------------------------------------------------- - -/// Backend-agnostic memory record. -/// -/// Phase 1 intentionally keeps this type independent of `KnowledgeNode` to -/// avoid dragging 30+ legacy fields through the trait surface. The SQLite -/// backend converts between `MemoryRecord` and `KnowledgeNode` at the -/// boundary. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MemoryRecord { - pub id: Uuid, - /// Empty = unclassified. Populated in Phase 4. - pub domains: Vec, - /// Raw similarity per domain centroid. Empty until Phase 4 runs clustering. - pub domain_scores: HashMap, - pub content: String, - pub node_type: String, - pub tags: Vec, - pub embedding: Option>, - pub created_at: DateTime, - pub updated_at: DateTime, - pub metadata: serde_json::Value, -} - -/// FSRS-6 scheduling state, one row per memory. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SchedulingState { - pub memory_id: Uuid, - pub stability: f64, - pub difficulty: f64, - pub retrievability: f64, - pub last_review: Option>, - pub next_review: Option>, - pub reps: u32, - pub lapses: u32, -} - -/// Hybrid search request. -#[derive(Debug, Clone, Default)] -pub struct SearchQuery { - pub domains: Option>, - pub text: Option, - pub embedding: Option>, - pub tags: Option>, - pub node_types: Option>, - pub limit: usize, - pub min_retrievability: Option, -} - -#[derive(Debug, Clone)] -pub struct SearchResult { - pub record: MemoryRecord, - pub score: f64, - pub fts_score: Option, - pub vector_score: Option, -} - -/// Edge in the spreading-activation graph. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MemoryEdge { - pub source_id: Uuid, - pub target_id: Uuid, - pub edge_type: String, - pub weight: f64, - pub created_at: DateTime, -} - -/// A topical domain (populated in Phase 4). Phase 1 only needs the type to -/// shape the trait surface; discover/classify are Phase 4 work. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Domain { - pub id: String, - pub label: String, - pub centroid: Vec, - pub top_terms: Vec, - pub memory_count: usize, - pub created_at: DateTime, -} - -/// Result of classifying one vector against all known domains. -#[derive(Debug, Clone)] -pub struct ClassificationResult { - pub scores: HashMap, - pub domains: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct StoreStats { - pub total_memories: usize, - pub memories_with_embeddings: usize, - pub total_edges: usize, - pub total_domains: usize, - pub registered_model_name: Option, - pub registered_model_dim: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum HealthStatus { - Healthy, - Degraded { reason: String }, - Unavailable { reason: String }, -} - -// ---------------------------------------------------------------------------- -// EMBEDDING MODEL SIGNATURE -// ---------------------------------------------------------------------------- - -/// Snapshot of the embedding model that was used to write vectors into the -/// store. Persisted in the `embedding_model` table; compared on every write -/// before the vector is accepted. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ModelSignature { - pub name: String, - pub dimension: usize, - /// Lowercase hex-encoded blake3 hash, 64 chars. - pub hash: String, -} - -// ---------------------------------------------------------------------------- -// TRAIT -// ---------------------------------------------------------------------------- - -/// Internal source trait declared with native async-fn-in-trait. -/// -/// `#[trait_variant::make(MemoryStoreSend: Send)]` derives a Send-bounded -/// variant whose returned futures are `Send`. In trait_variant 0.1.x the -/// macro emits the blanket `impl LocalMemoryStore for T`, -/// so backends implement `MemoryStoreSend` (the Send variant) and get -/// `LocalMemoryStore` (the non-Send variant) for free. -/// -/// Most callers should reach for the dyn-compatible `MemoryStore` trait -/// declared below, which adapts `MemoryStoreSend` into a boxed-future surface -/// and is the public storage abstraction for cognitive modules and tests -/// that want `Arc`. -#[trait_variant::make(MemoryStoreSend: Send)] -pub trait LocalMemoryStore: Sync + 'static { - // --- Lifecycle --- - async fn init(&self) -> MemoryStoreResult<()>; - async fn health_check(&self) -> MemoryStoreResult; - - // --- Embedding model registry --- - async fn registered_model(&self) -> MemoryStoreResult>; - async fn register_model(&self, sig: &ModelSignature) -> MemoryStoreResult<()>; - - // --- CRUD --- - async fn insert(&self, record: &MemoryRecord) -> MemoryStoreResult; - async fn get(&self, id: Uuid) -> MemoryStoreResult>; - async fn update(&self, record: &MemoryRecord) -> MemoryStoreResult<()>; - async fn delete(&self, id: Uuid) -> MemoryStoreResult<()>; - - // --- Search --- - async fn search(&self, query: &SearchQuery) -> MemoryStoreResult>; - async fn fts_search(&self, text: &str, limit: usize) -> MemoryStoreResult>; - async fn vector_search( - &self, - embedding: &[f32], - limit: usize, - ) -> MemoryStoreResult>; - - // --- FSRS Scheduling --- - async fn get_scheduling(&self, memory_id: Uuid) -> MemoryStoreResult>; - async fn update_scheduling(&self, state: &SchedulingState) -> MemoryStoreResult<()>; - async fn get_due_memories( - &self, - before: DateTime, - limit: usize, - ) -> MemoryStoreResult>; - - // --- Graph (spreading activation) --- - async fn add_edge(&self, edge: &MemoryEdge) -> MemoryStoreResult<()>; - async fn get_edges( - &self, - node_id: Uuid, - edge_type: Option<&str>, - ) -> MemoryStoreResult>; - async fn remove_edge(&self, source: Uuid, target: Uuid) -> MemoryStoreResult<()>; - async fn get_neighbors( - &self, - node_id: Uuid, - depth: usize, - ) -> MemoryStoreResult>; - - // --- Domains (Phase 1: stubs return empty; full impl in Phase 4) --- - async fn list_domains(&self) -> MemoryStoreResult>; - async fn get_domain(&self, id: &str) -> MemoryStoreResult>; - async fn upsert_domain(&self, domain: &Domain) -> MemoryStoreResult<()>; - async fn delete_domain(&self, id: &str) -> MemoryStoreResult<()>; - /// Phase 1: returns `Ok(vec![])` since no centroids exist. Phase 4 wires - /// the full soft-assignment pass. - async fn classify(&self, embedding: &[f32]) -> MemoryStoreResult>; - - // --- Bulk / Maintenance --- - async fn count(&self) -> MemoryStoreResult; - async fn get_stats(&self) -> MemoryStoreResult; - async fn vacuum(&self) -> MemoryStoreResult<()>; -} - -// ---------------------------------------------------------------------------- -// DYN-COMPATIBLE STORAGE TRAIT -// ---------------------------------------------------------------------------- - -/// Boxed Send future returning a `MemoryStoreResult`, bound to the lifetime -/// of the borrows captured by the call (typically `&self` plus any reference -/// arguments). Used as the return type of every method on the dyn-compatible -/// `MemoryStore` trait below. -pub type BoxedStoreFuture<'a, T> = Pin> + Send + 'a>>; - -/// Dyn-compatible storage trait. -/// -/// `MemoryStoreSend` above is the trait users implement; it uses native -/// async-fn-in-trait return types (RPITIT), which gives zero-allocation -/// static dispatch but is not dyn-safe. This trait wraps every method in -/// `Pin>` so `Arc` works for -/// the cognitive module surface and the Phase 1 integration tests. -/// -/// Implementations should not target this trait directly; the blanket -/// `impl MemoryStore for T` adapts every Send-variant -/// implementation automatically. Each call boxes the returned future -/// exactly once, identical to the cost of the previous design. -pub trait MemoryStore: Send + Sync + 'static { - fn init<'a>(&'a self) -> BoxedStoreFuture<'a, ()>; - fn health_check<'a>(&'a self) -> BoxedStoreFuture<'a, HealthStatus>; - - fn registered_model<'a>(&'a self) -> BoxedStoreFuture<'a, Option>; - fn register_model<'a>(&'a self, sig: &'a ModelSignature) -> BoxedStoreFuture<'a, ()>; - - fn insert<'a>(&'a self, record: &'a MemoryRecord) -> BoxedStoreFuture<'a, Uuid>; - fn get<'a>(&'a self, id: Uuid) -> BoxedStoreFuture<'a, Option>; - fn update<'a>(&'a self, record: &'a MemoryRecord) -> BoxedStoreFuture<'a, ()>; - fn delete<'a>(&'a self, id: Uuid) -> BoxedStoreFuture<'a, ()>; - - fn search<'a>(&'a self, query: &'a SearchQuery) -> BoxedStoreFuture<'a, Vec>; - fn fts_search<'a>( - &'a self, - text: &'a str, - limit: usize, - ) -> BoxedStoreFuture<'a, Vec>; - fn vector_search<'a>( - &'a self, - embedding: &'a [f32], - limit: usize, - ) -> BoxedStoreFuture<'a, Vec>; - - fn get_scheduling<'a>( - &'a self, - memory_id: Uuid, - ) -> BoxedStoreFuture<'a, Option>; - fn update_scheduling<'a>(&'a self, state: &'a SchedulingState) -> BoxedStoreFuture<'a, ()>; - fn get_due_memories<'a>( - &'a self, - before: DateTime, - limit: usize, - ) -> BoxedStoreFuture<'a, Vec<(MemoryRecord, SchedulingState)>>; - - fn add_edge<'a>(&'a self, edge: &'a MemoryEdge) -> BoxedStoreFuture<'a, ()>; - fn get_edges<'a>( - &'a self, - node_id: Uuid, - edge_type: Option<&'a str>, - ) -> BoxedStoreFuture<'a, Vec>; - fn remove_edge<'a>(&'a self, source: Uuid, target: Uuid) -> BoxedStoreFuture<'a, ()>; - fn get_neighbors<'a>( - &'a self, - node_id: Uuid, - depth: usize, - ) -> BoxedStoreFuture<'a, Vec<(MemoryRecord, f64)>>; - - fn list_domains<'a>(&'a self) -> BoxedStoreFuture<'a, Vec>; - fn get_domain<'a>(&'a self, id: &'a str) -> BoxedStoreFuture<'a, Option>; - fn upsert_domain<'a>(&'a self, domain: &'a Domain) -> BoxedStoreFuture<'a, ()>; - fn delete_domain<'a>(&'a self, id: &'a str) -> BoxedStoreFuture<'a, ()>; - fn classify<'a>(&'a self, embedding: &'a [f32]) -> BoxedStoreFuture<'a, Vec<(String, f64)>>; - - fn count<'a>(&'a self) -> BoxedStoreFuture<'a, usize>; - fn get_stats<'a>(&'a self) -> BoxedStoreFuture<'a, StoreStats>; - fn vacuum<'a>(&'a self) -> BoxedStoreFuture<'a, ()>; -} - -impl MemoryStore for T -where - T: MemoryStoreSend, -{ - fn init<'a>(&'a self) -> BoxedStoreFuture<'a, ()> { - Box::pin(::init(self)) - } - fn health_check<'a>(&'a self) -> BoxedStoreFuture<'a, HealthStatus> { - Box::pin(::health_check(self)) - } - - fn registered_model<'a>(&'a self) -> BoxedStoreFuture<'a, Option> { - Box::pin(::registered_model(self)) - } - fn register_model<'a>(&'a self, sig: &'a ModelSignature) -> BoxedStoreFuture<'a, ()> { - Box::pin(::register_model(self, sig)) - } - - fn insert<'a>(&'a self, record: &'a MemoryRecord) -> BoxedStoreFuture<'a, Uuid> { - Box::pin(::insert(self, record)) - } - fn get<'a>(&'a self, id: Uuid) -> BoxedStoreFuture<'a, Option> { - Box::pin(::get(self, id)) - } - fn update<'a>(&'a self, record: &'a MemoryRecord) -> BoxedStoreFuture<'a, ()> { - Box::pin(::update(self, record)) - } - fn delete<'a>(&'a self, id: Uuid) -> BoxedStoreFuture<'a, ()> { - Box::pin(::delete(self, id)) - } - - fn search<'a>(&'a self, query: &'a SearchQuery) -> BoxedStoreFuture<'a, Vec> { - Box::pin(::search(self, query)) - } - fn fts_search<'a>( - &'a self, - text: &'a str, - limit: usize, - ) -> BoxedStoreFuture<'a, Vec> { - Box::pin(::fts_search(self, text, limit)) - } - fn vector_search<'a>( - &'a self, - embedding: &'a [f32], - limit: usize, - ) -> BoxedStoreFuture<'a, Vec> { - Box::pin(::vector_search( - self, embedding, limit, - )) - } - - fn get_scheduling<'a>( - &'a self, - memory_id: Uuid, - ) -> BoxedStoreFuture<'a, Option> { - Box::pin(::get_scheduling(self, memory_id)) - } - fn update_scheduling<'a>(&'a self, state: &'a SchedulingState) -> BoxedStoreFuture<'a, ()> { - Box::pin(::update_scheduling(self, state)) - } - fn get_due_memories<'a>( - &'a self, - before: DateTime, - limit: usize, - ) -> BoxedStoreFuture<'a, Vec<(MemoryRecord, SchedulingState)>> { - Box::pin(::get_due_memories( - self, before, limit, - )) - } - - fn add_edge<'a>(&'a self, edge: &'a MemoryEdge) -> BoxedStoreFuture<'a, ()> { - Box::pin(::add_edge(self, edge)) - } - fn get_edges<'a>( - &'a self, - node_id: Uuid, - edge_type: Option<&'a str>, - ) -> BoxedStoreFuture<'a, Vec> { - Box::pin(::get_edges(self, node_id, edge_type)) - } - fn remove_edge<'a>(&'a self, source: Uuid, target: Uuid) -> BoxedStoreFuture<'a, ()> { - Box::pin(::remove_edge(self, source, target)) - } - fn get_neighbors<'a>( - &'a self, - node_id: Uuid, - depth: usize, - ) -> BoxedStoreFuture<'a, Vec<(MemoryRecord, f64)>> { - Box::pin(::get_neighbors(self, node_id, depth)) - } - - fn list_domains<'a>(&'a self) -> BoxedStoreFuture<'a, Vec> { - Box::pin(::list_domains(self)) - } - fn get_domain<'a>(&'a self, id: &'a str) -> BoxedStoreFuture<'a, Option> { - Box::pin(::get_domain(self, id)) - } - fn upsert_domain<'a>(&'a self, domain: &'a Domain) -> BoxedStoreFuture<'a, ()> { - Box::pin(::upsert_domain(self, domain)) - } - fn delete_domain<'a>(&'a self, id: &'a str) -> BoxedStoreFuture<'a, ()> { - Box::pin(::delete_domain(self, id)) - } - fn classify<'a>(&'a self, embedding: &'a [f32]) -> BoxedStoreFuture<'a, Vec<(String, f64)>> { - Box::pin(::classify(self, embedding)) - } - - fn count<'a>(&'a self) -> BoxedStoreFuture<'a, usize> { - Box::pin(::count(self)) - } - fn get_stats<'a>(&'a self) -> BoxedStoreFuture<'a, StoreStats> { - Box::pin(::get_stats(self)) - } - fn vacuum<'a>(&'a self) -> BoxedStoreFuture<'a, ()> { - Box::pin(::vacuum(self)) - } -} - -// ---------------------------------------------------------------------------- -// UNIT TESTS -// ---------------------------------------------------------------------------- - -#[cfg(test)] -mod tests { - use super::*; - use crate::storage::StorageError; - - #[test] - fn memory_store_error_from_storage_error() { - let se = StorageError::NotFound("abc".to_string()); - let mse = MemoryStoreError::from(se); - assert!(matches!(mse, MemoryStoreError::NotFound(_))); - - let se2 = StorageError::Init("init failure".to_string()); - let mse2 = MemoryStoreError::from(se2); - assert!(matches!(mse2, MemoryStoreError::Init(_))); - } - - #[test] - fn model_signature_serde_round_trip() { - let sig = ModelSignature { - name: "nomic-ai/nomic-embed-text-v1.5".to_string(), - dimension: 256, - hash: "a".repeat(64), - }; - let json = serde_json::to_string(&sig).expect("serialize"); - let sig2: ModelSignature = serde_json::from_str(&json).expect("deserialize"); - assert_eq!(sig, sig2); - } - - #[test] - fn memory_record_serde_round_trip() { - let rec = MemoryRecord { - id: Uuid::new_v4(), - domains: vec!["dev".to_string()], - domain_scores: { - let mut m = HashMap::new(); - m.insert("dev".to_string(), 0.9); - m - }, - content: "hello".to_string(), - node_type: "fact".to_string(), - tags: vec!["tag1".to_string()], - embedding: None, - created_at: Utc::now(), - updated_at: Utc::now(), - metadata: serde_json::json!({}), - }; - let json = serde_json::to_string(&rec).expect("serialize"); - let rec2: MemoryRecord = serde_json::from_str(&json).expect("deserialize"); - assert_eq!(rec.content, rec2.content); - assert_eq!(rec.domains, rec2.domains); - } -} diff --git a/crates/vestige-core/src/storage/migrations.rs b/crates/vestige-core/src/storage/migrations.rs index 3440f61..2f1ac5b 100644 --- a/crates/vestige-core/src/storage/migrations.rs +++ b/crates/vestige-core/src/storage/migrations.rs @@ -59,46 +59,6 @@ pub const MIGRATIONS: &[Migration] = &[ description: "v2.0.7 Cleanup: drop dead knowledge_edges and compressed_memories tables", up: MIGRATION_V11_UP, }, - Migration { - version: 12, - description: "v2.1.1 Sync: tombstones for merge-capable portable storage", - up: MIGRATION_V12_UP, - }, - Migration { - version: 13, - description: "v2.1.2 Honest Memory: non-content purge tombstones", - up: MIGRATION_V13_UP, - }, - Migration { - version: 14, - description: "v2.1.25 Merge/Supersede: reversible operation log, merge plans, bitemporal lineage, protected pins", - up: MIGRATION_V14_UP, - }, - Migration { - version: 15, - description: "ComposedGraph: composition events, members, outcomes", - up: MIGRATION_V15_UP, - }, - Migration { - version: 16, - description: "ADR 0001 Phase 1: embedding_model registry, domains/domain_scores columns, domains table", - up: MIGRATION_V16_UP, - }, - Migration { - version: 17, - description: "#57 Source envelope: provenance columns + connector cursor checkpoints for idempotent external-source sync", - up: MIGRATION_V17_UP, - }, - Migration { - version: 18, - description: "Agent Black Box + Memory Receipts + Memory PRs: replayable run traces, retrieval receipts, risk-gated brain-change review queue", - up: MIGRATION_V18_UP, - }, - Migration { - version: 19, - description: "Scope the source idempotency key by source_project so same-system sources with overlapping ids no longer clobber each other", - up: MIGRATION_V19_UP, - }, ]; /// A database migration @@ -721,184 +681,6 @@ DROP TABLE IF EXISTS compressed_memories; UPDATE schema_version SET version = 11, applied_at = datetime('now'); "#; -/// V12: Merge-capable sync tombstones. -/// -/// Portable sync needs to propagate deletions between devices. `knowledge_nodes` -/// remains the source of truth for live memories; this table records deletes so -/// another device can remove the same memory during a merge import. -const MIGRATION_V12_UP: &str = r#" -CREATE TABLE IF NOT EXISTS sync_tombstones ( - table_name TEXT NOT NULL, - row_id TEXT NOT NULL, - deleted_at TEXT NOT NULL, - reason TEXT, - PRIMARY KEY (table_name, row_id) -); - -CREATE INDEX IF NOT EXISTS idx_sync_tombstones_deleted_at -ON sync_tombstones(deleted_at); - -UPDATE schema_version SET version = 12, applied_at = datetime('now'); -"#; - -/// V13: non-content purge tombstones. -/// -/// `memory(action="purge")` permanently removes memory content and embeddings, -/// but keeps a content-free audit/sync record so users can verify that a memory -/// was removed without Vestige retaining the text it was told to forget. -const MIGRATION_V13_UP: &str = r#" -CREATE TABLE IF NOT EXISTS deletion_tombstones ( - memory_id TEXT PRIMARY KEY, - deleted_at TEXT NOT NULL, - reason TEXT, - node_type TEXT NOT NULL, - tags TEXT NOT NULL DEFAULT '[]', - edges_pruned INTEGER NOT NULL DEFAULT 0, - insights_rewritten INTEGER NOT NULL DEFAULT 0, - insights_deleted INTEGER NOT NULL DEFAULT 0, - children_orphaned INTEGER NOT NULL DEFAULT 0 -); - -CREATE INDEX IF NOT EXISTS idx_deletion_tombstones_deleted_at -ON deletion_tombstones(deleted_at); - -UPDATE schema_version SET version = 13, applied_at = datetime('now'); -"#; - -/// V14: Merge / Supersede controls (Phase 3). -/// -/// Adds the four pieces the merge/supersede feature needs on a never-delete -/// (bitemporal) store: -/// -/// 1. `merge_plans` — previewable, not-yet-applied plans. `plan_merge` and -/// `plan_supersede` write a plan row containing a JSON diff; `apply_plan` -/// consumes it by id. Plans are append-only; status moves -/// pending -> applied / cancelled. -/// 2. `merge_operations` — the reversible operation log (the "memory reflog"). -/// Every applied merge/supersede records one row with a JSON `undo_payload` -/// capturing exactly what changed, so `merge_undo` can reverse it. The -/// `signals` column records WHY the memories combined (provenance), which is -/// the self-explaining differentiator. -/// 3. `knowledge_nodes.protected` — pin flag. A protected memory can never be -/// auto-merged, superseded, or forgotten. -/// 4. `knowledge_nodes.superseded_by` — bitemporal lineage pointer. Superseding -/// A with B does NOT delete A: it stamps A.valid_until = B.valid_from and -/// sets A.superseded_by = B.id, leaving A fully queryable for audit -/// (Graphiti-style invalidate-don't-delete). -// The two `protected` / `superseded_by` ADD COLUMNs (and their indexes) are -// applied separately in `apply_migrations` BEFORE this batch runs, guarded -// against "duplicate column" on replay, since SQLite has no -// `ADD COLUMN IF NOT EXISTS`. The rest of V14 is idempotent (CREATE ... IF NOT -// EXISTS). -const MIGRATION_V14_UP: &str = r#" -CREATE INDEX IF NOT EXISTS idx_nodes_protected ON knowledge_nodes(protected); -CREATE INDEX IF NOT EXISTS idx_nodes_superseded_by ON knowledge_nodes(superseded_by); - --- Previewable plans (a diff) produced by plan_merge / plan_supersede. --- `kind` is 'merge' | 'supersede'. `payload` is the full JSON plan/diff. -CREATE TABLE IF NOT EXISTS merge_plans ( - id TEXT PRIMARY KEY, - kind TEXT NOT NULL, - status TEXT NOT NULL DEFAULT 'pending', -- pending | applied | cancelled - created_at TEXT NOT NULL, - applied_at TEXT, - survivor_id TEXT, -- node kept after the op - member_ids TEXT NOT NULL DEFAULT '[]', -- JSON array of all involved node ids - confidence REAL, -- Fellegi-Sunter match score (0-1) - classification TEXT, -- match | possible | non_match - payload TEXT NOT NULL -- full JSON plan/diff -); - -CREATE INDEX IF NOT EXISTS idx_merge_plans_status ON merge_plans(status); -CREATE INDEX IF NOT EXISTS idx_merge_plans_created_at ON merge_plans(created_at); - --- Reversible operation log — the "git reflog for your agent's memory". --- One row per applied merge/supersede; `undo_payload` carries everything --- needed to reverse it, `signals` records why the memories combined. -CREATE TABLE IF NOT EXISTS merge_operations ( - id TEXT PRIMARY KEY, - plan_id TEXT, -- merge_plans.id this came from - op_type TEXT NOT NULL, -- merge | supersede | undo - status TEXT NOT NULL DEFAULT 'applied', -- applied | reverted - created_at TEXT NOT NULL, - reverted_at TEXT, - reverts_op_id TEXT, -- set when op_type = 'undo' - survivor_id TEXT, -- node kept - affected_ids TEXT NOT NULL DEFAULT '[]', -- JSON array of node ids touched - confidence REAL, - signals TEXT, -- JSON: why they combined (provenance) - reason TEXT, -- human-readable explanation - undo_payload TEXT NOT NULL -- JSON snapshot to reverse the op -); - -CREATE INDEX IF NOT EXISTS idx_merge_operations_status ON merge_operations(status); -CREATE INDEX IF NOT EXISTS idx_merge_operations_created_at ON merge_operations(created_at); -CREATE INDEX IF NOT EXISTS idx_merge_operations_survivor ON merge_operations(survivor_id); - -UPDATE schema_version SET version = 14, applied_at = datetime('now'); -"#; - -/// V15: ComposedGraph persistence for memory composition outcomes. -/// -/// These tables record which memories were used together, which tool/query -/// produced the composition, and what happened afterward. `memory_id` values -/// are intentionally historical references instead of foreign keys to -/// `knowledge_nodes`: purging or superseding a memory must not erase the fact -/// that a bounty lane or reasoning path was previously composed. -const MIGRATION_V15_UP: &str = r#" -CREATE TABLE IF NOT EXISTS composition_events ( - id TEXT PRIMARY KEY, - created_at TEXT NOT NULL, - tool TEXT NOT NULL, - mode TEXT NOT NULL DEFAULT 'deep_reference', - query TEXT, - query_hash TEXT, - confidence REAL, - status TEXT, - output_preview TEXT, - metadata TEXT NOT NULL DEFAULT '{}' -); - -CREATE INDEX IF NOT EXISTS idx_composition_events_created_at ON composition_events(created_at); -CREATE INDEX IF NOT EXISTS idx_composition_events_tool ON composition_events(tool); -CREATE INDEX IF NOT EXISTS idx_composition_events_mode ON composition_events(mode); -CREATE INDEX IF NOT EXISTS idx_composition_events_query_hash ON composition_events(query_hash); - -CREATE TABLE IF NOT EXISTS composition_members ( - event_id TEXT NOT NULL, - memory_id TEXT NOT NULL, - role TEXT NOT NULL, -- primary | supporting | contradicting | superseded | related - rank INTEGER NOT NULL DEFAULT 0, - trust REAL, - score REAL, - preview TEXT, - metadata TEXT NOT NULL DEFAULT '{}', - PRIMARY KEY (event_id, memory_id, role), - FOREIGN KEY (event_id) REFERENCES composition_events(id) ON DELETE CASCADE -); - -CREATE INDEX IF NOT EXISTS idx_composition_members_memory ON composition_members(memory_id); -CREATE INDEX IF NOT EXISTS idx_composition_members_role ON composition_members(role); - -CREATE TABLE IF NOT EXISTS composition_outcomes ( - id TEXT PRIMARY KEY, - event_id TEXT NOT NULL, - outcome_type TEXT NOT NULL, - labeled_at TEXT NOT NULL, - label_source TEXT NOT NULL DEFAULT 'tool', - confidence_delta REAL, - notes TEXT, - metadata TEXT NOT NULL DEFAULT '{}', - FOREIGN KEY (event_id) REFERENCES composition_events(id) ON DELETE CASCADE -); - -CREATE INDEX IF NOT EXISTS idx_composition_outcomes_event ON composition_outcomes(event_id); -CREATE INDEX IF NOT EXISTS idx_composition_outcomes_type ON composition_outcomes(outcome_type); -CREATE INDEX IF NOT EXISTS idx_composition_outcomes_labeled_at ON composition_outcomes(labeled_at); - -UPDATE schema_version SET version = 15, applied_at = datetime('now'); -"#; - /// Get current schema version from database pub fn get_current_version(conn: &rusqlite::Connection) -> rusqlite::Result { conn.query_row( @@ -909,262 +691,7 @@ pub fn get_current_version(conn: &rusqlite::Connection) -> rusqlite::Result .or(Ok(0)) } -/// Run an `ALTER TABLE ... ADD COLUMN` statement, treating a "duplicate column -/// name" failure as success so migration replay stays idempotent (SQLite has no -/// `ADD COLUMN IF NOT EXISTS`). -fn add_column_if_missing(conn: &rusqlite::Connection, sql: &str) -> rusqlite::Result<()> { - match conn.execute(sql, []) { - Ok(_) => Ok(()), - Err(rusqlite::Error::SqliteFailure(_, Some(msg))) - if msg.contains("duplicate column name") => - { - Ok(()) - } - Err(e) => Err(e), - } -} - -/// V16: ADR 0001 Phase 1 - embedding_model registry + domain columns. -/// -/// The ALTER TABLE statements are split out into `MIGRATION_V16_ALTER_COLUMNS` -/// because SQLite has no `ALTER TABLE ... ADD COLUMN IF NOT EXISTS`. The -/// migration runner handles them individually so replaying V16 is idempotent. -const MIGRATION_V16_UP: &str = r#" --- Migration V16: embedding model registry + per-memory domain columns. - --- 1. Embedding model registry. Single logical row; the (id = 1) constraint is --- enforced in code via `register_model` (SQLite CHECK on a single-row --- table is uglier than a constraint we already enforce in Rust). -CREATE TABLE IF NOT EXISTS embedding_model ( - id INTEGER PRIMARY KEY CHECK (id = 1), - name TEXT NOT NULL, - dimension INTEGER NOT NULL, - hash TEXT NOT NULL, - created_at TEXT NOT NULL -); - --- 2. Per-memory domain columns are applied separately (see apply_migrations). - --- 3. Index on the domains JSON column to enable LIKE-style filter in Phase 4. -CREATE INDEX IF NOT EXISTS idx_nodes_domains ON knowledge_nodes(domains); -CREATE INDEX IF NOT EXISTS idx_nodes_domain_scores ON knowledge_nodes(domain_scores); - --- 4. Domains catalogue (empty until Phase 4 populates). -CREATE TABLE IF NOT EXISTS domains ( - id TEXT PRIMARY KEY, - label TEXT NOT NULL, - centroid BLOB, - top_terms TEXT NOT NULL DEFAULT '[]', - memory_count INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL -); - -CREATE INDEX IF NOT EXISTS idx_domains_created_at ON domains(created_at); - -UPDATE schema_version SET version = 16, applied_at = datetime('now'); -"#; - -/// The two ALTER TABLE statements for V16. Kept separate so the migration -/// runner can try each individually and ignore "duplicate column" errors, -/// making V16 idempotent on replay (SQLite has no ADD COLUMN IF NOT EXISTS). -pub const MIGRATION_V16_ALTER_COLUMNS: &[&str] = &[ - "ALTER TABLE knowledge_nodes ADD COLUMN domains TEXT NOT NULL DEFAULT '[]'", - "ALTER TABLE knowledge_nodes ADD COLUMN domain_scores TEXT NOT NULL DEFAULT '{}'", -]; - -/// V17: #57 Source envelope — structured provenance for connector-ingested -/// records, plus a per-connector cursor checkpoint table. -/// -/// The provenance columns live directly on `knowledge_nodes` (rather than a -/// side table) so search can filter and cite them with no join. They are all -/// nullable and default-NULL, so every existing memory is untouched and the -/// migration is purely additive — legacy rows simply have no envelope. -/// -/// The `(source_system, source_id)` pair is the idempotency key for -/// `upsert_by_source`; the unique index enforces one memory per external -/// record. `content_hash` is the change detector. `connector_cursors` holds the -/// incremental-sync high-water mark and last full-reconcile time per -/// (source_system, scope). -/// -/// The `ALTER TABLE ... ADD COLUMN` statements are split into -/// `MIGRATION_V17_ALTER_COLUMNS` and run individually by the migration runner, -/// because SQLite has no `ADD COLUMN IF NOT EXISTS`; duplicate-column errors are -/// swallowed so replay stays idempotent. -const MIGRATION_V17_UP: &str = r#" --- Idempotency key: at most one memory per (source_system, source_id). --- Partial unique index so the millions of envelope-less legacy rows (all NULL) --- don't collide and don't pay index cost. -CREATE UNIQUE INDEX IF NOT EXISTS idx_nodes_source_key - ON knowledge_nodes(source_system, source_id) - WHERE source_system IS NOT NULL AND source_id IS NOT NULL; - --- Filter/scan support for source-aware search + reconciliation passes. -CREATE INDEX IF NOT EXISTS idx_nodes_source_system - ON knowledge_nodes(source_system) - WHERE source_system IS NOT NULL; - -CREATE INDEX IF NOT EXISTS idx_nodes_source_project - ON knowledge_nodes(source_project) - WHERE source_project IS NOT NULL; - --- Per-connector incremental-sync checkpoint. One row per (source_system, scope) --- e.g. ('github', 'samvallad33/vestige'). `cursor_updated_at` is the --- high-water mark on the source's update timestamp; `last_full_reconcile_at` --- gates the (expensive) deletion-reconcile pass. -CREATE TABLE IF NOT EXISTS connector_cursors ( - source_system TEXT NOT NULL, - scope TEXT NOT NULL, - cursor_updated_at TEXT, - last_synced_at TEXT, - last_full_reconcile_at TEXT, - records_seen INTEGER NOT NULL DEFAULT 0, - config TEXT NOT NULL DEFAULT '{}', - PRIMARY KEY (source_system, scope) -); - -UPDATE schema_version SET version = 17, applied_at = datetime('now'); -"#; - -/// The `ALTER TABLE` statements for V17. Run individually + idempotently by the -/// migration runner (SQLite has no `ADD COLUMN IF NOT EXISTS`). -pub const MIGRATION_V17_ALTER_COLUMNS: &[&str] = &[ - "ALTER TABLE knowledge_nodes ADD COLUMN source_system TEXT", - "ALTER TABLE knowledge_nodes ADD COLUMN source_id TEXT", - "ALTER TABLE knowledge_nodes ADD COLUMN source_url TEXT", - "ALTER TABLE knowledge_nodes ADD COLUMN source_updated_at TEXT", - "ALTER TABLE knowledge_nodes ADD COLUMN content_hash TEXT", - "ALTER TABLE knowledge_nodes ADD COLUMN synced_at TEXT", - "ALTER TABLE knowledge_nodes ADD COLUMN source_project TEXT", - "ALTER TABLE knowledge_nodes ADD COLUMN source_type TEXT", - "ALTER TABLE knowledge_nodes ADD COLUMN source_author TEXT", -]; - -/// V18: Agent Black Box + Memory Receipts + Memory PRs. -/// -/// Three append-only / review tables that turn Vestige into the *black box, -/// immune system, and cinematic debugger for agent memory*: -/// -/// - `agent_traces` — one row per [`crate::trace::MemoryTraceEvent`], ordered by -/// `(run_id, seq)`. Append-only so a run replays exactly as the agent -/// experienced it. `payload` is the full serialized event; `event_type` and -/// `run_id` are denormalized for fast filtering and the `vestige://trace/{id}` -/// resource. `args_hash` (for `mcp.call`) is stored, never the raw args, so -/// traces can't leak prompt contents or secrets. -/// -/// - `memory_receipts` — one row per retrieval receipt. `payload` holds the full -/// [`crate::trace::Receipt`]; the scalar columns (`trust_floor`, `decay_risk`) -/// are denormalized for list/sort without parsing JSON. -/// -/// - `memory_prs` — the risk-gated review queue. A risky write (contradiction -/// vs high-trust, supersede/forget/merge/protect, sensitive topic, dream -/// consolidation, decay resurrection, low-confidence batch, weak-provenance -/// connector) lands here as `pending` instead of auto-committing. `diff` is the -/// structured before/after, `signals` is the self-explaining risk evidence, -/// `run_id` links the PR back to the black-box trace that produced it. -/// -/// `memory_id` / `run_id` references are intentionally *not* foreign keys to -/// `knowledge_nodes`: forgetting or superseding a memory must never erase the -/// audit trail of the trace, receipt, or PR that touched it (same -/// audit-preserving stance as V15's composition tables). -const MIGRATION_V18_UP: &str = r#" --- Black-box trace events: append-only, ordered by (run_id, seq). -CREATE TABLE IF NOT EXISTS agent_traces ( - id TEXT PRIMARY KEY, - run_id TEXT NOT NULL, - seq INTEGER NOT NULL, - event_type TEXT NOT NULL, -- mcp.call | memory.retrieve | ... - tool TEXT, -- denormalized for mcp.call rows - payload TEXT NOT NULL, -- full serialized MemoryTraceEvent (JSON) - at INTEGER NOT NULL, -- wall-clock millis - created_at TEXT NOT NULL -); - -CREATE INDEX IF NOT EXISTS idx_agent_traces_run ON agent_traces(run_id, seq); -CREATE INDEX IF NOT EXISTS idx_agent_traces_type ON agent_traces(event_type); -CREATE INDEX IF NOT EXISTS idx_agent_traces_at ON agent_traces(at); - --- One row per agent run, for the Black Box run list (denormalized roll-up). -CREATE TABLE IF NOT EXISTS agent_runs ( - run_id TEXT PRIMARY KEY, - first_tool TEXT, - event_count INTEGER NOT NULL DEFAULT 0, - retrieved_count INTEGER NOT NULL DEFAULT 0, - suppressed_count INTEGER NOT NULL DEFAULT 0, - write_count INTEGER NOT NULL DEFAULT 0, - veto_count INTEGER NOT NULL DEFAULT 0, - started_at INTEGER NOT NULL, -- millis of first event - last_at INTEGER NOT NULL, -- millis of latest event - created_at TEXT NOT NULL -); - -CREATE INDEX IF NOT EXISTS idx_agent_runs_last_at ON agent_runs(last_at DESC); - --- Retrieval receipts (the "nutrition label" for a piece of agent memory). -CREATE TABLE IF NOT EXISTS memory_receipts ( - receipt_id TEXT PRIMARY KEY, - run_id TEXT, -- links to the trace, if any - tool TEXT, - query TEXT, - retrieved_count INTEGER NOT NULL DEFAULT 0, - suppressed_count INTEGER NOT NULL DEFAULT 0, - trust_floor REAL NOT NULL DEFAULT 0, - decay_risk TEXT NOT NULL DEFAULT 'low', - payload TEXT NOT NULL, -- full serialized Receipt (JSON) - created_at TEXT NOT NULL -); - -CREATE INDEX IF NOT EXISTS idx_memory_receipts_run ON memory_receipts(run_id); -CREATE INDEX IF NOT EXISTS idx_memory_receipts_created_at ON memory_receipts(created_at DESC); - --- Memory PRs: the risk-gated review queue for brain changes. -CREATE TABLE IF NOT EXISTS memory_prs ( - id TEXT PRIMARY KEY, - kind TEXT NOT NULL, -- new_fact | contradiction_detected | ... - status TEXT NOT NULL DEFAULT 'pending', - title TEXT NOT NULL, - subject_id TEXT, -- the memory this PR concerns, if any - run_id TEXT, -- the run that produced it - diff TEXT NOT NULL DEFAULT '{}', -- structured before/after (JSON) - signals TEXT NOT NULL DEFAULT '[]', -- self-explaining RiskSignal[] (JSON) - decision TEXT, -- promote | merge | supersede | ... - created_at TEXT NOT NULL, - decided_at TEXT -); - -CREATE INDEX IF NOT EXISTS idx_memory_prs_status ON memory_prs(status); -CREATE INDEX IF NOT EXISTS idx_memory_prs_kind ON memory_prs(kind); -CREATE INDEX IF NOT EXISTS idx_memory_prs_created_at ON memory_prs(created_at DESC); - -UPDATE schema_version SET version = 18, applied_at = datetime('now'); -"#; - -const MIGRATION_V19_UP: &str = r#" --- Scope the source idempotency key by source_project. The V17 index keyed only --- on (source_system, source_id), so two sources of the same system (e.g. github --- repos octocat/repoA + octocat/repoB, or two Redmine instances) with the same --- bare per-project id ("5") collided and overwrote each other's memory in place. --- Include source_project so each (system, project, id) gets its own row. --- COALESCE keeps legacy rows (source_project NULL) sharing a single bucket, --- matching the upsert lookup's `source_project IS ?` semantics. -DROP INDEX IF EXISTS idx_nodes_source_key; - -CREATE UNIQUE INDEX IF NOT EXISTS idx_nodes_source_key - ON knowledge_nodes(source_system, COALESCE(source_project, ''), source_id) - WHERE source_system IS NOT NULL AND source_id IS NOT NULL; - -UPDATE schema_version SET version = 19, applied_at = datetime('now'); -"#; - /// Apply pending migrations -/// -/// Each migration is applied inside an explicit transaction so its schema -/// change and its trailing `UPDATE schema_version` commit atomically. If a -/// migration fails partway (crash-free error, lock, disk-full), the whole -/// migration rolls back and `schema_version` stays behind — so the next run -/// replays it from a clean state instead of hitting a fatal -/// `duplicate column name` on a half-applied `ADD COLUMN` (which previously -/// bricked the DB permanently). VACUUM (V7) cannot run inside a transaction, -/// so it runs after the transaction commits. pub fn apply_migrations(conn: &rusqlite::Connection) -> rusqlite::Result { let current_version = get_current_version(conn)?; let mut applied = 0; @@ -1177,62 +704,14 @@ pub fn apply_migrations(conn: &rusqlite::Connection) -> rusqlite::Result { migration.description ); - // Atomic per-migration: schema change + version bump commit together - // or roll back together. On rollback, schema_version is unchanged so - // the migration cleanly re-applies next run. - { - let tx = conn.unchecked_transaction()?; + // Use execute_batch to handle multi-statement SQL including triggers + conn.execute_batch(migration.up)?; - // V14: add the two bitemporal/protect columns BEFORE the batch (the - // batch's indexes reference them). SQLite lacks - // `ADD COLUMN IF NOT EXISTS`, so swallow the "duplicate column" - // error to stay idempotent on replay. - if migration.version == 14 { - add_column_if_missing( - &tx, - "ALTER TABLE knowledge_nodes ADD COLUMN protected INTEGER NOT NULL DEFAULT 0", - )?; - add_column_if_missing( - &tx, - "ALTER TABLE knowledge_nodes ADD COLUMN superseded_by TEXT", - )?; - } - - // V16 adds columns via ALTER TABLE, which SQLite does not support - // with IF NOT EXISTS. Run them individually and ignore duplicate - // column errors so replay stays idempotent. - if migration.version == 16 { - for stmt in MIGRATION_V16_ALTER_COLUMNS { - add_column_if_missing(&tx, stmt)?; - } - } - - // V17 (#57) adds the source-envelope columns. Same idempotent - // ALTER handling as V16 — the unique index in the V17 batch - // references these columns, so they must exist before the batch. - if migration.version == 17 { - for stmt in MIGRATION_V17_ALTER_COLUMNS { - add_column_if_missing(&tx, stmt)?; - } - } - - // Use execute_batch to handle multi-statement SQL including triggers - tx.execute_batch(migration.up)?; - - tx.commit()?; - } - - // V7: Upgrade page_size to 8192 (10-30% faster large-row reads). - // VACUUM rewrites the DB with the new page size — it cannot run - // inside a transaction, so it runs after the migration commits. - // Under WAL, changing page_size + VACUUM is silently ignored; SQLite - // requires a non-WAL journal mode to repage. Drop to DELETE, repage, - // then restore WAL so the performance upgrade actually takes effect. + // V7: Upgrade page_size to 8192 (10-30% faster large-row reads) + // VACUUM rewrites the DB with the new page size — can't run inside execute_batch if migration.version == 7 { - conn.pragma_update(None, "journal_mode", "DELETE")?; conn.pragma_update(None, "page_size", 8192)?; conn.execute_batch("VACUUM;")?; - conn.pragma_update(None, "journal_mode", "WAL")?; tracing::info!("Database page_size upgraded to 8192 via VACUUM"); } @@ -1247,70 +726,21 @@ pub fn apply_migrations(conn: &rusqlite::Connection) -> rusqlite::Result { mod tests { use super::*; - /// Regression: a migration that is interrupted after its `ADD COLUMN` - /// commits but before `schema_version` advances must NOT permanently brick - /// the DB on replay with `duplicate column name`. Because each migration now - /// runs in a transaction, an interrupted migration rolls back atomically and - /// replays cleanly. - /// - /// We simulate the pre-fix corrupt state directly: run all migrations, then - /// hand-apply one of V2's `ADD COLUMN`s again on a DB whose version we roll - /// back, and confirm `apply_migrations` still succeeds (the transaction - /// makes the whole migration atomic, so a real interruption can never leave - /// the half-applied state the old code could). - #[test] - fn test_interrupted_migration_replays_without_duplicate_column_brick() { - // A fresh DB migrates cleanly and reaches the latest version. - let conn = rusqlite::Connection::open_in_memory().expect("open in-memory"); - apply_migrations(&conn).expect("initial migrations succeed"); - let latest = MIGRATIONS.last().unwrap().version; - assert_eq!(get_current_version(&conn).expect("version"), latest); - - // Running apply_migrations again on an already-migrated DB is a no-op and - // must never error (idempotent) — the previous brick surfaced here. - let applied = apply_migrations(&conn).expect("replay must not brick"); - assert_eq!(applied, 0, "no migrations should re-apply on a current DB"); - - // Directly prove atomicity: an ADD COLUMN inside a rolled-back - // transaction leaves no trace, so a retried migration sees a clean slate. - let conn2 = rusqlite::Connection::open_in_memory().expect("open in-memory 2"); - conn2 - .execute_batch( - "CREATE TABLE t (id INTEGER); - CREATE TABLE schema_version (version INTEGER, applied_at TEXT); - INSERT INTO schema_version (version, applied_at) VALUES (0, datetime('now'));", - ) - .expect("seed"); - { - let tx = conn2.unchecked_transaction().expect("tx"); - tx.execute_batch("ALTER TABLE t ADD COLUMN c INTEGER;") - .expect("add column in tx"); - // Simulate mid-migration failure: drop the tx without committing. - drop(tx); - } - // The column must be gone (rolled back), so re-adding it succeeds — the - // exact operation that previously failed with "duplicate column name". - conn2 - .execute_batch("ALTER TABLE t ADD COLUMN c INTEGER;") - .expect("column must not survive a rolled-back transaction"); - } - /// 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_v16_and_drops_dead_tables() { + 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 the latest migration + // 1. schema_version advanced to V11 let version = get_current_version(&conn).expect("read schema_version"); - let latest = MIGRATIONS.last().unwrap().version; assert_eq!( - version, latest, - "schema_version must be the latest migration after all migrations" + version, 11, + "schema_version must be 11 after all migrations" ); // 2. knowledge_edges is gone (V11 drops it) @@ -1338,79 +768,6 @@ mod tests { compressed_memories_rows, 0, "compressed_memories table must be dropped by V11" ); - - // 4. sync_tombstones exists (V12 creates it) - let sync_tombstone_rows: i64 = conn - .query_row( - "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='sync_tombstones'", - [], - |row| row.get(0), - ) - .expect("query sqlite_master"); - assert_eq!( - sync_tombstone_rows, 1, - "sync_tombstones table must be created by V12" - ); - - // 5. deletion_tombstones exists (V13 creates it) - let deletion_tombstone_rows: i64 = conn - .query_row( - "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='deletion_tombstones'", - [], - |row| row.get(0), - ) - .expect("query sqlite_master"); - assert_eq!( - deletion_tombstone_rows, 1, - "deletion_tombstones table must be created by V13" - ); - - // 6. merge_plans + merge_operations exist (V14 creates them) - for table in ["merge_plans", "merge_operations"] { - let rows: i64 = conn - .query_row( - "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?1", - [table], - |row| row.get(0), - ) - .expect("query sqlite_master"); - assert_eq!(rows, 1, "{table} table must be created by V14"); - } - - // 7. ComposedGraph tables exist (V15) - for table in [ - "composition_events", - "composition_members", - "composition_outcomes", - ] { - let rows: i64 = conn - .query_row( - "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?1", - [table], - |row| row.get(0), - ) - .expect("query sqlite_master"); - assert_eq!(rows, 1, "{table} table must be created by V15"); - } - - // 8. knowledge_nodes gains `protected` + `superseded_by` (V14) - let node_cols: Vec = { - let mut stmt = conn - .prepare("PRAGMA table_info(knowledge_nodes)") - .expect("prepare table_info"); - stmt.query_map([], |row| row.get::<_, String>(1)) - .expect("query table_info") - .filter_map(|r| r.ok()) - .collect() - }; - assert!( - node_cols.iter().any(|c| c == "protected"), - "knowledge_nodes must have `protected` column after V14" - ); - assert!( - node_cols.iter().any(|c| c == "superseded_by"), - "knowledge_nodes must have `superseded_by` column after V14" - ); } /// V11 must be idempotent on replay — if the tables were already dropped @@ -1428,235 +785,10 @@ mod tests { conn.execute("UPDATE schema_version SET version = 10", []) .expect("rewind schema_version"); - // Replay V11 onward. V11 uses DROP TABLE IF EXISTS so it is idempotent. - // V12/V13 tombstone tables use CREATE TABLE IF NOT EXISTS. V14/V16 ALTER - // TABLE idempotency is handled by the migration runner. - apply_migrations(&conn).expect("V11..V17 replay must be idempotent"); + // Replay must not error. + apply_migrations(&conn).expect("V11 replay must be idempotent"); - // After replaying from V10, the schema advances to the latest version. let version = get_current_version(&conn).expect("read schema_version"); - assert_eq!( - version, - MIGRATIONS.last().unwrap().version, - "schema_version back at latest after replay" - ); - } - - #[test] - fn v16_adds_embedding_model_table() { - let conn = rusqlite::Connection::open_in_memory().expect("open in-memory"); - apply_migrations(&conn).expect("apply_migrations"); - let count: i64 = conn - .query_row( - "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='embedding_model'", - [], - |row| row.get(0), - ) - .expect("query sqlite_master"); - assert_eq!(count, 1, "embedding_model table must exist after V16"); - } - - #[test] - fn v16_adds_domains_columns() { - let conn = rusqlite::Connection::open_in_memory().expect("open in-memory"); - apply_migrations(&conn).expect("apply_migrations"); - let info: Vec = { - let mut stmt = conn - .prepare("PRAGMA table_info(knowledge_nodes)") - .expect("prepare"); - stmt.query_map([], |row| row.get::<_, String>(1)) - .expect("query_map") - .map(|r| r.expect("row")) - .collect() - }; - assert!( - info.contains(&"domains".to_string()), - "domains column missing" - ); - assert!( - info.contains(&"domain_scores".to_string()), - "domain_scores column missing" - ); - } - - #[test] - fn v16_default_values_empty_json() { - let conn = rusqlite::Connection::open_in_memory().expect("open in-memory"); - apply_migrations(&conn).expect("apply_migrations"); - // Insert a minimal row to test defaults - conn.execute( - "INSERT INTO knowledge_nodes (id, content, node_type, created_at, updated_at, last_accessed, \ - stability, difficulty, reps, lapses, learning_state, storage_strength, retrieval_strength, \ - retention_strength, next_review, scheduled_days, has_embedding) \ - VALUES ('test-id','content','fact',datetime('now'),datetime('now'),datetime('now'),\ - 1.0,0.3,0,0,'new',1.0,1.0,1.0,datetime('now'),1,0)", - [], - ).expect("insert row"); - let (domains, domain_scores): (String, String) = conn - .query_row( - "SELECT domains, domain_scores FROM knowledge_nodes WHERE id='test-id'", - [], - |row| Ok((row.get(0)?, row.get(1)?)), - ) - .expect("query row"); - assert_eq!(domains, "[]"); - assert_eq!(domain_scores, "{}"); - } - - #[test] - fn v16_is_replayable() { - let conn = rusqlite::Connection::open_in_memory().expect("open in-memory"); - apply_migrations(&conn).expect("first apply"); - // Rewind to V15 so V16 runs again. - conn.execute("UPDATE schema_version SET version = 15", []) - .expect("rewind"); - // V16 uses CREATE TABLE IF NOT EXISTS and idempotent ALTER handling. - apply_migrations(&conn).expect("V16 replay must be idempotent"); - let version = get_current_version(&conn).expect("read version"); - assert_eq!( - version, - MIGRATIONS.last().unwrap().version, - "schema_version must be latest after replay" - ); - } - - #[test] - fn v17_adds_source_envelope_columns_and_cursor_table() { - let conn = rusqlite::Connection::open_in_memory().expect("open in-memory"); - apply_migrations(&conn).expect("apply_migrations"); - - // All nine envelope columns must exist on knowledge_nodes. - let cols: Vec = { - let mut stmt = conn - .prepare("PRAGMA table_info(knowledge_nodes)") - .expect("prepare"); - stmt.query_map([], |row| row.get::<_, String>(1)) - .expect("query_map") - .filter_map(|r| r.ok()) - .collect() - }; - for c in [ - "source_system", - "source_id", - "source_url", - "source_updated_at", - "content_hash", - "synced_at", - "source_project", - "source_type", - "source_author", - ] { - assert!( - cols.iter().any(|x| x == c), - "knowledge_nodes must have `{c}` column after V17" - ); - } - - // connector_cursors table must exist. - let cursor_rows: i64 = conn - .query_row( - "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='connector_cursors'", - [], - |row| row.get(0), - ) - .expect("query sqlite_master"); - assert_eq!(cursor_rows, 1, "connector_cursors must be created by V17"); - } - - #[test] - fn v17_unique_source_key_index_allows_many_null_legacy_rows() { - let conn = rusqlite::Connection::open_in_memory().expect("open in-memory"); - apply_migrations(&conn).expect("apply_migrations"); - - // Two legacy rows with NULL source key must NOT collide on the partial - // unique index (the index only covers non-NULL keys). - for id in ["a", "b"] { - conn.execute( - "INSERT INTO knowledge_nodes (id, content, node_type, created_at, updated_at, last_accessed, \ - stability, difficulty, reps, lapses, learning_state, storage_strength, retrieval_strength, \ - retention_strength, next_review, scheduled_days, has_embedding) \ - VALUES (?1,'c','fact',datetime('now'),datetime('now'),datetime('now'),\ - 1.0,0.3,0,0,'new',1.0,1.0,1.0,datetime('now'),1,0)", - [id], - ) - .expect("insert legacy row"); - } - - // Two real connector rows that share (source_system, source_id) MUST - // collide — the unique index is the idempotency guarantee. - conn.execute( - "UPDATE knowledge_nodes SET source_system='github', source_id='1' WHERE id='a'", - [], - ) - .expect("set source key on a"); - let dup = conn.execute( - "UPDATE knowledge_nodes SET source_system='github', source_id='1' WHERE id='b'", - [], - ); - assert!( - dup.is_err(), - "duplicate (source_system, source_id) must violate the unique index" - ); - } - - #[test] - fn v17_is_replayable() { - let conn = rusqlite::Connection::open_in_memory().expect("open in-memory"); - apply_migrations(&conn).expect("first apply"); - conn.execute("UPDATE schema_version SET version = 16", []) - .expect("rewind to 16"); - apply_migrations(&conn).expect("V17 replay must be idempotent"); - let version = get_current_version(&conn).expect("read version"); - assert_eq!( - version, - MIGRATIONS.last().unwrap().version, - "schema_version must be latest after replay" - ); - } - - #[test] - fn v16_preserves_existing_rows_from_v15() { - let conn = rusqlite::Connection::open_in_memory().expect("open in-memory"); - // Apply up to V15 only, including the V14 ALTER TABLE columns that - // `apply_migrations` normally runs before the V14 SQL batch. - for migration in MIGRATIONS { - if migration.version <= 15 { - if migration.version == 14 { - add_column_if_missing( - &conn, - "ALTER TABLE knowledge_nodes ADD COLUMN protected INTEGER NOT NULL DEFAULT 0", - ) - .expect("apply V14 protected column"); - add_column_if_missing( - &conn, - "ALTER TABLE knowledge_nodes ADD COLUMN superseded_by TEXT", - ) - .expect("apply V14 superseded_by column"); - } - conn.execute_batch(migration.up).expect("apply migration"); - } - } - // Insert a row under the V15 schema, before PR #61's V16 columns exist. - conn.execute( - "INSERT INTO knowledge_nodes (id, content, node_type, created_at, updated_at, last_accessed, \ - stability, difficulty, reps, lapses, learning_state, storage_strength, retrieval_strength, \ - retention_strength, next_review, scheduled_days, has_embedding) \ - VALUES ('existing-id','old content','fact',datetime('now'),datetime('now'),datetime('now'),\ - 1.0,0.3,0,0,'new',1.0,1.0,1.0,datetime('now'),1,0)", - [], - ).expect("insert pre-v16 row"); - apply_migrations(&conn).expect("apply V16 migration"); - - // Check the old row has defaults - let (domains, domain_scores): (String, String) = conn - .query_row( - "SELECT domains, domain_scores FROM knowledge_nodes WHERE id='existing-id'", - [], - |row| Ok((row.get(0)?, row.get(1)?)), - ) - .expect("query pre-v16 row"); - assert_eq!(domains, "[]"); - assert_eq!(domain_scores, "{}"); + assert_eq!(version, 11, "schema_version back at 11 after replay"); } } - diff --git a/crates/vestige-core/src/storage/mod.rs b/crates/vestige-core/src/storage/mod.rs index 84bb2de..eb224fa 100644 --- a/crates/vestige-core/src/storage/mod.rs +++ b/crates/vestige-core/src/storage/mod.rs @@ -1,41 +1,16 @@ //! Storage Module //! -//! Backend-agnostic memory store abstraction plus SQLite reference impl. +//! SQLite-based storage layer with: +//! - FTS5 full-text search with query sanitization +//! - Embedded vector storage +//! - FSRS-6 state management +//! - Temporal memory support -#[cfg(feature = "cloud-sync")] -mod cloud_crypto; -#[cfg(feature = "cloud-sync")] -mod cloud_sync; -mod memory_store; mod migrations; -mod portable; mod sqlite; -mod trace_store; -#[cfg(feature = "cloud-sync")] -pub use cloud_sync::HttpPortableSyncBackend; - -pub use memory_store::{ - ClassificationResult, Domain, HealthStatus, LocalMemoryStore, MemoryEdge, MemoryRecord, - MemoryStore, MemoryStoreError, MemoryStoreResult, MemoryStoreSend, ModelSignature, - SchedulingState, SearchQuery, SearchResult, StoreStats, -}; pub use migrations::MIGRATIONS; -pub use portable::{ - PORTABLE_ARCHIVE_FORMAT, PortableArchive, PortableImportMode, PortableImportReport, - PortableTable, PortableValue, -}; pub use sqlite::{ - CompositionEventRecord, CompositionMemberRecord, CompositionNeighborRecord, - CompositionOutcomeRecord, ConnectionRecord, ConnectorCursor, ConsolidationHistoryRecord, - DreamHistoryRecord, FilePortableSyncBackend, InsightRecord, IntentionRecord, - NeverComposedCandidate, PortableSyncBackend, PortableSyncReport, ReconcileReport, Result, - SmartIngestResult, SourceUpsertOutcome, SourceUpsertResult, SqliteMemoryStore, - StateTransitionRecord, StorageError, + ConnectionRecord, ConsolidationHistoryRecord, DreamHistoryRecord, InsightRecord, + IntentionRecord, Result, SmartIngestResult, StateTransitionRecord, Storage, StorageError, }; -pub use trace_store::AgentRunSummary; - -/// Backwards-compatibility alias. Retained until Phase 4 completes so every -/// existing `Arc` call site keeps compiling. Scheduled for removal -/// once no downstream source file references it. -pub type Storage = SqliteMemoryStore; diff --git a/crates/vestige-core/src/storage/portable.rs b/crates/vestige-core/src/storage/portable.rs deleted file mode 100644 index dda0a74..0000000 --- a/crates/vestige-core/src/storage/portable.rs +++ /dev/null @@ -1,171 +0,0 @@ -//! Portable archive types for exact Vestige-to-Vestige transfer. -//! -//! This format preserves SQLite row data instead of re-ingesting memories. It is -//! intentionally storage-level: import can keep IDs, FSRS state, graph edges, -//! suppression state, embeddings, and audit/history rows intact. - -use chrono::{DateTime, Utc}; -use rusqlite::types::Value; -use serde::{Deserialize, Serialize}; - -/// Current portable archive format identifier. -pub const PORTABLE_ARCHIVE_FORMAT: &str = "vestige.portable.v1"; - -/// Full exact portable archive. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PortableArchive { - /// Stable format marker used for compatibility checks. - pub archive_format: String, - /// Vestige version that produced the archive. - pub vestige_version: String, - /// SQLite schema version of the source database. - pub schema_version: u32, - /// Archive creation timestamp. - pub exported_at: DateTime, - /// Export mode. v1 only writes "exact". - pub mode: String, - /// Dumped storage tables in deterministic import order. - pub tables: Vec, -} - -impl PortableArchive { - /// Count all rows across all tables. - pub fn total_rows(&self) -> usize { - self.tables.iter().map(|table| table.rows.len()).sum() - } -} - -/// One table in a portable archive. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PortableTable { - /// SQLite table name. - pub name: String, - /// Column names in row value order. - pub columns: Vec, - /// Raw rows. Each row has the same order as `columns`. - pub rows: Vec>, -} - -/// SQLite value encoded in JSON. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(tag = "type", content = "value", rename_all = "camelCase")] -pub enum PortableValue { - /// SQL NULL. - Null, - /// SQL INTEGER. - Integer(i64), - /// SQL REAL. - Real(f64), - /// SQL TEXT. - Text(String), - /// SQL BLOB, hex encoded. - Blob(String), -} - -impl PortableValue { - /// Convert this portable value back into a rusqlite owned value. - pub(crate) fn to_sql_value(&self) -> Result { - match self { - Self::Null => Ok(Value::Null), - Self::Integer(value) => Ok(Value::Integer(*value)), - Self::Real(value) => Ok(Value::Real(*value)), - Self::Text(value) => Ok(Value::Text(value.clone())), - Self::Blob(value) => decode_hex(value).map(Value::Blob), - } - } -} - -/// Import behavior for duplicate primary keys. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PortableImportMode { - /// Reject import if user data already exists, then insert rows exactly. - EmptyOnly, - /// Merge archive rows into an existing database. - /// - /// This mode is intended for file-backed sync between devices. It applies - /// tombstones, upserts row-keyed state, and appends audit/history rows. - Merge, -} - -/// Summary of an exact portable import. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PortableImportReport { - /// Number of imported tables. - pub tables_imported: usize, - /// Number of imported rows. - pub rows_imported: usize, - /// Number of archive tables skipped because the target schema lacks them. - pub tables_skipped: usize, - /// Whether FTS was rebuilt after import. - pub fts_rebuilt: bool, - /// Number of rows inserted. - #[serde(default)] - pub rows_inserted: usize, - /// Number of existing rows updated/replaced. - #[serde(default)] - pub rows_updated: usize, - /// Number of rows skipped because local state was newer or unsupported. - #[serde(default)] - pub rows_skipped: usize, - /// Number of local rows deleted by imported tombstones. - #[serde(default)] - pub rows_deleted: usize, - /// Number of merge conflicts resolved by keeping local state. - #[serde(default)] - pub conflicts_kept_local: usize, -} - -pub(crate) fn encode_hex(bytes: &[u8]) -> String { - const HEX: &[u8; 16] = b"0123456789abcdef"; - let mut out = String::with_capacity(bytes.len() * 2); - for &byte in bytes { - out.push(HEX[(byte >> 4) as usize] as char); - out.push(HEX[(byte & 0x0f) as usize] as char); - } - out -} - -fn decode_hex(input: &str) -> Result, String> { - if !input.len().is_multiple_of(2) { - return Err("hex blob has odd length".to_string()); - } - - let mut out = Vec::with_capacity(input.len() / 2); - let bytes = input.as_bytes(); - for chunk in bytes.chunks_exact(2) { - let high = hex_value(chunk[0])?; - let low = hex_value(chunk[1])?; - out.push((high << 4) | low); - } - Ok(out) -} - -fn hex_value(byte: u8) -> Result { - match byte { - b'0'..=b'9' => Ok(byte - b'0'), - b'a'..=b'f' => Ok(byte - b'a' + 10), - b'A'..=b'F' => Ok(byte - b'A' + 10), - _ => Err(format!("invalid hex byte: {}", byte as char)), - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn hex_round_trip() { - let bytes = vec![0, 1, 2, 15, 16, 127, 128, 255]; - let encoded = encode_hex(&bytes); - assert_eq!(decode_hex(&encoded).unwrap(), bytes); - } - - #[test] - fn rejects_invalid_hex() { - assert!(decode_hex("f").is_err()); - assert!(decode_hex("zz").is_err()); - } -} diff --git a/crates/vestige-core/src/storage/sqlite.rs b/crates/vestige-core/src/storage/sqlite.rs index c48fcec..398db9f 100644 --- a/crates/vestige-core/src/storage/sqlite.rs +++ b/crates/vestige-core/src/storage/sqlite.rs @@ -2,43 +2,32 @@ //! //! Core storage layer with integrated embeddings and vector search. -use chrono::{DateTime, Duration, NaiveDateTime, Utc}; -use directories::{BaseDirs, ProjectDirs}; -#[cfg(all(feature = "embeddings", feature = "vector-search"))] +use chrono::{DateTime, Duration, Utc}; +use directories::ProjectDirs; +#[cfg(feature = "embeddings")] use lru::LruCache; -use rusqlite::types::{Type, Value, ValueRef}; -use rusqlite::{Connection, OptionalExtension, params, params_from_iter}; -use std::collections::{HashMap, HashSet}; -use std::io::Write; -#[cfg(all(feature = "embeddings", feature = "vector-search"))] +use rusqlite::{Connection, OptionalExtension, params}; +#[cfg(feature = "embeddings")] use std::num::NonZeroUsize; -use std::path::{Component, Path, PathBuf}; +use std::path::PathBuf; use std::sync::Mutex; use uuid::Uuid; use crate::fsrs::{ - DEFAULT_DECAY, FSRSScheduler, FSRSState, LearningState, MAX_STABILITY, Rating, - retrievability_with_decay, + DEFAULT_DECAY, FSRSScheduler, FSRSState, LearningState, Rating, retrievability_with_decay, }; -use crate::fts::{sanitize_fts5_or_query, sanitize_fts5_query}; +use crate::fts::sanitize_fts5_query; use crate::memory::{ - ConsolidationResult, IngestInput, KnowledgeNode, MatchType, MemoryStats, RecallInput, - SearchMode, SearchResult, + ConsolidationResult, IngestInput, KnowledgeNode, MemoryStats, RecallInput, SearchMode, }; #[cfg(all(feature = "embeddings", feature = "vector-search"))] -use crate::memory::{EmbeddingResult, SimilarityResult}; -use crate::storage::portable::{ - PORTABLE_ARCHIVE_FORMAT, PortableArchive, PortableImportMode, PortableImportReport, - PortableTable, PortableValue, encode_hex, -}; +use crate::memory::{EmbeddingResult, MatchType, SearchResult, SimilarityResult}; #[cfg(feature = "embeddings")] -use crate::embeddings::EmbeddingService; -#[cfg(all(feature = "embeddings", feature = "vector-search"))] -use crate::embeddings::{EMBEDDING_DIMENSIONS, Embedding, matryoshka_truncate}; +use crate::embeddings::{EMBEDDING_DIMENSIONS, Embedding, EmbeddingService, matryoshka_truncate}; #[cfg(feature = "vector-search")] -use crate::search::{VectorIndex, reciprocal_rank_fusion}; +use crate::search::{VectorIndex, linear_combination}; #[cfg(all(feature = "embeddings", feature = "vector-search"))] use crate::search::hyde; @@ -87,382 +76,31 @@ pub struct SmartIngestResult { pub prediction_error: Option, /// Human-readable explanation of the decision pub reason: String, - /// Previous content when smart ingest mutated an existing memory. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub previous_content: Option, - /// Existing memory id that received merged or appended content. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub merged_from: Option, - /// Full updated content after a merge/append/context write. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub merge_preview: Option, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum MergeWrite { - Inserted, - Updated, -} - -/// Backend interface for portable sync storage. -/// -/// The first shipped backend is a local file, which works with Dropbox, iCloud, -/// Syncthing, Git, shared volumes, or any other folder sync tool. Remote stores -/// can implement this trait without changing merge semantics. -pub trait PortableSyncBackend { - /// Human-readable backend label for reports. - fn label(&self) -> String; - /// Read the current remote archive. `Ok(None)` means no remote exists yet. - fn read_archive(&self) -> Result>; - /// Atomically write the merged archive back to the backend when possible. - fn write_archive(&self, archive: &PortableArchive) -> Result<()>; -} - -/// File-backed portable sync backend. -#[derive(Debug, Clone)] -pub struct FilePortableSyncBackend { - path: PathBuf, -} - -impl FilePortableSyncBackend { - /// Create a file-backed sync backend for a portable archive path. - pub fn new(path: impl Into) -> Self { - Self { path: path.into() } - } - - /// Archive path backing this sync store. - pub fn path(&self) -> &Path { - &self.path - } -} - -impl PortableSyncBackend for FilePortableSyncBackend { - fn label(&self) -> String { - format!("file:{}", self.path.display()) - } - - fn read_archive(&self) -> Result> { - if !self.path.exists() { - return Ok(None); - } - let file = std::fs::File::open(&self.path)?; - let archive: PortableArchive = serde_json::from_reader(file).map_err(|e| { - StorageError::Init(format!( - "Failed to parse portable sync archive '{}': {}", - self.path.display(), - e - )) - })?; - Ok(Some(archive)) - } - - fn write_archive(&self, archive: &PortableArchive) -> Result<()> { - let parent = self.path.parent().unwrap_or_else(|| Path::new(".")); - std::fs::create_dir_all(parent)?; - let filename = self - .path - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or("vestige-sync.json"); - let temp_path = parent.join(format!(".{}.tmp-{}", filename, Uuid::new_v4())); - - #[cfg(unix)] - let mut file = { - use std::os::unix::fs::OpenOptionsExt; - std::fs::OpenOptions::new() - .write(true) - .create_new(true) - .mode(0o600) - .open(&temp_path)? - }; - #[cfg(not(unix))] - let mut file = std::fs::File::create(&temp_path)?; - if let Err(e) = serde_json::to_writer_pretty(&mut file, archive) { - let _ = std::fs::remove_file(&temp_path); - return Err(StorageError::Init(format!( - "Failed to write portable sync archive '{}': {}", - self.path.display(), - e - ))); - } - file.flush()?; - file.sync_all()?; - drop(file); - - if let Err(rename_err) = std::fs::rename(&temp_path, &self.path) { - if self.path.exists() { - std::fs::remove_file(&self.path)?; - std::fs::rename(&temp_path, &self.path)?; - } else { - let _ = std::fs::remove_file(&temp_path); - return Err(rename_err.into()); - } - } - Ok(()) - } -} - -/// Summary of a pull-merge-push sync operation. -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PortableSyncReport { - /// Backend label that was synced. - pub backend: String, - /// Whether an existing remote archive was pulled before pushing. - pub pulled: bool, - /// Merge report from the pull phase, if a remote archive existed. - pub pull: Option, - /// Number of tables written to the backend during push. - pub pushed_tables: usize, - /// Number of rows written to the backend during push. - pub pushed_rows: usize, - /// Portable archive format written during push. - pub archive_format: String, -} - -/// Report returned by an irreversible content purge. -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PurgeReport { - /// Memory ID requested for purge. - pub memory_id: String, - /// Whether a live memory row was found and removed. - pub deleted: bool, - /// Non-content tombstone timestamp. - pub deleted_at: DateTime, - /// Number of graph edges removed by foreign-key cascade. - pub edges_pruned: i64, - /// Number of insight rows whose source list was rewritten. - pub insights_rewritten: i64, - /// Number of insight rows dropped because fewer than two source memories remained. - pub insights_deleted: i64, - /// Number of temporal-summary children detached from this parent. - pub children_orphaned: i64, } // ============================================================================ // STORAGE // ============================================================================ -const PORTABLE_TABLES: &[&str] = &[ - "knowledge_nodes", - "node_embeddings", - "fsrs_cards", - "memory_states", - "memory_connections", - "memory_access_log", - "state_transitions", - "intentions", - "insights", - "sessions", - "fsrs_config", - "consolidation_history", - "dream_history", - "retention_snapshots", - "sync_tombstones", - "deletion_tombstones", - "composition_events", - "composition_members", - "composition_outcomes", -]; - -const PORTABLE_USER_DATA_TABLES: &[&str] = &[ - "knowledge_nodes", - "node_embeddings", - "fsrs_cards", - "memory_states", - "memory_connections", - "memory_access_log", - "state_transitions", - "intentions", - "insights", - "sessions", - "consolidation_history", - "dream_history", - "retention_snapshots", - "sync_tombstones", - "deletion_tombstones", - "composition_events", - "composition_members", - "composition_outcomes", -]; - -#[derive(Default)] -struct PortableMergeState { - locally_newer_nodes: HashSet, -} - -const DATA_DIR_ENV: &str = "VESTIGE_DATA_DIR"; -const DATABASE_FILE: &str = "vestige.db"; -const VESTIGE_DISABLE_VECTOR_SEARCH: &str = "VESTIGE_DISABLE_VECTOR_SEARCH"; - /// Main storage struct with integrated embedding and vector search /// /// Uses separate reader/writer connections for interior mutability. /// All methods take `&self` (not `&mut self`), making Storage `Send + Sync` /// so the MCP layer can use `Arc` instead of `Arc>`. -pub struct SqliteMemoryStore { - db_path: PathBuf, - // `pub(crate)` so the sibling `trace_store` module (Black Box / Receipts / - // Memory PRs CRUD) can lock the same writer/reader connections and follow - // the established store idiom without duplicating connection management. - pub(crate) writer: Mutex, - pub(crate) reader: Mutex, +pub struct Storage { + writer: Mutex, + reader: Mutex, scheduler: Mutex, #[cfg(feature = "embeddings")] embedding_service: EmbeddingService, #[cfg(feature = "vector-search")] - vector_index: Option>, + vector_index: Mutex, /// LRU cache for query embeddings to avoid re-embedding repeated queries - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - query_cache: Option>>>, - /// Cached model signature. `None` until the first embedding is written. - registered_model: std::sync::RwLock>, + #[cfg(feature = "embeddings")] + query_cache: Mutex>>, } -impl SqliteMemoryStore { - #[cfg(feature = "vector-search")] - fn vector_search_enabled_by_cpu() -> bool { - #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] - let has_required_features = std::arch::is_x86_feature_detected!("avx2") - && std::arch::is_x86_feature_detected!("fma"); - - #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] - let has_required_features = true; - - let disabled_by_env = std::env::var_os(VESTIGE_DISABLE_VECTOR_SEARCH) - .and_then(|v| { - let value = v.to_ascii_lowercase(); - if value == "1" - || value == "true" - || value == "yes" - || value == "on" - || value == "enable" - || value == "enabled" - { - Some(()) - } else { - None - } - }) - .is_some(); - - has_required_features && !disabled_by_env - } - - #[cfg(feature = "vector-search")] - fn vector_search_unavailable_reason() -> Option<&'static str> { - if std::env::var_os(VESTIGE_DISABLE_VECTOR_SEARCH).is_some() { - return Some("disabled by VESTIGE_DISABLE_VECTOR_SEARCH"); - } - - #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] - { - if !std::arch::is_x86_feature_detected!("avx2") { - return Some("unsupported CPU: AVX2 required"); - } - if !std::arch::is_x86_feature_detected!("fma") { - return Some("unsupported CPU: FMA required"); - } - } - - None - } - - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - fn vector_search_available(&self) -> bool { - self.vector_index.is_some() - } - - #[cfg(not(all(feature = "embeddings", feature = "vector-search")))] - fn vector_search_available(&self) -> bool { - false - } - - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - fn regular_ingest_result( - &self, - input: IngestInput, - reason: impl Into, - ) -> Result { - let node = self.ingest(input)?; - Ok(SmartIngestResult { - decision: "create".to_string(), - node, - superseded_id: None, - similarity: None, - prediction_error: Some(1.0), - reason: reason.into(), - previous_content: None, - merged_from: None, - merge_preview: None, - }) - } - - fn data_dir_from_env() -> Option { - std::env::var_os(DATA_DIR_ENV).and_then(|value| { - if value.is_empty() { - None - } else { - Some(PathBuf::from(value)) - } - }) - } - - fn expand_tilde(path: PathBuf) -> PathBuf { - let rest = { - let mut components = path.components(); - match components.next() { - Some(Component::Normal(first)) if first == "~" => { - Some(components.as_path().to_path_buf()) - } - _ => None, - } - }; - - match rest { - Some(rest) => BaseDirs::new() - .map(|dirs| dirs.home_dir().join(rest)) - .unwrap_or(path), - None => path, - } - } - - fn prepare_data_dir(data_dir: PathBuf) -> Result { - let data_dir = Self::expand_tilde(data_dir); - std::fs::create_dir_all(&data_dir)?; - // Restrict directory permissions to owner-only on Unix - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let perms = std::fs::Permissions::from_mode(0o700); - let _ = std::fs::set_permissions(&data_dir, perms); - } - Ok(data_dir.join(DATABASE_FILE)) - } - - /// Resolve a Vestige database path from an explicit data directory. - pub fn db_path_for_data_dir(data_dir: PathBuf) -> Result { - Self::prepare_data_dir(data_dir) - } - - /// Resolve the default Vestige database path. - /// - /// `VESTIGE_DATA_DIR` is treated as a directory and wins over the platform - /// per-user data directory. The database file is always `vestige.db` inside - /// that directory. - pub fn default_db_path() -> Result { - if let Some(data_dir) = Self::data_dir_from_env() { - return Self::prepare_data_dir(data_dir); - } - - let proj_dirs = ProjectDirs::from("com", "vestige", "core").ok_or_else(|| { - StorageError::Init("Could not determine project directories".to_string()) - })?; - - Self::prepare_data_dir(proj_dirs.data_dir().to_path_buf()) - } - +impl Storage { /// Apply PRAGMAs and optional encryption to a connection fn configure_connection(conn: &Connection) -> Result<()> { // Apply encryption key if SQLCipher is enabled and key is provided @@ -495,7 +133,22 @@ impl SqliteMemoryStore { pub fn new(db_path: Option) -> Result { let path = match db_path { Some(p) => p, - None => Self::default_db_path()?, + None => { + let proj_dirs = ProjectDirs::from("com", "vestige", "core").ok_or_else(|| { + StorageError::Init("Could not determine project directories".to_string()) + })?; + + let data_dir = proj_dirs.data_dir(); + std::fs::create_dir_all(data_dir)?; + // Restrict directory permissions to owner-only on Unix + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let perms = std::fs::Permissions::from_mode(0o700); + let _ = std::fs::set_permissions(data_dir, perms); + } + data_dir.join("vestige.db") + } }; // Open writer connection @@ -522,120 +175,63 @@ impl SqliteMemoryStore { let embedding_service = EmbeddingService::new(); #[cfg(feature = "vector-search")] - let vector_index = if Self::vector_search_enabled_by_cpu() { - let vector_index = VectorIndex::new() - .map_err(|e| StorageError::Init(format!("Failed to create vector index: {}", e)))?; - Some(Mutex::new(vector_index)) - } else { - tracing::warn!( - "Vector search disabled: {}", - Self::vector_search_unavailable_reason().unwrap_or("manual override"), - ); - None - }; + let vector_index = VectorIndex::new() + .map_err(|e| StorageError::Init(format!("Failed to create vector index: {}", e)))?; - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - let query_cache = if vector_index.is_some() { - Some(Mutex::new(LruCache::new( - NonZeroUsize::new(100).expect("100 is non-zero"), - ))) - } else { - None - }; + // Initialize LRU cache for query embeddings (capacity: 100 queries) + // SAFETY: 100 is always non-zero, this cannot fail + #[cfg(feature = "embeddings")] + let query_cache = Mutex::new(LruCache::new( + NonZeroUsize::new(100).expect("100 is non-zero"), + )); let storage = Self { - db_path: path, writer: Mutex::new(writer_conn), reader: Mutex::new(reader_conn), scheduler: Mutex::new(FSRSScheduler::default()), #[cfg(feature = "embeddings")] embedding_service, #[cfg(feature = "vector-search")] - vector_index, - #[cfg(all(feature = "embeddings", feature = "vector-search"))] + vector_index: Mutex::new(vector_index), + #[cfg(feature = "embeddings")] query_cache, - registered_model: std::sync::RwLock::new(None), }; #[cfg(all(feature = "embeddings", feature = "vector-search"))] - if storage.vector_index.is_some() { - storage.load_embeddings_into_index()?; - } + storage.load_embeddings_into_index()?; Ok(storage) } - /// Absolute path of the SQLite database this storage instance uses. - pub fn db_path(&self) -> &Path { - &self.db_path - } - - /// Data directory containing the SQLite database and sidecar folders. - pub fn data_dir(&self) -> &Path { - self.db_path.parent().unwrap_or_else(|| Path::new(".")) - } - - /// Sidecar directory for files belonging to this storage instance. - pub fn sidecar_dir(&self, name: &str) -> PathBuf { - self.data_dir().join(name) - } - /// Load existing embeddings into vector index #[cfg(all(feature = "embeddings", feature = "vector-search"))] fn load_embeddings_into_index(&self) -> Result<()> { - let Some(index) = self.vector_index.as_ref() else { - return Ok(()); - }; - - let mut index = index - .lock() - .map_err(|_| StorageError::Init("Vector index lock poisoned".to_string()))?; let reader = self .reader .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let mut stmt = reader.prepare("SELECT node_id, embedding, model FROM node_embeddings")?; + let mut stmt = reader.prepare("SELECT node_id, embedding FROM node_embeddings")?; - let embeddings: Vec<(String, Vec, String)> = stmt - .query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))? + let embeddings: Vec<(String, Vec)> = stmt + .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))? .filter_map(|r| r.ok()) .collect(); drop(stmt); drop(reader); - *index = VectorIndex::new().map_err(|e| { - StorageError::Init(format!("Failed to rebuild vector index before load: {}", e)) - })?; + let mut index = self + .vector_index + .lock() + .map_err(|_| StorageError::Init("Vector index lock poisoned".to_string()))?; let mut load_failures = 0u32; - let mut skipped_model_mismatches = 0u32; - let active_model = self.embedding_service.model_name(); - for (node_id, embedding_bytes, model_name) in embeddings { - if !Self::embedding_model_matches_active(&model_name, active_model) { - skipped_model_mismatches += 1; - continue; - } - + for (node_id, embedding_bytes) in embeddings { if let Some(embedding) = Embedding::from_bytes(&embedding_bytes) { - // Handle Matryoshka models explicitly. Do not silently truncate - // unknown embedding families into the active 256d index. + // Handle Matryoshka migration: old 768-dim → truncate to 256-dim let vector = if embedding.dimensions != EMBEDDING_DIMENSIONS { - let model_lower = model_name.to_ascii_lowercase(); - if model_lower.contains("nomic") || model_lower.contains("qwen3") { - matryoshka_truncate(embedding.vector) - } else { - load_failures += 1; - tracing::warn!( - node_id = %node_id, - model = %model_name, - dimensions = embedding.dimensions, - expected = EMBEDDING_DIMENSIONS, - "Skipping embedding with incompatible dimensions" - ); - continue; - } + matryoshka_truncate(embedding.vector) } else { embedding.vector }; @@ -652,13 +248,6 @@ impl SqliteMemoryStore { load_failures ); } - if skipped_model_mismatches > 0 { - tracing::info!( - count = skipped_model_mismatches, - active_model = active_model, - "Vector index skipped embeddings from a different model family; run consolidation to re-embed them" - ); - } Ok(()) } @@ -686,12 +275,6 @@ impl SqliteMemoryStore { let valid_from_str = input.valid_from.map(|dt| dt.to_rfc3339()); let valid_until_str = input.valid_until.map(|dt| dt.to_rfc3339()); - // #57 Source envelope — flatten to nullable column values. A node with - // no external provenance leaves all nine columns NULL (legacy shape). - let env = input.source_envelope.clone().unwrap_or_default(); - let env_source_updated_at = env.source_updated_at.map(|dt| dt.to_rfc3339()); - let env_synced_at = env.synced_at.map(|dt| dt.to_rfc3339()); - { let writer = self .writer @@ -703,19 +286,13 @@ impl SqliteMemoryStore { stability, difficulty, reps, lapses, learning_state, storage_strength, retrieval_strength, retention_strength, sentiment_score, sentiment_magnitude, next_review, scheduled_days, - source, tags, valid_from, valid_until, has_embedding, embedding_model, - domains, domain_scores, - source_system, source_id, source_url, source_updated_at, - content_hash, synced_at, source_project, source_type, source_author + source, tags, valid_from, valid_until, has_embedding, embedding_model ) VALUES ( ?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, ?26, ?27, ?28, - ?29, ?30, ?31, ?32, ?33 + ?19, ?20, ?21, ?22, ?23, ?24 )", params![ id, @@ -724,10 +301,7 @@ impl SqliteMemoryStore { now.to_rfc3339(), now.to_rfc3339(), now.to_rfc3339(), - // Clamp to MAX_STABILITY: the sentiment boost is otherwise - // persisted unbounded, letting an emotional memory's stability - // exceed the FSRS-6 ceiling every other write path respects. - (fsrs_state.stability * sentiment_boost).min(MAX_STABILITY), + fsrs_state.stability * sentiment_boost, fsrs_state.difficulty, fsrs_state.reps, fsrs_state.lapses, @@ -745,15 +319,6 @@ impl SqliteMemoryStore { valid_until_str, 0, Option::::None, - env.source_system, - env.source_id, - env.source_url, - env_source_updated_at, - env.content_hash, - env_synced_at, - env.source_project, - env.source_type, - env.source_author, ], )?; } @@ -778,37 +343,22 @@ impl SqliteMemoryStore { /// This solves the "bad vs good similar memory" problem. #[cfg(all(feature = "embeddings", feature = "vector-search"))] pub fn smart_ingest(&self, input: IngestInput) -> Result { - self.smart_ingest_excluding(input, &[]) - } - - /// Smart ingest with caller-provided candidate exclusions. - /// - /// Batch callers use this to keep two new items from the same caller-curated - /// batch from merging into each other while still allowing smart updates - /// against memories that existed before the batch began. - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - pub fn smart_ingest_excluding( - &self, - input: IngestInput, - excluded_node_ids: &[String], - ) -> Result { use crate::advanced::prediction_error::{ CandidateMemory, GateDecision, PredictionErrorGate, UpdateType, }; // Generate embedding for new content if !self.embedding_service.is_ready() { - return self.regular_ingest_result( - input, - "Embeddings not available, falling back to regular ingest", - ); - } - - if !self.vector_search_available() { - return self.regular_ingest_result( - input, - "Vector search unavailable, falling back to regular ingest", - ); + // Fall back to regular ingest if embeddings not available + let node = self.ingest(input)?; + return Ok(SmartIngestResult { + decision: "create".to_string(), + node, + superseded_id: None, + similarity: None, + prediction_error: Some(1.0), + reason: "Embeddings not available, falling back to regular ingest".to_string(), + }); } let new_embedding = self @@ -822,9 +372,6 @@ impl SqliteMemoryStore { // Build candidate memories let mut candidates: Vec = Vec::new(); for (node_id, _similarity) in similar.iter() { - if excluded_node_ids.iter().any(|id| id == node_id) { - continue; - } if let Some(node) = self.get_node(node_id)? { // Get embedding for this node if let Some(emb) = self.get_node_embedding(node_id)? { @@ -874,9 +421,6 @@ impl SqliteMemoryStore { reason, related_memory_ids ) }, - previous_content: None, - merged_from: None, - merge_preview: None, }) } GateDecision::Update { @@ -900,9 +444,6 @@ impl SqliteMemoryStore { prediction_error: Some(prediction_error), reason: "Content nearly identical - reinforced existing memory" .to_string(), - previous_content: None, - merged_from: None, - merge_preview: None, }) } UpdateType::Merge | UpdateType::Append => { @@ -910,11 +451,10 @@ impl SqliteMemoryStore { let existing = self .get_node(&target_id)? .ok_or_else(|| StorageError::NotFound(target_id.clone()))?; - let previous_content = existing.content.clone(); let merged_content = format!( "{}\n\n[Updated {}]\n{}", - previous_content, + existing.content, chrono::Utc::now().format("%Y-%m-%d"), input.content ); @@ -933,18 +473,10 @@ impl SqliteMemoryStore { similarity: Some(similarity), prediction_error: Some(prediction_error), reason: "Merged with existing similar memory".to_string(), - previous_content: Some(previous_content), - merged_from: Some(target_id), - merge_preview: Some(merged_content), }) } UpdateType::Replace => { // Replace content entirely - let existing = self - .get_node(&target_id)? - .ok_or_else(|| StorageError::NotFound(target_id.clone()))?; - let previous_content = existing.content; - self.update_node_content(&target_id, &input.content)?; let node = self .get_node(&target_id)? @@ -957,9 +489,6 @@ impl SqliteMemoryStore { similarity: Some(similarity), prediction_error: Some(prediction_error), reason: "Replaced existing memory with new content".to_string(), - previous_content: Some(previous_content), - merged_from: Some(target_id), - merge_preview: Some(input.content), }) } UpdateType::AddContext => { @@ -967,10 +496,9 @@ impl SqliteMemoryStore { let existing = self .get_node(&target_id)? .ok_or_else(|| StorageError::NotFound(target_id.clone()))?; - let previous_content = existing.content.clone(); let merged_content = - format!("{}\n\n---\nContext: {}", previous_content, input.content); + format!("{}\n\n---\nContext: {}", existing.content, input.content); self.update_node_content(&target_id, &merged_content)?; let node = self @@ -984,9 +512,6 @@ impl SqliteMemoryStore { similarity: Some(similarity), prediction_error: Some(prediction_error), reason: "Added new content as context to existing memory".to_string(), - previous_content: Some(previous_content), - merged_from: Some(target_id), - merge_preview: Some(merged_content), }) } } @@ -1010,9 +535,6 @@ impl SqliteMemoryStore { similarity: Some(similarity), prediction_error: Some(prediction_error), reason: format!("New memory supersedes old: {:?}", supersede_reason), - previous_content: None, - merged_from: None, - merge_preview: None, }) } GateDecision::Merge { @@ -1034,9 +556,6 @@ impl SqliteMemoryStore { memory_ids.len(), strategy ), - previous_content: None, - merged_from: None, - merge_preview: None, }) } } @@ -1050,19 +569,14 @@ impl SqliteMemoryStore { .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; let mut stmt = - reader.prepare("SELECT embedding, model FROM node_embeddings WHERE node_id = ?1")?; + reader.prepare("SELECT embedding FROM node_embeddings WHERE node_id = ?1")?; - let embedding_row: Option<(Vec, String)> = stmt - .query_row(params![node_id], |row| Ok((row.get(0)?, row.get(1)?))) + let embedding_bytes: Option> = stmt + .query_row(params![node_id], |row| row.get(0)) .optional()?; - Ok(embedding_row.and_then(|(bytes, model)| { - Self::embedding_vector_for_active_model( - &bytes, - &model, - self.embedding_service.model_name(), - ) - })) + Ok(embedding_bytes + .and_then(|bytes| crate::embeddings::Embedding::from_bytes(&bytes).map(|e| e.vector))) } /// Get all embedding vectors for duplicate detection @@ -1072,32 +586,23 @@ impl SqliteMemoryStore { .reader .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let mut stmt = reader.prepare("SELECT node_id, embedding, model FROM node_embeddings")?; - let active_model = self.embedding_service.model_name(); + let mut stmt = reader.prepare("SELECT node_id, embedding FROM node_embeddings")?; let results: Vec<(String, Vec)> = stmt .query_map([], |row| { let node_id: String = row.get(0)?; let embedding_bytes: Vec = row.get(1)?; - let model: String = row.get(2)?; - Ok((node_id, embedding_bytes, model)) + Ok((node_id, embedding_bytes)) })? .filter_map(|r| r.ok()) - .filter_map(|(id, bytes, model)| { - Self::embedding_vector_for_active_model(&bytes, &model, active_model) - .map(|vector| (id, vector)) + .filter_map(|(id, bytes)| { + crate::embeddings::Embedding::from_bytes(&bytes).map(|e| (id, e.vector)) }) .collect(); Ok(results) } - /// Fallback for builds without local embeddings/vector search. - #[cfg(not(all(feature = "embeddings", feature = "vector-search")))] - pub fn get_node_embedding(&self, _node_id: &str) -> Result>> { - Ok(None) - } - /// Update the content of an existing node pub fn update_node_content(&self, id: &str, new_content: &str) -> Result<()> { let now = Utc::now(); @@ -1117,28 +622,12 @@ impl SqliteMemoryStore { #[cfg(all(feature = "embeddings", feature = "vector-search"))] { // Remove old embedding from index - if let Some(index) = self.vector_index.as_ref() - && let Ok(mut index) = index.lock() - { + if let Ok(mut index) = self.vector_index.lock() { let _ = index.remove(id); } - // Generate new embedding. If the embedder isn't ready yet (e.g. the - // model is still downloading on first run), generate_embedding_for_node - // is a no-op — which previously left the OLD, now-stale embedding row - // with has_embedding = 1, so semantic search kept matching the old - // content and the consolidation regeneration query (which only selects - // has_embedding = 0 / missing rows / model mismatch) never refreshed - // it. Flip has_embedding to 0 on the not-ready path so the stale vector - // is picked up and rebuilt once the embedder comes online. - if self.embedding_service.is_ready() { - if let Err(e) = self.generate_embedding_for_node(id, new_content) { - tracing::warn!("Failed to regenerate embedding for {}: {}", id, e); - } - } else if let Ok(writer) = self.writer.lock() { - let _ = writer.execute( - "UPDATE knowledge_nodes SET has_embedding = 0 WHERE id = ?1", - params![id], - ); + // Generate new embedding + if let Err(e) = self.generate_embedding_for_node(id, new_content) { + tracing::warn!("Failed to regenerate embedding for {}: {}", id, e); } } @@ -1156,7 +645,6 @@ impl SqliteMemoryStore { .embedding_service .embed(content) .map_err(|e| StorageError::Init(format!("Embedding failed: {}", e)))?; - let model_name = self.embedding_service.model_name(); let now = Utc::now(); @@ -1171,26 +659,25 @@ impl SqliteMemoryStore { params![ node_id, embedding.to_bytes(), - embedding.dimensions as i32, - model_name, + EMBEDDING_DIMENSIONS as i32, + "nomic-embed-text-v1.5", now.to_rfc3339(), ], )?; writer.execute( - "UPDATE knowledge_nodes SET has_embedding = 1, embedding_model = ?2 WHERE id = ?1", - params![node_id, model_name], + "UPDATE knowledge_nodes SET has_embedding = 1, embedding_model = 'nomic-embed-text-v1.5' WHERE id = ?1", + params![node_id], )?; } - if let Some(index) = self.vector_index.as_ref() { - let mut index = index - .lock() - .map_err(|_| StorageError::Init("Vector index lock poisoned".to_string()))?; - index - .add(node_id, &embedding.vector) - .map_err(|e| StorageError::Init(format!("Vector index add failed: {}", e)))?; - } + let mut index = self + .vector_index + .lock() + .map_err(|_| StorageError::Init("Vector index lock poisoned".to_string()))?; + index + .add(node_id, &embedding.vector) + .map_err(|e| StorageError::Init(format!("Vector index add failed: {}", e)))?; Ok(()) } @@ -1207,41 +694,20 @@ impl SqliteMemoryStore { Ok(node) } - /// Parse a stored timestamp into a UTC `DateTime`. - /// - /// The canonical on-disk format is RFC 3339 (every Rust writer in this - /// crate uses `DateTime::to_rfc3339()`). However, timestamps can also be - /// written by external tooling that bypasses this storage layer — most - /// notably session hooks or manual maintenance that touch the DB with raw - /// `sqlite3`. SQLite's native `datetime('now')` / `CURRENT_TIMESTAMP` - /// emit a space-separated, timezone-less `YYYY-MM-DD HH:MM:SS[.fff]` - /// string that `parse_from_rfc3339` rejects, which would otherwise make - /// every affected row unreadable. - /// - /// We therefore parse RFC 3339 first and fall back to the SQLite-native - /// format (assumed UTC) so the store stays tolerant of either writer. + /// Parse RFC3339 timestamp fn parse_timestamp(value: &str, field_name: &str) -> rusqlite::Result> { - if let Ok(dt) = DateTime::parse_from_rfc3339(value) { - return Ok(dt.with_timezone(&Utc)); - } - - // Fallback: SQLite-native "YYYY-MM-DD HH:MM:SS" (with optional - // fractional seconds), which has no timezone and is assumed UTC. - if let Ok(naive) = NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S%.f") { - return Ok(naive.and_utc()); - } - - Err(rusqlite::Error::FromSqlConversionFailure( - 0, - rusqlite::types::Type::Text, - Box::new(std::io::Error::new( - std::io::ErrorKind::InvalidData, - format!( - "Invalid {} timestamp '{}': not RFC 3339 or SQLite datetime format", - field_name, value - ), - )), - )) + DateTime::parse_from_rfc3339(value) + .map(|dt| dt.with_timezone(&Utc)) + .map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + 0, + rusqlite::types::Type::Text, + Box::new(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("Invalid {} timestamp '{}': {}", field_name, value, e), + )), + ) + }) } /// Convert a row to KnowledgeNode @@ -1297,33 +763,6 @@ impl SqliteMemoryStore { .ok() }); - // #57 Source envelope columns (Migration V17). `.ok().flatten()` is - // tolerant of pre-V17 databases that lack these columns. Collapse an - // all-NULL envelope to `None` so legacy nodes serialize unchanged. - let parse_ts = |s: Option| -> Option> { - s.and_then(|s| { - DateTime::parse_from_rfc3339(&s) - .map(|dt| dt.with_timezone(&Utc)) - .ok() - }) - }; - let envelope = crate::memory::SourceEnvelope { - source_system: row.get("source_system").ok().flatten(), - source_id: row.get("source_id").ok().flatten(), - source_url: row.get("source_url").ok().flatten(), - source_updated_at: parse_ts(row.get("source_updated_at").ok().flatten()), - content_hash: row.get("content_hash").ok().flatten(), - synced_at: parse_ts(row.get("synced_at").ok().flatten()), - source_project: row.get("source_project").ok().flatten(), - source_type: row.get("source_type").ok().flatten(), - source_author: row.get("source_author").ok().flatten(), - }; - let source_envelope = if envelope.is_empty() { - None - } else { - Some(envelope) - }; - Ok(KnowledgeNode { id: row.get("id")?, content: row.get("content")?, @@ -1360,8 +799,6 @@ impl SqliteMemoryStore { // v2.0.5 Active Forgetting suppression_count, suppressed_at, - // #57 Source envelope - source_envelope, }) } @@ -1373,12 +810,8 @@ impl SqliteMemoryStore { } #[cfg(all(feature = "embeddings", feature = "vector-search"))] SearchMode::Semantic => { - if !self.vector_search_available() { - self.keyword_search(&input.query, input.limit, input.min_retention)? - } else { - let results = self.semantic_search(&input.query, input.limit, 0.3)?; - results.into_iter().map(|r| r.node).collect() - } + let results = self.semantic_search(&input.query, input.limit, 0.3)?; + results.into_iter().map(|r| r.node).collect() } #[cfg(all(feature = "embeddings", feature = "vector-search"))] SearchMode::Hybrid => { @@ -1557,10 +990,9 @@ impl SqliteMemoryStore { // Content-aware cross-memory reinforcement: boost semantically similar neighbors #[cfg(all(feature = "embeddings", feature = "vector-search"))] { - if let Some(index) = self.vector_index.as_ref() - && let Ok(Some(embedding)) = self.get_node_embedding(id) - { - let index = index + if let Ok(Some(embedding)) = self.get_node_embedding(id) { + let index = self + .vector_index .lock() .map_err(|_| StorageError::Init("Vector index lock poisoned".to_string()))?; @@ -1674,42 +1106,6 @@ impl SqliteMemoryStore { .ok_or_else(|| StorageError::NotFound(id.to_string())) } - /// Backfill-specific promote: identical retrieval/retention boost to - /// `promote_memory`, but the stability multiply is CAPPED at an additive - /// +365-day ceiling: `MIN(stability * 1.5, stability + 365.0)`. The `1.5` - /// factor preserves the multiplier `promote_memory` already applied; the - /// `+365` ceiling is the same additive bound `retroactive_backfill.rs` - /// uses for its reason string (that module pairs +365 with a 2.5 factor - /// for display only — this DB write intentionally keeps 1.5 so backfill - /// promotion strength is unchanged, just bounded). Repeated per-(cause, - /// failure) backfill promotions therefore cannot inflate stability without - /// bound. Used by the step-8.5 auto-fire path and the manual `backfill` tool. - pub fn promote_memory_backfill(&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, - retrieval_strength = MIN(1.0, retrieval_strength + 0.20), - retention_strength = MIN(1.0, retention_strength + 0.10), - stability = MIN(stability * 1.5, stability + 365.0) - WHERE id = ?2", - params![now.to_rfc3339(), id], - )?; - } - - let _ = self.log_access(id, "promote"); - let _ = self.set_waking_tag(id); - - self.get_node(id)? - .ok_or_else(|| StorageError::NotFound(id.to_string())) - } - /// Demote a memory (thumbs down) - used when a memory led to a bad outcome /// Significantly reduces retrieval strength so better alternatives surface /// Does NOT delete - the memory stays for reference but ranks lower @@ -1818,15 +1214,6 @@ impl SqliteMemoryStore { .writer .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; - // True inverse of suppress_memory (which applies stability * 0.4, - // retrieval - 0.35, retention - 0.20). Dividing by 0.4 exactly undoes - // the * 0.4, and adding back the same 0.35 / 0.20 deltas (clamped to - // 1.0) undoes the subtraction. Previously this used non-inverse deltas - // (* 1.25, + 0.15, + 0.10), so suppress-then-reverse left stability - // permanently halved (0.4 * 1.25 = 0.5) while reporting a full undo. - // Note: where the forward pass hit the MAX(0.05) floor, the exact - // pre-value is unrecoverable without a snapshot — that clip aside, - // this restores the pre-suppression FSRS state. writer.execute( "UPDATE knowledge_nodes SET suppression_count = MAX(0, COALESCE(suppression_count, 0) - 1), @@ -1834,9 +1221,9 @@ impl SqliteMemoryStore { WHEN COALESCE(suppression_count, 0) - 1 <= 0 THEN NULL ELSE suppressed_at END, - retrieval_strength = MIN(1.0, retrieval_strength + 0.35), - retention_strength = MIN(1.0, retention_strength + 0.20), - stability = stability / 0.4 + 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], )?; @@ -1848,75 +1235,6 @@ impl SqliteMemoryStore { .ok_or_else(|| StorageError::NotFound(id.to_string())) } - /// Release a memory from quarantine **unconditionally** (no labile-window - /// limit), used when a Memory PR is approved. - /// - /// Unlike [`Self::reverse_suppression`] (which models a time-bounded "undo" - /// of an active-forgetting decision and refuses after the labile window), - /// approving a quarantined risky write is an explicit reviewer decision that - /// must always restore the memory's retrieval influence — even days later. - /// Fully clears the suppression (count → 0, `suppressed_at` → NULL) and - /// restores strengths. A no-op (returns the node) if it isn't suppressed. - pub fn release_quarantine(&self, id: &str) -> Result { - let node = self - .get_node(id)? - .ok_or_else(|| StorageError::NotFound(id.to_string()))?; - - if node.suppression_count == 0 && node.suppressed_at.is_none() { - // Nothing to release — idempotent. - return Ok(node); - } - - { - let writer = self - .writer - .lock() - .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; - writer.execute( - "UPDATE knowledge_nodes SET - suppression_count = 0, - suppressed_at = NULL, - 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, "release_quarantine"); - - self.get_node(id)? - .ok_or_else(|| StorageError::NotFound(id.to_string())) - } - - /// Test-only: backdate a node's `suppressed_at` to simulate a suppression - /// that happened long ago (e.g. to verify release works past the labile - /// window). `pub(crate)` so sibling test modules can reach it. - #[cfg(test)] - pub(crate) fn set_suppressed_at_for_test(&self, id: &str, when: DateTime) { - if let Ok(writer) = self.writer.lock() { - let _ = writer.execute( - "UPDATE knowledge_nodes SET suppressed_at = ?1 WHERE id = ?2", - params![when.to_rfc3339(), id], - ); - } - } - - /// Backdate a node's `created_at`. Intended for tests and demo seeding (e.g. - /// to simulate a memory formed days ago so Retroactive Salience Backfill can - /// reach back to it). Cross-crate `pub` so the MCP backfill test + demo - /// harness can plant a dated cause. Returns Ok(()) on success. - pub fn set_created_at(&self, id: &str, when: DateTime) -> Result<()> { - if let Ok(writer) = self.writer.lock() { - writer.execute( - "UPDATE knowledge_nodes SET created_at = ?1 WHERE id = ?2", - params![when.to_rfc3339(), id], - )?; - } - Ok(()) - } - /// Count memories currently in a suppressed state (suppression_count > 0). pub fn count_suppressed(&self) -> Result { let reader = self @@ -2131,56 +1449,11 @@ impl SqliteMemoryStore { |row| row.get(0), )?; - let embedding_model: Option = reader - .query_row( - "SELECT model - FROM node_embeddings - GROUP BY model - ORDER BY COUNT(*) DESC, model ASC - LIMIT 1", - [], - |row| row.get(0), - ) - .optional()?; - - #[cfg(feature = "embeddings")] - let active_embedding_model = Some(self.embedding_service.model_name().to_string()); - #[cfg(not(feature = "embeddings"))] - let active_embedding_model = None; - - #[cfg(feature = "embeddings")] - let (nodes_with_active_embeddings, nodes_with_mismatched_embeddings) = { - let active_model = active_embedding_model.as_deref().unwrap_or_default(); - let model_pattern = Self::active_embedding_model_like_pattern(active_model); - let active_count: i64 = reader.query_row( - "SELECT COUNT(*) - FROM knowledge_nodes kn - WHERE kn.has_embedding = 1 - AND EXISTS ( - SELECT 1 FROM node_embeddings ne - WHERE ne.node_id = kn.id - AND ne.model LIKE ?1 - )", - params![&model_pattern], - |row| row.get(0), - )?; - let mismatched_count: i64 = reader.query_row( - "SELECT COUNT(*) - FROM knowledge_nodes kn - WHERE kn.has_embedding = 1 - AND NOT EXISTS ( - SELECT 1 FROM node_embeddings ne - WHERE ne.node_id = kn.id - AND ne.model LIKE ?1 - )", - params![&model_pattern], - |row| row.get(0), - )?; - (active_count, mismatched_count) + let embedding_model: Option = if nodes_with_embeddings > 0 { + Some("nomic-embed-text-v1.5".to_string()) + } else { + None }; - #[cfg(not(feature = "embeddings"))] - let (nodes_with_active_embeddings, nodes_with_mismatched_embeddings) = - (nodes_with_embeddings, 0); Ok(MemoryStats { total_nodes: total, @@ -2199,125 +1472,22 @@ impl SqliteMemoryStore { .ok() }), nodes_with_embeddings, - nodes_with_active_embeddings, - nodes_with_mismatched_embeddings, embedding_model, - active_embedding_model, - }) - } - - /// Introspect the live SQLite schema: schema version + per-table row/column - /// shape + embedding-coverage convenience fields. - /// - /// This is the v2.1.24+ replacement for the direct-SQLite reads that - /// audit scripts and migration guards previously had to perform. The set - /// of tables walked matches `PORTABLE_USER_DATA_TABLES` — the same - /// canonical set used by portable export / import — so the surface stays - /// stable across migrations rather than chasing arbitrary - /// `sqlite_master` rows. - /// - /// Cost: O(N_tables) `COUNT(*)` queries + one PRAGMA per table. Negligible - /// at the table cardinalities Vestige carries (~15 tables, all indexed). - /// Safe to call on every MCP `system_status` invocation when the flag is - /// set; callers wanting to limit cost should leave the flag off (default). - pub fn schema_introspection(&self) -> Result { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - - let schema_version = Self::current_schema_version(&reader)?; - - // schema_version has the row (version PK + applied_at TEXT). Read the - // applied_at for the current version row; tolerate failure (legacy - // databases may have skipped the applied_at fill on early upgrades). - let applied_at_str: Option = reader - .query_row( - "SELECT applied_at FROM schema_version WHERE version = ?1", - params![schema_version as i64], - |row| row.get(0), - ) - .optional()?; - let schema_version_applied_at = applied_at_str.and_then(|s| { - // The migration scripts use `datetime('now')` which yields - // SQLite's "YYYY-MM-DD HH:MM:SS" UTC form (NOT RFC3339). - // Try the SQLite form first, fall back to RFC3339 for any - // future migrations that switch. - chrono::NaiveDateTime::parse_from_str(&s, "%Y-%m-%d %H:%M:%S") - .map(|naive| naive.and_utc()) - .or_else(|_| DateTime::parse_from_rfc3339(&s).map(|dt| dt.with_timezone(&Utc))) - .ok() - }); - - let mut tables = Vec::with_capacity(PORTABLE_USER_DATA_TABLES.len()); - for table_name in PORTABLE_USER_DATA_TABLES { - if Self::table_exists(&reader, table_name)? { - let rows = Self::table_row_count(&reader, table_name)?; - let columns = Self::table_columns(&reader, table_name)?; - tables.push(crate::TableIntrospection { - name: (*table_name).to_string(), - rows, - columns, - }); - } - } - - // Convenience: embedding-coverage NULL count. Defined as the number - // of knowledge_nodes with NO matching row in node_embeddings. This is - // distinct from `nodes_with_embeddings` in MemoryStats (which uses - // the `has_embedding` column flag); we compute the join-based truth - // here so audit scripts can detect drift between the flag and the - // actual embeddings table. - let embedding_null_count: i64 = reader - .query_row( - "SELECT COUNT(*) FROM knowledge_nodes kn - WHERE NOT EXISTS ( - SELECT 1 FROM node_embeddings ne WHERE ne.node_id = kn.id - )", - [], - |row| row.get(0), - ) - .unwrap_or(0); - - #[cfg(feature = "embeddings")] - let active_embedding_model = Some(self.embedding_service.model_name().to_string()); - #[cfg(not(feature = "embeddings"))] - let active_embedding_model: Option = None; - - #[cfg(feature = "embeddings")] - let active_embedding_dimensions: Option = - Some(self.embedding_service.dimensions() as u32); - #[cfg(not(feature = "embeddings"))] - let active_embedding_dimensions: Option = None; - - Ok(crate::SchemaIntrospection { - schema_version, - schema_version_applied_at, - tables, - embedding_null_count, - active_embedding_model, - active_embedding_dimensions, }) } /// Delete a node pub fn delete_node(&self, id: &str) -> Result { - let mut writer = self + let writer = self .writer .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; - let tx = writer.transaction()?; - if Self::node_exists(&tx, id)? { - Self::record_sync_tombstone(&tx, "knowledge_nodes", id, "delete_node")?; - } - let rows = tx.execute("DELETE FROM knowledge_nodes WHERE id = ?1", params![id])?; - tx.commit()?; + 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 Some(index) = self.vector_index.as_ref() - && let Ok(mut index) = index.lock() + && let Ok(mut index) = self.vector_index.lock() { let _ = index.remove(id); } @@ -2325,170 +1495,9 @@ impl SqliteMemoryStore { Ok(rows > 0) } - /// Permanently purge a memory's content and embeddings. - /// - /// Unlike `delete_node`, purge also scrubs non-FK JSON references in - /// `insights.source_memories`, detaches temporal-summary children, and - /// writes a content-free deletion tombstone for audit/sync. - pub fn purge_node(&self, id: &str, reason: Option<&str>) -> Result { - let deleted_at = Utc::now(); - let mut writer = self - .writer - .lock() - .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; - let tx = writer.transaction()?; - - let node = tx - .prepare("SELECT * FROM knowledge_nodes WHERE id = ?1")? - .query_row(params![id], Self::row_to_node) - .optional()?; - - let Some(node) = node else { - return Ok(PurgeReport { - memory_id: id.to_string(), - deleted: false, - deleted_at, - edges_pruned: 0, - insights_rewritten: 0, - insights_deleted: 0, - children_orphaned: 0, - }); - }; - - let edges_pruned: i64 = tx.query_row( - "SELECT COUNT(*) FROM memory_connections WHERE source_id = ?1 OR target_id = ?1", - params![id], - |row| row.get(0), - )?; - - let insight_refs: Vec<(String, String)> = { - let mut stmt = tx.prepare( - "SELECT id, source_memories FROM insights WHERE source_memories LIKE ?1", - )?; - let pattern = format!("%{}%", id); - stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))? - .filter_map(|row| row.ok()) - .collect() - }; - - let mut insights_rewritten = 0_i64; - let mut insights_deleted = 0_i64; - for (insight_id, source_json) in insight_refs { - let mut sources: Vec = serde_json::from_str(&source_json).unwrap_or_default(); - let before = sources.len(); - sources.retain(|source_id| source_id != id); - - if sources.len() == before { - continue; - } - - if sources.len() < 2 { - insights_deleted += - tx.execute("DELETE FROM insights WHERE id = ?1", params![insight_id])? as i64; - } else { - let rewritten = serde_json::to_string(&sources).unwrap_or_else(|_| "[]".into()); - insights_rewritten += tx.execute( - "UPDATE insights SET source_memories = ?1 WHERE id = ?2", - params![rewritten, insight_id], - )? as i64; - } - } - - let children_orphaned = tx.execute( - "UPDATE knowledge_nodes SET summary_parent_id = NULL WHERE summary_parent_id = ?1", - params![id], - )? as i64; - - tx.execute( - "UPDATE composition_members SET preview = NULL WHERE memory_id = ?1", - params![id], - )?; - - let tags_json = serde_json::to_string(&node.tags).unwrap_or_else(|_| "[]".to_string()); - tx.execute( - "INSERT INTO deletion_tombstones ( - memory_id, deleted_at, reason, node_type, tags, - edges_pruned, insights_rewritten, insights_deleted, children_orphaned - ) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9) - ON CONFLICT(memory_id) DO UPDATE SET - deleted_at = excluded.deleted_at, - reason = excluded.reason, - node_type = excluded.node_type, - tags = excluded.tags, - edges_pruned = excluded.edges_pruned, - insights_rewritten = excluded.insights_rewritten, - insights_deleted = excluded.insights_deleted, - children_orphaned = excluded.children_orphaned", - params![ - id, - deleted_at.to_rfc3339(), - reason, - node.node_type, - tags_json, - edges_pruned, - insights_rewritten, - insights_deleted, - children_orphaned, - ], - )?; - - Self::record_sync_tombstone(&tx, "knowledge_nodes", id, "purge_node")?; - tx.execute("DELETE FROM knowledge_nodes WHERE id = ?1", params![id])?; - tx.commit()?; - - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - if let Some(index) = self.vector_index.as_ref() - && let Ok(mut index) = index.lock() - { - let _ = index.remove(id); - } - - Ok(PurgeReport { - memory_id: id.to_string(), - deleted: true, - deleted_at, - edges_pruned, - insights_rewritten, - insights_deleted, - children_orphaned, - }) - } - - fn node_exists(conn: &Connection, id: &str) -> Result { - let count: i64 = conn.query_row( - "SELECT COUNT(*) FROM knowledge_nodes WHERE id = ?1", - params![id], - |row| row.get(0), - )?; - Ok(count > 0) - } - - fn record_sync_tombstone( - conn: &Connection, - table_name: &str, - row_id: &str, - reason: &str, - ) -> Result<()> { - conn.execute( - "INSERT INTO sync_tombstones (table_name, row_id, deleted_at, reason) - VALUES (?1, ?2, ?3, ?4) - ON CONFLICT(table_name, row_id) DO UPDATE SET - deleted_at = excluded.deleted_at, - reason = excluded.reason", - params![table_name, row_id, Utc::now().to_rfc3339(), reason], - )?; - Ok(()) - } - /// Search with full-text search pub fn search(&self, query: &str, limit: i32) -> Result> { - // OR-of-tokens + BM25 rank: matches rows sharing ANY distinctive token, - // ranked by lexical relevance. (The old whole-string phrase match required - // all tokens adjacent and in order, so multi-word queries returned nothing.) - let Some(sanitized_query) = sanitize_fts5_or_query(query) else { - return Ok(Vec::new()); - }; + let sanitized_query = sanitize_fts5_query(query); let reader = self .reader @@ -2543,202 +1552,6 @@ impl SqliteMemoryStore { Ok(result) } - /// Concrete keyword/literal search that skips semantic expansion and - /// cognitive reranking. - /// - /// This path is for identifiers, paths, quoted strings, env vars, UUIDs, - /// and other exact user intent where "close enough" is wrong. - pub fn concrete_search_filtered( - &self, - query: &str, - limit: i32, - include_types: Option<&[String]>, - exclude_types: Option<&[String]>, - ) -> Result> { - let literal = Self::normalize_literal_query(query); - if literal.is_empty() { - return Ok(vec![]); - } - - let limit = limit.max(1) as usize; - let fetch_limit = ((limit * 10).min(500)) as i32; - let mut by_id: HashMap = HashMap::new(); - - if let Some(terms) = crate::fts::sanitize_fts5_terms(&literal) { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let mut stmt = reader.prepare( - "SELECT n.*, rank AS fts_rank FROM knowledge_nodes n - JOIN knowledge_fts fts ON n.id = fts.id - WHERE knowledge_fts MATCH ?1 - ORDER BY rank - LIMIT ?2", - )?; - - let rows = stmt.query_map(params![terms, fetch_limit], |row| { - let node = Self::row_to_node(row)?; - let rank = row.get::<_, f64>("fts_rank").unwrap_or(0.0); - Ok((node, rank)) - })?; - - for (idx, row) in rows.enumerate() { - let (node, rank) = row?; - if !Self::node_matches_type_filters(&node, include_types, exclude_types) { - continue; - } - let base_score = (1.0 / (idx as f32 + 1.0)).max((-rank as f32).max(0.0)); - Self::upsert_concrete_result(&mut by_id, node, base_score, Some(base_score)); - } - } - - let escaped = Self::escape_like(&literal.to_lowercase()); - let pattern = format!("%{}%", escaped); - let prefix_pattern = format!("{}%", escaped); - { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let mut stmt = reader.prepare( - "SELECT n.* FROM knowledge_nodes n - WHERE lower(n.id) = ?2 - OR lower(n.content) LIKE ?1 ESCAPE '\\' - OR lower(COALESCE(n.source, '')) LIKE ?1 ESCAPE '\\' - OR lower(n.tags) LIKE ?1 ESCAPE '\\' - ORDER BY - CASE - WHEN lower(n.id) = ?2 THEN 0 - WHEN lower(n.content) = ?2 THEN 1 - WHEN lower(n.content) LIKE ?3 ESCAPE '\\' THEN 2 - ELSE 3 - END, - n.updated_at DESC - LIMIT ?4", - )?; - - let rows = stmt.query_map( - params![pattern, literal.to_lowercase(), prefix_pattern, fetch_limit], - Self::row_to_node, - )?; - - for row in rows { - let node = row?; - if !Self::node_matches_type_filters(&node, include_types, exclude_types) { - continue; - } - if let Some(score) = Self::literal_match_score(&literal, &node) { - Self::upsert_concrete_result(&mut by_id, node, score, Some(score)); - } - } - } - - let mut results: Vec = by_id.into_values().collect(); - results.sort_by(|a, b| { - b.combined_score - .partial_cmp(&a.combined_score) - .unwrap_or(std::cmp::Ordering::Equal) - .then_with(|| b.node.updated_at.cmp(&a.node.updated_at)) - }); - results.truncate(limit); - Ok(results) - } - - fn upsert_concrete_result( - by_id: &mut HashMap, - node: KnowledgeNode, - score: f32, - keyword_score: Option, - ) { - by_id - .entry(node.id.clone()) - .and_modify(|existing| { - existing.combined_score = existing.combined_score.max(score); - existing.keyword_score = match (existing.keyword_score, keyword_score) { - (Some(a), Some(b)) => Some(a.max(b)), - (None, Some(b)) => Some(b), - (a, None) => a, - }; - }) - .or_insert(SearchResult { - node, - keyword_score, - semantic_score: None, - combined_score: score, - match_type: MatchType::Keyword, - }); - } - - fn normalize_literal_query(query: &str) -> String { - let trimmed = query.trim(); - if trimmed.len() >= 2 { - let bytes = trimmed.as_bytes(); - let quoted = (bytes[0] == b'"' && bytes[bytes.len() - 1] == b'"') - || (bytes[0] == b'\'' && bytes[bytes.len() - 1] == b'\''); - if quoted { - return trimmed[1..trimmed.len() - 1].trim().to_string(); - } - } - trimmed.to_string() - } - - fn escape_like(value: &str) -> String { - let mut escaped = String::with_capacity(value.len()); - for ch in value.chars() { - match ch { - '\\' | '%' | '_' => { - escaped.push('\\'); - escaped.push(ch); - } - _ => escaped.push(ch), - } - } - escaped - } - - fn literal_match_score(query: &str, node: &KnowledgeNode) -> Option { - let q = query.to_lowercase(); - let content = node.content.to_lowercase(); - let tags = node.tags.join(" ").to_lowercase(); - let source = node.source.as_deref().unwrap_or("").to_lowercase(); - let id = node.id.to_lowercase(); - - if id == q { - Some(3.0) - } else if content == q { - Some(2.5) - } else if content.starts_with(&q) { - Some(2.0) - } else if content.contains(&q) { - Some(1.6) - } else if source.contains(&q) { - Some(1.4) - } else if tags.contains(&q) { - Some(1.2) - } else { - None - } - } - - fn node_matches_type_filters( - node: &KnowledgeNode, - include_types: Option<&[String]>, - exclude_types: Option<&[String]>, - ) -> bool { - if let Some(includes) = include_types - && !includes.is_empty() - { - return includes.iter().any(|t| t == &node.node_type); - } - if let Some(excludes) = exclude_types - && !excludes.is_empty() - { - return !excludes.iter().any(|t| t == &node.node_type); - } - true - } - /// Get all nodes (paginated) pub fn get_all_nodes(&self, limit: i32, offset: i32) -> Result> { let reader = self @@ -2839,18 +1652,15 @@ impl SqliteMemoryStore { } /// Get query embedding from cache or compute it - #[cfg(all(feature = "embeddings", feature = "vector-search"))] + #[cfg(feature = "embeddings")] fn get_query_embedding(&self, query: &str) -> Result> { - let cache_key = format!("{}\0{}", self.embedding_service.model_name(), query); // Check cache first - let Some(index_cache) = self.query_cache.as_ref() else { - return Err(StorageError::Init("Query cache unavailable".to_string())); - }; { - let mut cache = index_cache + let mut cache = self + .query_cache .lock() .map_err(|_| StorageError::Init("Query cache lock poisoned".to_string()))?; - if let Some(cached) = cache.get(&cache_key) { + if let Some(cached) = cache.get(query) { return Ok(cached.clone()); } } @@ -2858,15 +1668,16 @@ impl SqliteMemoryStore { // Not in cache, compute embedding let embedding = self .embedding_service - .embed_query(query) + .embed(query) .map_err(|e| StorageError::Init(format!("Failed to embed query: {}", e)))?; // Store in cache { - let mut cache = index_cache + let mut cache = self + .query_cache .lock() .map_err(|_| StorageError::Init("Query cache lock poisoned".to_string()))?; - cache.put(cache_key, embedding.vector.clone()); + cache.put(query.to_string(), embedding.vector.clone()); } Ok(embedding.vector) @@ -2880,19 +1691,14 @@ impl SqliteMemoryStore { limit: i32, min_similarity: f32, ) -> Result> { - let Some(index_lock) = self.vector_index.as_ref() else { - return Err(StorageError::Init( - "Vector search unavailable: disabled for this machine".to_string(), - )); - }; - if !self.embedding_service.is_ready() { return Err(StorageError::Init("Embedding model not ready".to_string())); } let query_embedding = self.get_query_embedding(query)?; - let index = index_lock + let index = self + .vector_index .lock() .map_err(|_| StorageError::Init("Vector index lock poisoned".to_string()))?; @@ -2952,22 +1758,19 @@ impl SqliteMemoryStore { exclude_types, )?; - let semantic_results = - if self.vector_search_available() && self.embedding_service.is_ready() { - self.semantic_search_raw(query, limit * overfetch_factor)? - } else { - vec![] - }; + let semantic_results = if self.embedding_service.is_ready() { + self.semantic_search_raw(query, limit * overfetch_factor)? + } else { + vec![] + }; - // Reciprocal Rank Fusion (k=60) when both lists are present: it is scale-free - // and rewards a memory that appears in BOTH the keyword and semantic lists — - // exactly the structurally-similar-different-words paraphrase that linear - // max-norm fusion buried. Falls back to linear when only one list exists. - // (keyword_weight/semantic_weight retained in the signature for compatibility; - // RRF is rank-based so the weights no longer scale the fused score.) - let _ = (keyword_weight, semantic_weight); let combined = if !semantic_results.is_empty() { - reciprocal_rank_fusion(&keyword_results, &semantic_results, 60.0) + linear_combination( + &keyword_results, + &semantic_results, + keyword_weight, + semantic_weight, + ) } else { keyword_results.clone() }; @@ -3006,18 +1809,18 @@ impl SqliteMemoryStore { (false, false) => MatchType::Keyword, }; - // Carry the RRF fused score as the relevance signal, NOT a linear - // kw*w + sem*w recomputation. RRF is what selected these candidates - // and rewards both-list agreement; overwriting it with the linear - // weighted_score made the final ranking diverge from RRF order - // (a both-list paraphrase could rank below a keyword-only hit). - // The min-max normalization in the rerank below then operates on - // RRF scores, so final relevance ordering matches RRF ordering. + let weighted_score = match (keyword_score, semantic_score) { + (Some(kw), Some(sem)) => kw * keyword_weight + sem * semantic_weight, + (Some(kw), None) => kw * keyword_weight, + (None, Some(sem)) => sem * semantic_weight, + (None, None) => combined_score, + }; + results.push(SearchResult { node, keyword_score, semantic_score, - combined_score, + combined_score: weighted_score, match_type, }); } @@ -3025,20 +1828,6 @@ impl SqliteMemoryStore { // Three-signal reranking (Park et al. Generative Agents 2023) // final_score = 0.2*recency + 0.3*importance + 0.5*relevance - // - // relevance MUST live in [0,1] for the weights to balance. The raw - // weighted_score does not: keyword-only results max out at - // `1.0 * keyword_weight` (0.3 by default), so the strongest match's - // relevance term was capped at 0.5*0.3 = 0.15 and lost to recency (up to - // 0.2) or importance (up to 0.3) — a fresh, weakly-relevant node could - // outrank the best match. Min-max normalize relevance across the result - // set so the best match scores ~1.0 regardless of the weight scaling. - let (min_rel, max_rel) = results.iter().fold( - (f32::INFINITY, f32::NEG_INFINITY), - |(mn, mx), r| (mn.min(r.combined_score), mx.max(r.combined_score)), - ); - let rel_span = (max_rel - min_rel) as f64; - let now = Utc::now(); for result in &mut results { let hours_since = (now - result.node.last_accessed).num_seconds() as f64 / 3600.0; @@ -3060,13 +1849,7 @@ impl SqliteMemoryStore { // Normalize ACT-R activation [-2, 5] → [0, 1] let importance = ((activation + 2.0) / 7.0).clamp(0.0, 1.0); - // Min-max normalized relevance in [0,1]. When every result ties - // (span 0), fall back to 1.0 so relevance still dominates ranking. - let relevance = if rel_span > f64::EPSILON { - (result.combined_score - min_rel) as f64 / rel_span - } else { - 1.0 - }; + let relevance = result.combined_score as f64; let final_score = 0.2 * recency + 0.3 * importance + 0.5 * relevance; result.combined_score = final_score as f32; @@ -3081,60 +1864,6 @@ impl SqliteMemoryStore { Ok(results) } - /// Keyword-only fallback for builds without local embeddings/vector search. - #[cfg(not(all(feature = "embeddings", feature = "vector-search")))] - pub fn hybrid_search( - &self, - query: &str, - limit: i32, - _keyword_weight: f32, - _semantic_weight: f32, - ) -> Result> { - self.hybrid_search_filtered(query, limit, 1.0, 0.0, None, None) - } - - /// Keyword-only fallback for builds without local embeddings/vector search. - #[cfg(not(all(feature = "embeddings", feature = "vector-search")))] - pub fn hybrid_search_filtered( - &self, - query: &str, - limit: i32, - _keyword_weight: f32, - _semantic_weight: f32, - include_types: Option<&[String]>, - exclude_types: Option<&[String]>, - ) -> Result> { - let nodes = self.search_terms(query, limit.max(1) * 4)?; - let mut results = Vec::new(); - - for node in nodes { - if let Some(includes) = include_types { - if !includes.iter().any(|t| t == &node.node_type) { - continue; - } - } else if let Some(excludes) = exclude_types - && excludes.iter().any(|t| t == &node.node_type) - { - continue; - } - - let score = 1.0 / (results.len() as f32 + 1.0); - results.push(SearchResult { - node, - keyword_score: Some(score), - semantic_score: None, - combined_score: score, - match_type: MatchType::Keyword, - }); - - if results.len() >= limit.max(1) as usize { - break; - } - } - - Ok(results) - } - /// Keyword search returning scores, with optional type filtering in the SQL query. #[cfg(all(feature = "embeddings", feature = "vector-search"))] fn keyword_search_with_scores( @@ -3229,9 +1958,6 @@ impl SqliteMemoryStore { /// Semantic search returning scores #[cfg(all(feature = "embeddings", feature = "vector-search"))] fn semantic_search_raw(&self, query: &str, limit: i32) -> Result> { - if !self.vector_search_available() { - return Ok(vec![]); - } if !self.embedding_service.is_ready() { return Ok(vec![]); } @@ -3258,8 +1984,8 @@ impl SqliteMemoryStore { _ => self.get_query_embedding(query)?, }; - let index = self.vector_index.as_ref().unwrap(); - let index = index + let index = self + .vector_index .lock() .map_err(|_| StorageError::Init("Vector index lock poisoned".to_string()))?; @@ -3282,30 +2008,66 @@ impl SqliteMemoryStore { } let mut result = EmbeddingResult::default(); - let active_model = self.embedding_service.model_name(); - let nodes = self.embedding_regeneration_candidates(node_ids, force)?; - for (id, content, stored_model) in nodes { + let nodes: Vec<(String, String)> = { + let reader = self + .reader + .lock() + .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; + if let Some(ids) = node_ids { + let placeholders = ids.iter().map(|_| "?").collect::>().join(","); + let query = format!( + "SELECT id, content FROM knowledge_nodes WHERE id IN ({})", + placeholders + ); + + let mut result_nodes = Vec::new(); + { + let mut stmt = reader.prepare(&query)?; + let params: Vec<&dyn rusqlite::ToSql> = + ids.iter().map(|s| s as &dyn rusqlite::ToSql).collect(); + + let rows = stmt.query_map(params.as_slice(), |row| { + Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)) + })?; + + for r in rows.flatten() { + result_nodes.push(r); + } + } + result_nodes + } else if force { + let mut stmt = reader.prepare("SELECT id, content FROM knowledge_nodes")?; + let rows = stmt.query_map([], |row| { + Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)) + })?; + rows.filter_map(|r| r.ok()).collect() + } else { + let mut stmt = reader.prepare( + "SELECT id, content FROM knowledge_nodes + WHERE has_embedding = 0 OR has_embedding IS NULL", + )?; + let rows = stmt.query_map([], |row| { + Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)) + })?; + rows.filter_map(|r| r.ok()).collect() + } + }; + + for (id, content) in nodes { if !force { - let (has_emb, stored_model): (i32, Option) = self + let has_emb: i32 = self .reader .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))? .query_row( - "SELECT COALESCE(kn.has_embedding, 0), COALESCE(ne.model, kn.embedding_model) - FROM knowledge_nodes kn - LEFT JOIN node_embeddings ne ON ne.node_id = kn.id - WHERE kn.id = ?1", - params![&id], - |row| Ok((row.get(0)?, row.get(1)?)), + "SELECT COALESCE(has_embedding, 0) FROM knowledge_nodes WHERE id = ?1", + params![id], + |row| row.get(0), ) - .unwrap_or((0, stored_model)); + .unwrap_or(0); - if has_emb == 1 - && stored_model.as_deref().is_some_and(|model| { - Self::embedding_model_matches_active(model, active_model) - }) - { + if has_emb == 1 { result.skipped += 1; continue; } @@ -3323,78 +2085,6 @@ impl SqliteMemoryStore { Ok(result) } - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - fn embedding_regeneration_candidates( - &self, - node_ids: Option<&[String]>, - force: bool, - ) -> Result)>> { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - - if let Some(ids) = node_ids { - if ids.is_empty() { - return Ok(Vec::new()); - } - - let placeholders = ids.iter().map(|_| "?").collect::>().join(","); - let query = format!( - "SELECT kn.id, kn.content, COALESCE(ne.model, kn.embedding_model) AS embedding_model - FROM knowledge_nodes kn - LEFT JOIN node_embeddings ne ON ne.node_id = kn.id - WHERE kn.id IN ({})", - placeholders - ); - - let mut stmt = reader.prepare(&query)?; - let params: Vec<&dyn rusqlite::ToSql> = - ids.iter().map(|s| s as &dyn rusqlite::ToSql).collect(); - let rows = stmt.query_map(params.as_slice(), |row| { - Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, Option>(2)?, - )) - })?; - return Ok(rows.filter_map(|r| r.ok()).collect()); - } - - if force { - let mut stmt = - reader.prepare("SELECT id, content, embedding_model FROM knowledge_nodes")?; - let rows = stmt.query_map([], |row| { - Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, Option>(2)?, - )) - })?; - return Ok(rows.filter_map(|r| r.ok()).collect()); - } - - let active_model = self.embedding_service.model_name(); - let model_pattern = Self::active_embedding_model_like_pattern(active_model); - let mut stmt = reader.prepare( - "SELECT kn.id, kn.content, COALESCE(ne.model, kn.embedding_model) AS embedding_model - FROM knowledge_nodes kn - LEFT JOIN node_embeddings ne ON ne.node_id = kn.id - WHERE kn.has_embedding = 0 - OR kn.has_embedding IS NULL - OR ne.node_id IS NULL - OR COALESCE(ne.model, kn.embedding_model, '') NOT LIKE ?1", - )?; - let rows = stmt.query_map(params![model_pattern], |row| { - Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, Option>(2)?, - )) - })?; - Ok(rows.filter_map(|r| r.ok()).collect()) - } - /// Query memories valid at a specific time pub fn query_at_time( &self, @@ -3668,8 +2358,7 @@ impl SqliteMemoryStore { } } - // 3. Generate missing and model-mismatched embeddings. - // This must drain the whole set so embedder upgrades do not strand v1 corpora. + // 3. Generate missing embeddings #[cfg(all(feature = "embeddings", feature = "vector-search"))] let embeddings_generated = self.generate_missing_embeddings()?; #[cfg(not(all(feature = "embeddings", feature = "vector-search")))] @@ -3731,149 +2420,6 @@ impl SqliteMemoryStore { } } - // 8.5. Retroactive Salience Backfill — memory with hindsight (auto-fire). - // - // The dream pass (step 8) replays memories forward to synthesize insights. - // This is its backward twin: when a recent memory is a salient FAILURE, - // reach BACKWARD across history and PROMOTE the quiet earlier memory that - // caused it — the root cause a semantic search structurally cannot surface - // because it is causally upstream, not *similar*. Faithful port of the - // offline ensemble co-reactivation in Zaki/Cai et al. 2024 Nature; the - // consolidation pass IS the offline window. Bounded on every axis so a - // noisy day cannot trigger a promotion storm, and idempotent across cycles - // via a durable causal edge (so the same cause is promoted once per - // failure, not every cycle). - // - // OPT-OUT (backfill-safety, v2.2.1): auto-fire is ON by default — it shipped - // and was documented in v2.2.0, so we keep the behavior — but is now bounded - // and disableable. It mutates FSRS scores on the canonical store and can lift - // a memory across a downstream consolidation floor, so a consumer that reads - // `stability` as a durability gate can turn it off with - // VESTIGE_BACKFILL_AUTOFIRE=0 (or false/off/no). The `backfill` MCP tool + CLI - // remain available for on-demand, operator-driven backfill regardless of the - // gate. The promote is bounded: both the auto-fire and manual paths call - // promote_memory_backfill (stability = MIN(stability*1.5, stability+365)) so - // repeated per-(cause, failure) promotions cannot inflate without bound (the - // prior comment claimed promote_memory was capped — it was not). - let backfill_autofire = std::env::var("VESTIGE_BACKFILL_AUTOFIRE") - .map(|v| { - let v = v.trim(); - !(v.eq_ignore_ascii_case("false") - || v.eq_ignore_ascii_case("off") - || v.eq_ignore_ascii_case("no") - || v == "0") - }) - .unwrap_or(true); - let mut backfilled_causes = 0i64; - if backfill_autofire { - use crate::advanced::retroactive_backfill::{ - self as rb, BackfillCandidate, FailureEvent, RetroactiveBackfill, - }; - const MAX_FAILURES_PER_CYCLE: usize = 5; - const CANDIDATE_SCAN: i32 = 500; - - let recent = self.get_all_nodes(CANDIDATE_SCAN, 0).unwrap_or_default(); - let failures: Vec<&KnowledgeNode> = recent - .iter() - .filter(|n| rb::looks_like_failure(&n.content, &n.tags)) - .take(MAX_FAILURES_PER_CYCLE) - .collect(); - - if !failures.is_empty() { - let backfill = RetroactiveBackfill::new(); - let mut already_promoted: std::collections::HashSet<(String, String)> = - std::collections::HashSet::new(); - - for failure_node in failures { - let failure = FailureEvent { - id: failure_node.id.clone(), - content: failure_node.content.clone(), - entities: rb::extract_entities(&failure_node.content, &failure_node.tags), - tags: failure_node.tags.clone(), - prediction_error: 0.9, - manual: false, - }; - // candidates = every OTHER memory strictly older than the - // failure, EXCLUDING other failures (a root cause is the quiet - // upstream change, not an earlier crash). - let candidates: Vec = recent - .iter() - .filter(|c| c.id != failure_node.id) - .filter(|c| !rb::looks_like_failure(&c.content, &c.tags)) - .filter_map(|c| { - let age = (failure_node.created_at - c.created_at).num_seconds() - as f64 - / 86_400.0; - if age <= 0.0 { - return None; - } - Some(BackfillCandidate { - id: c.id.clone(), - content: c.content.clone(), - entities: rb::extract_entities(&c.content, &c.tags), - age_days_before_failure: age, - stability: c.stability, - similarity_to_failure: None, - }) - }) - .collect(); - - let result = backfill.run(&failure, &candidates); - if !result.triggered { - continue; - } - for cause in &result.causes { - if !already_promoted - .insert((cause.memory_id.clone(), failure_node.id.clone())) - { - continue; - } - // Cross-cycle idempotency: a durable causal edge is both the - // dedup key and a first-class artifact. Write it FIRST, only - // promote if it persisted (a failed edge write => retry next - // cycle cleanly, never double-inflate). - let link_type = crate::memory::EdgeType::Causal.to_string(); - let already_linked = self - .get_connections_for_memory(&cause.memory_id) - .map(|conns| { - conns.iter().any(|c| { - c.source_id == cause.memory_id - && c.target_id == failure_node.id - && c.link_type == link_type - }) - }) - .unwrap_or(false); - if already_linked { - continue; - } - let conn = ConnectionRecord { - source_id: cause.memory_id.clone(), - target_id: failure_node.id.clone(), - strength: 1.0, - link_type, - created_at: Utc::now(), - last_activated: Utc::now(), - activation_count: 0, - }; - if self.save_connection(&conn).is_err() { - continue; - } - if self.promote_memory_backfill(&cause.memory_id).is_ok() { - backfilled_causes += 1; - } - } - } - if backfilled_causes > 0 { - tracing::info!( - backfilled_causes, - "Retroactive Salience Backfill: promoted {} root-cause memor{} a semantic search would miss", - backfilled_causes, - if backfilled_causes == 1 { "y" } else { "ies" } - ); - } - } - } - // 9. Memory Compression (old memories → summaries) let mut _memories_compressed = 0i64; { @@ -4047,41 +2593,15 @@ impl SqliteMemoryStore { neighbors_reinforced: 0, activations_computed, w20_optimized, - backfilled_causes, }) } /// Auto-deduplicate similar memories during consolidation (episodic → semantic merge) /// - /// Finds clusters with cosine similarity >= 0.85, keeps the strongest node, + /// Finds clusters with cosine similarity > 0.85, keeps the strongest node, /// appends unique content from weaker nodes, and deletes duplicates. - /// Honors the `VESTIGE_AUTO_CONSOLIDATE_MERGE` opt-out (unset → on) and - /// never merges away or deletes protected (pinned) nodes (#142). #[cfg(all(feature = "embeddings", feature = "vector-search"))] fn auto_dedup_consolidation(&self) -> Result { - // OPT-OUT (auto-consolidate-merge, #142): this pass concat-merges - // near-duplicate memories and HARD-DELETES the weaker ones with no - // reflog. It is ON by default (behavior unchanged), but a consumer that - // does not want unattended, no-audit merges can turn it off with - // VESTIGE_AUTO_CONSOLIDATE_MERGE=0 (or false/off/no). Parsed exactly like - // the sibling VESTIGE_BACKFILL_AUTOFIRE: unset or any other/malformed - // value → on (fail-open to the documented default). The `dedup` MCP tool - // remains available for on-demand, previewable, reversible merges - // regardless of the gate. Gate here (not the caller) so it stays with the - // pin filter and self-protects against a future second caller. - let auto_merge = std::env::var("VESTIGE_AUTO_CONSOLIDATE_MERGE") - .map(|v| { - let v = v.trim(); - !(v.eq_ignore_ascii_case("false") - || v.eq_ignore_ascii_case("off") - || v.eq_ignore_ascii_case("no") - || v == "0") - }) - .unwrap_or(true); - if !auto_merge { - return Ok(0); - } - let all_embeddings = self.get_all_embeddings()?; let n = all_embeddings.len(); @@ -4089,35 +2609,19 @@ impl SqliteMemoryStore { return Ok(0); } - // Protected (pinned) memories must never be touched by this unattended, - // no-audit pass — mirroring the interactive contract that a protected - // node may only survive a merge, never be absorbed (see `plan_merge`). - // Fetch the set ONCE here, before the per-cluster reader lock is taken: - // both `protected_node_ids()` and `is_protected()` take their OWN reader - // lock, so calling either inside the lock window below would self-deadlock - // the non-reentrant Mutex. Skipping protected ids at BOTH the outer - // (anchor) and inner (member) loops guarantees a protected node is never - // an anchor and never a cluster member — so it can never be the keeper nor - // land in weak_ids, and is thus never merged into and never deleted. Fails - // SAFE via `?`: on a poisoned lock the caller's unwrap_or(0) skips the - // merge this cycle rather than risk absorbing a pin. #142 - let protected = self.protected_node_ids()?; - const SIMILARITY_THRESHOLD: f32 = 0.85; let mut merged_count = 0i64; let mut consumed: std::collections::HashSet = std::collections::HashSet::new(); for i in 0..n { - if consumed.contains(&all_embeddings[i].0) || protected.contains(&all_embeddings[i].0) { + if consumed.contains(&all_embeddings[i].0) { continue; } let mut cluster: Vec<(usize, f32)> = Vec::new(); for j in (i + 1)..n { - if consumed.contains(&all_embeddings[j].0) - || protected.contains(&all_embeddings[j].0) - { + if consumed.contains(&all_embeddings[j].0) { continue; } let sim = crate::embeddings::cosine_similarity( @@ -4408,7 +2912,7 @@ impl SqliteMemoryStore { Ok(Some(optimized_w20)) } - /// Generate all missing or active-model-mismatched embeddings. + /// Generate missing embeddings #[cfg(all(feature = "embeddings", feature = "vector-search"))] fn generate_missing_embeddings(&self) -> Result { if !self.embedding_service.is_ready() @@ -4418,15 +2922,33 @@ impl SqliteMemoryStore { return Ok(0); } - let result = self.generate_embeddings(None, false)?; - if result.failed > 0 { - tracing::warn!( - failed = result.failed, - "Some embeddings could not be regenerated during consolidation" - ); + let nodes: Vec<(String, String)> = { + let reader = self + .reader + .lock() + .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; + reader + .prepare( + "SELECT id, content FROM knowledge_nodes + WHERE has_embedding = 0 OR has_embedding IS NULL + LIMIT 100", + )? + .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))? + .filter_map(|r| r.ok()) + .collect() + }; + + let mut count = 0i64; + + for (id, content) in nodes { + if let Err(e) = self.generate_embedding_for_node(&id, &content) { + tracing::warn!("Failed to generate embedding for {}: {}", id, e); + } else { + count += 1; + } } - Ok(result.successful) + Ok(count) } } @@ -4557,1006 +3079,7 @@ pub struct DreamHistoryRecord { pub creative_connections_found: Option, } -/// Composition event envelope for ComposedGraph. -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CompositionEventRecord { - pub id: String, - pub created_at: DateTime, - pub tool: String, - pub mode: String, - pub query: Option, - pub query_hash: Option, - pub confidence: Option, - pub status: Option, - pub output_preview: Option, - pub metadata: serde_json::Value, -} - -/// Memory participating in a composition event. -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CompositionMemberRecord { - pub event_id: String, - pub memory_id: String, - pub role: String, - pub rank: i32, - pub trust: Option, - pub score: Option, - pub preview: Option, - pub metadata: serde_json::Value, -} - -/// Outcome label attached to a composition event. -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CompositionOutcomeRecord { - pub id: String, - pub event_id: String, - pub outcome_type: String, - pub labeled_at: DateTime, - pub label_source: String, - pub confidence_delta: Option, - pub notes: Option, - pub metadata: serde_json::Value, -} - -/// Memory most often composed with another memory. -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CompositionNeighborRecord { - pub memory_id: String, - pub composed_count: i64, - pub latest_event_at: DateTime, -} - -/// Candidate memory pair that shares useful shape but has never been composed. -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct NeverComposedCandidate { - pub first_id: String, - pub second_id: String, - pub score: f64, - pub novelty_score: f64, - pub bridge_score: f64, - pub trust_score: f64, - pub outcome_score_adjustment: f64, - pub shared_tags: Vec, - pub boundary_tags: Vec, - pub shared_terms: Vec, - pub prior_outcomes: Vec, - pub outcome_signal: String, - pub first_node_type: String, - pub second_node_type: String, - pub first_preview: String, - pub second_preview: String, - pub reason: String, - pub composition_question: String, -} - -impl SqliteMemoryStore { - // ======================================================================== - // COMPOSEDGRAPH PERSISTENCE - // ======================================================================== - - /// Save a complete composition event with members and optional outcomes in one transaction. - pub fn save_composition( - &self, - event: &CompositionEventRecord, - members: &[CompositionMemberRecord], - outcomes: &[CompositionOutcomeRecord], - ) -> Result<()> { - let mut writer = self - .writer - .lock() - .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; - let tx = writer.transaction()?; - - let metadata_json = - serde_json::to_string(&event.metadata).unwrap_or_else(|_| "{}".to_string()); - tx.execute( - "INSERT OR REPLACE INTO composition_events ( - id, created_at, tool, mode, query, query_hash, confidence, status, - output_preview, metadata - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)", - params![ - event.id, - event.created_at.to_rfc3339(), - event.tool, - event.mode, - event.query, - event.query_hash, - event.confidence, - event.status, - event.output_preview, - metadata_json, - ], - )?; - - for member in members { - let mut member = member.clone(); - Self::snapshot_composition_member_tags(&tx, &mut member)?; - Self::insert_composition_member(&tx, &member)?; - } - for outcome in outcomes { - Self::insert_composition_outcome(&tx, outcome)?; - } - - tx.commit()?; - Ok(()) - } - - /// Add one outcome label to an existing composition event. - pub fn record_composition_outcome(&self, outcome: &CompositionOutcomeRecord) -> Result<()> { - let writer = self - .writer - .lock() - .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; - Self::insert_composition_outcome(&writer, outcome) - } - - /// Get one composition event by id. - pub fn get_composition_event(&self, id: &str) -> Result> { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let mut stmt = reader.prepare("SELECT * FROM composition_events WHERE id = ?1")?; - stmt.query_row(params![id], Self::row_to_composition_event) - .optional() - .map_err(StorageError::from) - } - - /// Get recent composition events. - pub fn get_recent_composition_events(&self, limit: i32) -> Result> { - self.get_recent_composition_events_page(limit, 0) - } - - /// Get recent composition events with explicit pagination. - pub fn get_recent_composition_events_page( - &self, - limit: i32, - offset: i32, - ) -> Result> { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let mut stmt = reader.prepare( - "SELECT * FROM composition_events - ORDER BY created_at DESC - LIMIT ?1 OFFSET ?2", - )?; - let rows = stmt.query_map( - params![limit.max(1), offset.max(0)], - Self::row_to_composition_event, - )?; - let mut result = Vec::new(); - for row in rows { - result.push(row?); - } - Ok(result) - } - - /// Get all members for a composition event. - pub fn get_composition_members(&self, event_id: &str) -> Result> { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let mut stmt = reader.prepare( - "SELECT * FROM composition_members - WHERE event_id = ?1 - ORDER BY rank ASC, role ASC, memory_id ASC", - )?; - let rows = stmt.query_map(params![event_id], Self::row_to_composition_member)?; - let mut result = Vec::new(); - for row in rows { - result.push(row?); - } - Ok(result) - } - - /// Get all outcomes for a composition event. - pub fn get_composition_outcomes( - &self, - event_id: &str, - ) -> Result> { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let mut stmt = reader.prepare( - "SELECT * FROM composition_outcomes - WHERE event_id = ?1 - ORDER BY labeled_at DESC", - )?; - let rows = stmt.query_map(params![event_id], Self::row_to_composition_outcome)?; - let mut result = Vec::new(); - for row in rows { - result.push(row?); - } - Ok(result) - } - - /// Get composition events containing a memory id. - pub fn get_compositions_for_memory( - &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 DISTINCT e.* - FROM composition_events e - JOIN composition_members m ON m.event_id = e.id - WHERE m.memory_id = ?1 - ORDER BY e.created_at DESC - LIMIT ?2", - )?; - let rows = stmt.query_map( - params![memory_id, limit.max(1)], - Self::row_to_composition_event, - )?; - let mut result = Vec::new(); - for row in rows { - result.push(row?); - } - Ok(result) - } - - /// Return memories most frequently composed with the requested memory. - pub fn get_composition_neighbors( - &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( - "WITH distinct_members AS ( - SELECT DISTINCT event_id, memory_id FROM composition_members - ) - SELECT other.memory_id, COUNT(DISTINCT other.event_id) AS composed_count, MAX(e.created_at) AS latest_event_at - FROM distinct_members self - JOIN distinct_members other - ON other.event_id = self.event_id AND other.memory_id != self.memory_id - JOIN composition_events e ON e.id = self.event_id - WHERE self.memory_id = ?1 - GROUP BY other.memory_id - ORDER BY composed_count DESC, latest_event_at DESC - LIMIT ?2", - )?; - let rows = stmt.query_map(params![memory_id, limit.max(1)], |row| { - Ok(CompositionNeighborRecord { - memory_id: row.get(0)?, - composed_count: row.get(1)?, - latest_event_at: Self::parse_timestamp( - &row.get::<_, String>(2)?, - "latest_event_at", - )?, - }) - })?; - let mut result = Vec::new(); - for row in rows { - result.push(row?); - } - Ok(result) - } - - /// Generate ranked memory pairs that share useful tags but have not yet been composed. - pub fn get_never_composed_candidates( - &self, - limit: i32, - tag_filter: Option<&[String]>, - ) -> Result> { - let nodes = self.composition_candidate_nodes(tag_filter)?; - let composed_pairs = self.composed_pair_set()?; - let composition_degrees = self.composition_degree_map()?; - let outcome_map = self.composition_outcome_map()?; - - // SEMANTIC-BAND GATE (the composition generativity unlock): load embeddings so a pair - // that shares NO literal tag/word but lives in the "distant-but-relatable" cosine band - // can still surface as a never-composed insight — exactly the non-obvious combination - // a keyword/exact-overlap gate (and cosine-NN search) can never return. The band excludes - // near-duplicates (>= 0.85, those are the same idea) and unrelated noise (< 0.45). - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - let embedding_map: std::collections::HashMap> = self - .get_all_embeddings() - .map(|v| v.into_iter().collect()) - .unwrap_or_default(); - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - const COMPOSE_BAND_LO: f32 = 0.45; - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - const COMPOSE_BAND_HI: f32 = 0.85; - - let mut candidates = Vec::new(); - - for i in 0..nodes.len() { - for j in (i + 1)..nodes.len() { - let a = &nodes[i]; - let b = &nodes[j]; - let pair = Self::pair_key(&a.id, &b.id); - if composed_pairs.contains(&pair) { - continue; - } - - if let Some(filter) = tag_filter - && !filter.is_empty() - && !Self::node_pair_matches_tag_filter(a, b, filter) - { - continue; - } - - let shared_tags = Self::shared_tags(&a.tags, &b.tags); - let shared_terms = Self::shared_content_terms(&a.content, &b.content, 8); - - // Semantic-band cosine: lets a pair with NO shared surface tokens but a - // related MEANING through the gate (the generative cross-domain combination). - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - let band_cos: Option = match (embedding_map.get(&a.id), embedding_map.get(&b.id)) - { - (Some(ea), Some(eb)) => { - let c = crate::embeddings::cosine_similarity(ea, eb); - if (COMPOSE_BAND_LO..COMPOSE_BAND_HI).contains(&c) { - Some(c) - } else { - None - } - } - _ => None, - }; - #[cfg(not(all(feature = "embeddings", feature = "vector-search")))] - let band_cos: Option = None; - - // Admit the pair if it shares surface signal OR it sits in the semantic band. - if shared_tags.is_empty() && shared_terms.is_empty() && band_cos.is_none() { - continue; - } - - let boundary_tags = Self::boundary_tags_for_pair(&a.tags, &b.tags); - let trust_score = - ((a.retention_strength + b.retention_strength) / 2.0).clamp(0.0, 1.0); - let degree_a = composition_degrees.get(&a.id).copied().unwrap_or(0) as f64; - let degree_b = composition_degrees.get(&b.id).copied().unwrap_or(0) as f64; - let novelty_score = ((1.0 / (1.0 + degree_a)) + (1.0 / (1.0 + degree_b))) / 2.0; - let bridge_score = Self::composition_bridge_score( - a, - b, - &shared_tags, - &shared_terms, - &boundary_tags, - ); - let anchor_score = - (shared_tags.len() as f64 * 0.45) + (shared_terms.len().min(5) as f64 * 0.25); - // Semantic-band pairs (no surface overlap) get an anchor from cosine so they - // clear the cutoff: a mid-band 0.45-0.85 meaning-match is a strong compose signal. - let band_anchor = band_cos.map(|c| 1.0 + (c as f64 - 0.45) * 2.0).unwrap_or(0.0); - let prior_outcomes = Self::pair_prior_outcomes(&outcome_map, &a.id, &b.id); - let outcome_signal = Self::outcome_signal(&prior_outcomes); - let outcome_score_adjustment = Self::outcome_score_adjustment(&prior_outcomes); - let score = anchor_score - + band_anchor - + (bridge_score * 2.0) - + (novelty_score * 1.5) - + trust_score - + outcome_score_adjustment; - if score < 1.6 { - continue; - } - - let reason = if !boundary_tags.is_empty() { - format!( - "Untried bridge across {} with {}", - boundary_tags.join(", "), - Self::anchor_summary(&shared_tags, &shared_terms) - ) - } else if a.node_type != b.node_type { - format!( - "Untried {} -> {} composition with {}", - a.node_type, - b.node_type, - Self::anchor_summary(&shared_tags, &shared_terms) - ) - } else { - format!( - "Never composed despite {}", - Self::anchor_summary(&shared_tags, &shared_terms) - ) - }; - let composition_question = - Self::composition_question(a, b, &shared_tags, &shared_terms, &boundary_tags); - candidates.push(NeverComposedCandidate { - first_id: a.id.clone(), - second_id: b.id.clone(), - score, - novelty_score, - bridge_score, - trust_score, - outcome_score_adjustment, - shared_tags, - boundary_tags, - shared_terms, - prior_outcomes, - outcome_signal, - first_node_type: a.node_type.clone(), - second_node_type: b.node_type.clone(), - first_preview: preview(&a.content, 160), - second_preview: preview(&b.content, 160), - reason, - composition_question, - }); - } - } - - candidates.sort_by(|a, b| { - b.score - .partial_cmp(&a.score) - .unwrap_or(std::cmp::Ordering::Equal) - }); - candidates.truncate(limit.max(1) as usize); - Ok(candidates) - } - - fn insert_composition_member( - conn: &Connection, - member: &CompositionMemberRecord, - ) -> Result<()> { - let metadata_json = - serde_json::to_string(&member.metadata).unwrap_or_else(|_| "{}".to_string()); - conn.execute( - "INSERT OR REPLACE INTO composition_members ( - event_id, memory_id, role, rank, trust, score, preview, metadata - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", - params![ - member.event_id, - member.memory_id, - member.role, - member.rank, - member.trust, - member.score, - member.preview, - metadata_json, - ], - )?; - Ok(()) - } - - fn snapshot_composition_member_tags( - conn: &Connection, - member: &mut CompositionMemberRecord, - ) -> Result<()> { - if member.metadata.get("tags").is_some() { - return Ok(()); - } - - let tags_json: Option = conn - .query_row( - "SELECT tags FROM knowledge_nodes WHERE id = ?1", - params![member.memory_id], - |row| row.get(0), - ) - .optional()?; - let Some(tags_json) = tags_json else { - return Ok(()); - }; - let Ok(tags) = serde_json::from_str::>(&tags_json) else { - return Ok(()); - }; - if tags.is_empty() { - return Ok(()); - } - - if let Some(object) = member.metadata.as_object_mut() { - object.insert("tags".to_string(), serde_json::json!(tags)); - } else { - member.metadata = serde_json::json!({ "tags": tags }); - } - Ok(()) - } - - fn insert_composition_outcome( - conn: &Connection, - outcome: &CompositionOutcomeRecord, - ) -> Result<()> { - let metadata_json = - serde_json::to_string(&outcome.metadata).unwrap_or_else(|_| "{}".to_string()); - conn.execute( - "INSERT OR REPLACE INTO composition_outcomes ( - id, event_id, outcome_type, labeled_at, label_source, - confidence_delta, notes, metadata - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", - params![ - outcome.id, - outcome.event_id, - outcome.outcome_type, - outcome.labeled_at.to_rfc3339(), - outcome.label_source, - outcome.confidence_delta, - outcome.notes, - metadata_json, - ], - )?; - Ok(()) - } - - fn row_to_composition_event(row: &rusqlite::Row) -> rusqlite::Result { - let metadata_json: String = row.get("metadata")?; - Ok(CompositionEventRecord { - id: row.get("id")?, - created_at: Self::parse_timestamp(&row.get::<_, String>("created_at")?, "created_at")?, - tool: row.get("tool")?, - mode: row.get("mode")?, - query: row.get("query").ok().flatten(), - query_hash: row.get("query_hash").ok().flatten(), - confidence: row.get("confidence").ok().flatten(), - status: row.get("status").ok().flatten(), - output_preview: row.get("output_preview").ok().flatten(), - metadata: serde_json::from_str(&metadata_json) - .unwrap_or_else(|_| serde_json::json!({})), - }) - } - - fn row_to_composition_member(row: &rusqlite::Row) -> rusqlite::Result { - let metadata_json: String = row.get("metadata")?; - Ok(CompositionMemberRecord { - event_id: row.get("event_id")?, - memory_id: row.get("memory_id")?, - role: row.get("role")?, - rank: row.get("rank").unwrap_or(0), - trust: row.get("trust").ok().flatten(), - score: row.get("score").ok().flatten(), - preview: row.get("preview").ok().flatten(), - metadata: serde_json::from_str(&metadata_json) - .unwrap_or_else(|_| serde_json::json!({})), - }) - } - - fn row_to_composition_outcome( - row: &rusqlite::Row, - ) -> rusqlite::Result { - let metadata_json: String = row.get("metadata")?; - Ok(CompositionOutcomeRecord { - id: row.get("id")?, - event_id: row.get("event_id")?, - outcome_type: row.get("outcome_type")?, - labeled_at: Self::parse_timestamp(&row.get::<_, String>("labeled_at")?, "labeled_at")?, - label_source: row - .get("label_source") - .unwrap_or_else(|_| "tool".to_string()), - confidence_delta: row.get("confidence_delta").ok().flatten(), - notes: row.get("notes").ok().flatten(), - metadata: serde_json::from_str(&metadata_json) - .unwrap_or_else(|_| serde_json::json!({})), - }) - } - - fn composition_event_exists(conn: &Connection, id: &str) -> Result { - let count: i64 = conn.query_row( - "SELECT COUNT(*) FROM composition_events WHERE id = ?1", - params![id], - |row| row.get(0), - )?; - Ok(count > 0) - } - - fn composed_pair_set(&self) -> Result> { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let mut stmt = reader.prepare( - "SELECT event_id, memory_id - FROM composition_members - ORDER BY event_id, memory_id", - )?; - let rows = stmt.query_map([], |row| { - Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)) - })?; - let mut grouped: HashMap> = HashMap::new(); - for row in rows { - let (event_id, memory_id) = row?; - grouped.entry(event_id).or_default().push(memory_id); - } - - let mut pairs = HashSet::new(); - for ids in grouped.values_mut() { - ids.sort(); - ids.dedup(); - for i in 0..ids.len() { - for j in (i + 1)..ids.len() { - pairs.insert(Self::pair_key(&ids[i], &ids[j])); - } - } - } - Ok(pairs) - } - - fn pair_key(a: &str, b: &str) -> (String, String) { - if a <= b { - (a.to_string(), b.to_string()) - } else { - (b.to_string(), a.to_string()) - } - } - - fn shared_tags(a: &[String], b: &[String]) -> Vec { - let b_set: HashSet<&str> = b.iter().map(String::as_str).collect(); - let mut shared = a - .iter() - .filter(|tag| b_set.contains(tag.as_str())) - .cloned() - .collect::>(); - shared.sort(); - shared.dedup(); - shared - } - - fn node_pair_matches_tag_filter( - a: &KnowledgeNode, - b: &KnowledgeNode, - tag_filter: &[String], - ) -> bool { - a.tags.iter().chain(b.tags.iter()).any(|tag| { - tag_filter - .iter() - .any(|wanted| wanted == tag || tag.starts_with(&format!("{wanted}:"))) - }) - } - - fn boundary_tags_for_pair(a: &[String], b: &[String]) -> Vec { - let mut tags = a - .iter() - .chain(b.iter()) - .filter(|tag| Self::is_boundary_tag(tag)) - .cloned() - .collect::>(); - tags.sort(); - tags.dedup(); - tags - } - - fn composition_bridge_score( - a: &KnowledgeNode, - b: &KnowledgeNode, - shared_tags: &[String], - shared_terms: &[String], - boundary_tags: &[String], - ) -> f64 { - let tag_distance = Self::tag_distance(&a.tags, &b.tags); - let node_type_bridge = if a.node_type != b.node_type { 1.0 } else { 0.0 }; - let boundary_bridge = (boundary_tags.len() as f64 / 4.0).min(1.0); - let lexical_anchor = if shared_terms.is_empty() { 0.0 } else { 1.0 }; - let tag_anchor = if shared_tags.is_empty() { 0.0 } else { 1.0 }; - - (tag_distance * 0.30 - + node_type_bridge * 0.20 - + boundary_bridge * 0.25 - + lexical_anchor * 0.15 - + tag_anchor * 0.10) - .clamp(0.0, 1.0) - } - - fn tag_distance(a: &[String], b: &[String]) -> f64 { - let a_set = a.iter().map(String::as_str).collect::>(); - let b_set = b.iter().map(String::as_str).collect::>(); - let union = a_set.union(&b_set).count(); - if union == 0 { - return 0.0; - } - let intersection = a_set.intersection(&b_set).count(); - 1.0 - (intersection as f64 / union as f64) - } - - fn shared_content_terms(a: &str, b: &str, limit: usize) -> Vec { - let a_terms = Self::content_terms(a); - let b_terms = Self::content_terms(b); - let mut shared = a_terms - .intersection(&b_terms) - .cloned() - .collect::>(); - shared.sort_by(|left, right| { - Self::term_specificity_score(right) - .cmp(&Self::term_specificity_score(left)) - .then_with(|| left.cmp(right)) - }); - shared.truncate(limit); - shared - } - - fn content_terms(content: &str) -> HashSet { - const STOPWORDS: &[&str] = &[ - "about", "after", "again", "against", "because", "before", "between", "could", "every", - "first", "from", "have", "into", "memory", "needs", "should", "their", "there", - "these", "thing", "through", "using", "where", "which", "while", "would", - ]; - content - .to_ascii_lowercase() - .split(|c: char| !c.is_ascii_alphanumeric() && c != '-' && c != '_') - .filter(|term| term.len() >= 5 && !STOPWORDS.contains(term)) - .map(ToOwned::to_owned) - .collect() - } - - fn term_specificity_score(term: &str) -> usize { - term.len() - + term.chars().filter(|ch| ch.is_ascii_digit()).count() * 2 - + usize::from(term.contains('-')) * 2 - + usize::from(term.contains('_')) * 2 - } - - fn anchor_summary(shared_tags: &[String], shared_terms: &[String]) -> String { - if !shared_tags.is_empty() && !shared_terms.is_empty() { - format!( - "shared tags ({}) and shared terms ({})", - shared_tags.join(", "), - shared_terms - .iter() - .take(4) - .cloned() - .collect::>() - .join(", ") - ) - } else if !shared_tags.is_empty() { - format!("shared tags ({})", shared_tags.join(", ")) - } else { - format!( - "shared terms ({})", - shared_terms - .iter() - .take(4) - .cloned() - .collect::>() - .join(", ") - ) - } - } - - fn composition_question( - a: &KnowledgeNode, - b: &KnowledgeNode, - shared_tags: &[String], - shared_terms: &[String], - boundary_tags: &[String], - ) -> String { - let anchor = if !boundary_tags.is_empty() { - boundary_tags.join(", ") - } else if !shared_tags.is_empty() { - shared_tags.join(", ") - } else { - shared_terms - .iter() - .take(3) - .cloned() - .collect::>() - .join(", ") - }; - format!( - "What changes if a {} memory and a {} memory are composed through {}?", - a.node_type, b.node_type, anchor - ) - } - - fn composition_degree_map(&self) -> Result> { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let mut stmt = reader.prepare( - "SELECT memory_id, COUNT(DISTINCT event_id) AS composition_count - FROM composition_members - GROUP BY memory_id", - )?; - let rows = stmt.query_map([], |row| { - Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?)) - })?; - let mut result = HashMap::new(); - for row in rows { - let (memory_id, count) = row?; - result.insert(memory_id, count); - } - Ok(result) - } - - fn composition_candidate_nodes( - &self, - tag_filter: Option<&[String]>, - ) -> Result> { - const BASE_SCAN_LIMIT: i32 = 750; - const TAGGED_SCAN_LIMIT: i32 = 1500; - - let mut nodes = self.get_all_nodes(BASE_SCAN_LIMIT, 0)?; - if let Some(filter) = tag_filter - && !filter.is_empty() - { - let tagged_nodes = self.get_nodes_matching_any_tag_prefix(filter, TAGGED_SCAN_LIMIT)?; - let mut by_id = HashMap::new(); - for node in nodes.into_iter().chain(tagged_nodes) { - by_id.entry(node.id.clone()).or_insert(node); - } - nodes = by_id.into_values().collect(); - nodes.sort_by(|a, b| { - b.retention_strength - .partial_cmp(&a.retention_strength) - .unwrap_or(std::cmp::Ordering::Equal) - .then_with(|| b.created_at.cmp(&a.created_at)) - }); - } - Ok(nodes) - } - - fn get_nodes_matching_any_tag_prefix( - &self, - tag_filter: &[String], - limit: i32, - ) -> Result> { - let mut patterns = Vec::new(); - for wanted in tag_filter - .iter() - .map(|tag| tag.trim()) - .filter(|tag| !tag.is_empty()) - { - patterns.push(format!("%\"{}\"%", wanted)); - patterns.push(format!("%\"{}:%", wanted)); - } - if patterns.is_empty() { - return Ok(Vec::new()); - } - - let clauses = std::iter::repeat_n("tags LIKE ?", patterns.len()) - .collect::>() - .join(" OR "); - let sql = format!( - "SELECT * FROM knowledge_nodes - WHERE {clauses} - ORDER BY retention_strength DESC, created_at DESC - LIMIT {}", - limit.clamp(1, 5000) - ); - - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let mut stmt = reader.prepare(&sql)?; - let rows = stmt.query_map(params_from_iter(patterns.iter()), Self::row_to_node)?; - let mut result = Vec::new(); - for row in rows { - result.push(row?); - } - Ok(result) - } - - fn composition_outcome_map(&self) -> Result>> { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let mut stmt = reader.prepare( - "SELECT DISTINCT m.memory_id, o.outcome_type - FROM composition_members m - JOIN composition_outcomes o ON o.event_id = m.event_id", - )?; - let rows = stmt.query_map([], |row| { - Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)) - })?; - let mut result: HashMap> = HashMap::new(); - for row in rows { - let (memory_id, outcome) = row?; - result.entry(memory_id).or_default().insert(outcome); - } - Ok(result) - } - - fn pair_prior_outcomes( - outcome_map: &HashMap>, - first_id: &str, - second_id: &str, - ) -> Vec { - let mut outcomes = outcome_map - .get(first_id) - .into_iter() - .chain(outcome_map.get(second_id)) - .flat_map(|values| values.iter().cloned()) - .collect::>(); - outcomes.sort(); - outcomes.dedup(); - outcomes - } - - fn outcome_signal(prior_outcomes: &[String]) -> String { - if prior_outcomes.is_empty() { - return "clean".to_string(); - } - - let has_closed = prior_outcomes.iter().any(|outcome| { - matches!( - outcome.as_str(), - "dead_end" - | "rejected" - | "bad_severity" - | "user_demoted" - | "closed_by_scope" - | "closed_by_false_assumption" - | "closed_by_user" - | "expired_lane" - ) - }); - let has_duplicate = prior_outcomes - .iter() - .any(|outcome| matches!(outcome.as_str(), "duplicate_risk" | "closed_by_duplicate")); - let has_success = prior_outcomes.iter().any(|outcome| { - matches!( - outcome.as_str(), - "accepted" | "helpful" | "submitted" | "user_promoted" - ) - }); - let has_needs_poc = prior_outcomes.iter().any(|outcome| outcome == "needs_poc"); - - if (has_closed || has_duplicate) && has_success { - "mixed_prior_outcomes".to_string() - } else if has_closed { - "prior_closed_door".to_string() - } else if has_duplicate { - "prior_duplicate_risk".to_string() - } else if has_success { - "prior_success".to_string() - } else if has_needs_poc { - "prior_needs_poc".to_string() - } else { - "prior_outcome".to_string() - } - } - - fn outcome_score_adjustment(prior_outcomes: &[String]) -> f64 { - let mut adjustment: f64 = 0.0; - for outcome in prior_outcomes { - adjustment += match outcome.as_str() { - "accepted" => 0.35, - "helpful" => 0.25, - "submitted" => 0.15, - "user_promoted" => 0.20, - "needs_poc" => -0.05, - "duplicate_risk" => -0.35, - "closed_by_duplicate" => -0.40, - "dead_end" - | "rejected" - | "bad_severity" - | "closed_by_scope" - | "closed_by_false_assumption" - | "closed_by_user" - | "expired_lane" => -0.45, - "user_demoted" => -0.20, - _ => 0.0, - }; - } - adjustment.clamp(-0.8, 0.5) - } - - fn is_boundary_tag(tag: &str) -> bool { - let lowered = tag.to_ascii_lowercase(); - lowered.starts_with("boundary-") - || matches!( - lowered.as_str(), - "time" - | "chain" - | "role" - | "oracle" - | "queue" - | "settlement" - | "keeper" - | "upgrade" - | "pause" - | "accounting" - | "scope" - ) - } - +impl Storage { // ======================================================================== // INTENTIONS PERSISTENCE // ======================================================================== @@ -5924,25 +3447,6 @@ impl SqliteMemoryStore { Ok(result) } - /// The most recently created connections, capped at `limit`. Used by polling - /// surfaces (e.g. the dashboard changelog) that only need recent activity and - /// must not load the entire `memory_connections` table on every request. - pub fn get_recent_connections(&self, limit: usize) -> Result> { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let mut stmt = reader.prepare( - "SELECT * FROM memory_connections ORDER BY created_at DESC LIMIT ?1", - )?; - let rows = stmt.query_map([limit as i64], Self::row_to_connection)?; - let mut result = Vec::new(); - for row in rows { - result.push(row?); - } - Ok(result) - } - /// Strengthen a connection pub fn strengthen_connection( &self, @@ -6359,14 +3863,19 @@ impl SqliteMemoryStore { Ok(count) } - fn scan_last_backup_timestamp(backup_dir: &Path) -> Option> { + /// Get last backup timestamp by scanning the backups directory. + /// Parses `vestige-YYYYMMDD-HHMMSS.db` filenames. + pub fn get_last_backup_timestamp() -> Option> { + let proj_dirs = directories::ProjectDirs::from("com", "vestige", "core")?; + let backup_dir = proj_dirs.data_dir().parent()?.join("backups"); + if !backup_dir.exists() { return None; } let mut latest: Option> = None; - if let Ok(entries) = std::fs::read_dir(backup_dir) { + if let Ok(entries) = std::fs::read_dir(&backup_dir) { for entry in entries.flatten() { let name = entry.file_name(); let name_str = name.to_string_lossy(); @@ -6388,1088 +3897,6 @@ impl SqliteMemoryStore { latest } - /// Get last backup timestamp for this storage instance. - /// Parses `vestige-YYYYMMDD-HHMMSS.db` filenames. - pub fn last_backup_timestamp(&self) -> Option> { - Self::scan_last_backup_timestamp(&self.sidecar_dir("backups")) - } - - /// Get last backup timestamp in the default backups directory. - /// Kept for compatibility with older callers. - pub fn get_last_backup_timestamp() -> Option> { - let backup_dir = Self::default_db_path().ok()?.parent()?.join("backups"); - Self::scan_last_backup_timestamp(&backup_dir) - } - - /// Export an exact portable archive preserving raw Vestige storage rows. - /// - /// Unlike the user-facing JSON export, this preserves IDs, timestamps, - /// FSRS state, graph edges, suppression state, history tables, and raw - /// embedding blobs. It is intended for Vestige-to-Vestige device transfer. - pub fn export_portable_archive(&self) -> Result { - let mut reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let tx = reader.transaction()?; - - let schema_version = Self::current_schema_version(&tx)?; - let mut tables = Vec::new(); - - for table_name in PORTABLE_TABLES { - if !Self::table_exists(&tx, table_name)? { - continue; - } - - let quoted_table = Self::quote_ident(table_name); - let mut stmt = tx.prepare(&format!("SELECT * FROM {} ORDER BY rowid", quoted_table))?; - let columns: Vec = stmt - .column_names() - .iter() - .map(|name| (*name).to_string()) - .collect(); - let column_count = columns.len(); - - let rows = stmt.query_map([], |row| { - let mut values = Vec::with_capacity(column_count); - for idx in 0..column_count { - values.push(Self::portable_value_from_ref(row.get_ref(idx)?)?); - } - Ok(values) - })?; - - let mut portable_rows = Vec::new(); - for row in rows { - portable_rows.push(row?); - } - - tables.push(PortableTable { - name: (*table_name).to_string(), - columns, - rows: portable_rows, - }); - } - - let archive = PortableArchive { - archive_format: PORTABLE_ARCHIVE_FORMAT.to_string(), - vestige_version: crate::VERSION.to_string(), - schema_version, - exported_at: Utc::now(), - mode: "exact".to_string(), - tables, - }; - tx.commit()?; - Ok(archive) - } - - /// Write an exact portable archive to a JSON file. - pub fn export_portable_archive_to_path( - &self, - path: &std::path::Path, - ) -> Result { - let archive = self.export_portable_archive()?; - let parent = path.parent().unwrap_or_else(|| std::path::Path::new(".")); - let filename = path - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or("vestige-portable.json"); - let temp_path = parent.join(format!(".{}.tmp-{}", filename, Uuid::new_v4())); - - #[cfg(unix)] - let mut file = { - use std::os::unix::fs::OpenOptionsExt; - std::fs::OpenOptions::new() - .write(true) - .create_new(true) - .mode(0o600) - .open(&temp_path)? - }; - #[cfg(not(unix))] - let mut file = std::fs::File::create(&temp_path)?; - if let Err(e) = serde_json::to_writer_pretty(&mut file, &archive) { - let _ = std::fs::remove_file(&temp_path); - return Err(StorageError::Init(format!( - "Failed to write portable archive: {}", - e - ))); - } - file.flush()?; - file.sync_all()?; - drop(file); - - if let Err(rename_err) = std::fs::rename(&temp_path, path) { - if path.exists() { - std::fs::remove_file(path)?; - std::fs::rename(&temp_path, path)?; - } else { - let _ = std::fs::remove_file(&temp_path); - return Err(rename_err.into()); - } - } - Ok(archive) - } - - /// Import an exact portable archive. - /// - /// `EmptyOnly` preserves the conservative migration path. `Merge` is used - /// by portable sync to combine non-empty databases with tombstones and - /// newer-local conflict handling. - pub fn import_portable_archive( - &self, - archive: &PortableArchive, - mode: PortableImportMode, - ) -> Result { - if archive.archive_format != PORTABLE_ARCHIVE_FORMAT { - return Err(StorageError::Init(format!( - "Unsupported portable archive format '{}'", - archive.archive_format - ))); - } - if archive.mode != "exact" { - return Err(StorageError::Init(format!( - "Unsupported portable archive mode '{}'", - archive.mode - ))); - } - - let mut seen_tables = std::collections::HashSet::new(); - let mut tables_by_name = std::collections::HashMap::new(); - for table in &archive.tables { - if !PORTABLE_TABLES.contains(&table.name.as_str()) { - return Err(StorageError::Init(format!( - "Portable archive contains unsupported table '{}'", - table.name - ))); - } - if !seen_tables.insert(table.name.as_str()) { - return Err(StorageError::Init(format!( - "Portable archive contains duplicate table '{}'", - table.name - ))); - } - tables_by_name.insert(table.name.as_str(), table); - } - - let mut report = PortableImportReport { - tables_imported: 0, - rows_imported: 0, - tables_skipped: 0, - fts_rebuilt: false, - rows_inserted: 0, - rows_updated: 0, - rows_skipped: 0, - rows_deleted: 0, - conflicts_kept_local: 0, - }; - - { - let mut writer = self - .writer - .lock() - .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; - - let current_schema = Self::current_schema_version(&writer)?; - if archive.schema_version > current_schema { - return Err(StorageError::Init(format!( - "Archive schema version {} is newer than this Vestige database schema {}", - archive.schema_version, current_schema - ))); - } - - match mode { - PortableImportMode::EmptyOnly => { - Self::ensure_portable_import_target_empty(&writer)? - } - PortableImportMode::Merge => {} - } - - let tx = writer.transaction()?; - let mut merge_state = PortableMergeState::default(); - - for table_name in PORTABLE_TABLES { - let Some(table) = tables_by_name.get(table_name) else { - continue; - }; - - if !Self::table_exists(&tx, table_name)? { - report.tables_skipped += 1; - continue; - } - - if mode == PortableImportMode::Merge { - Self::merge_portable_table( - &tx, - table_name, - table, - &mut report, - &mut merge_state, - )?; - report.tables_imported += 1; - continue; - } - - let target_columns = Self::table_columns(&tx, table_name)?; - let mut insert_columns = Vec::new(); - let mut source_indexes = Vec::new(); - - for (idx, column) in table.columns.iter().enumerate() { - if target_columns.iter().any(|target| target == column) { - insert_columns.push(column.clone()); - source_indexes.push(idx); - } - } - - if insert_columns.is_empty() { - report.tables_skipped += 1; - continue; - } - - let quoted_table = Self::quote_ident(table_name); - let quoted_columns = insert_columns - .iter() - .map(|column| Self::quote_ident(column)) - .collect::>() - .join(", "); - let placeholders = std::iter::repeat_n("?", insert_columns.len()) - .collect::>() - .join(", "); - let verb = if *table_name == "fsrs_config" { - "INSERT OR REPLACE" - } else { - "INSERT" - }; - let sql = format!( - "{} INTO {} ({}) VALUES ({})", - verb, quoted_table, quoted_columns, placeholders - ); - - for row in &table.rows { - if row.len() != table.columns.len() { - return Err(StorageError::Init(format!( - "Portable archive row in table '{}' has {} values for {} columns", - table_name, - row.len(), - table.columns.len() - ))); - } - - let values = source_indexes - .iter() - .map(|idx| row[*idx].to_sql_value()) - .collect::, _>>() - .map_err(|e| { - StorageError::Init(format!("Invalid portable value: {}", e)) - })?; - tx.execute(&sql, params_from_iter(values))?; - report.rows_imported += 1; - report.rows_inserted += 1; - } - - report.tables_imported += 1; - } - - if Self::table_exists(&tx, "knowledge_fts")? { - tx.execute( - "INSERT INTO knowledge_fts(knowledge_fts) VALUES('rebuild')", - [], - )?; - report.fts_rebuilt = true; - } - - tx.commit()?; - } - - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - self.load_embeddings_into_index()?; - - Ok(report) - } - - /// Read and import an exact portable archive JSON file. - pub fn import_portable_archive_from_path( - &self, - path: &std::path::Path, - mode: PortableImportMode, - ) -> Result { - let file = std::fs::File::open(path)?; - let archive: PortableArchive = serde_json::from_reader(file) - .map_err(|e| StorageError::Init(format!("Failed to parse portable archive: {}", e)))?; - self.import_portable_archive(&archive, mode) - } - - /// Synchronize this database with a pluggable portable archive backend. - /// - /// Sync is pull-merge-push: - /// 1. read remote archive if present, - /// 2. merge it into the local database using tombstones and conflict rules, - /// 3. export the merged local database, - /// 4. write the archive back through the backend. - pub fn sync_portable_archive( - &self, - backend: &B, - ) -> Result { - let (pulled, pull) = match backend.read_archive()? { - Some(remote) => ( - true, - Some(self.import_portable_archive(&remote, PortableImportMode::Merge)?), - ), - None => (false, None), - }; - - let archive = self.export_portable_archive()?; - let pushed_tables = archive.tables.len(); - let pushed_rows = archive.total_rows(); - let archive_format = archive.archive_format.clone(); - backend.write_archive(&archive)?; - - Ok(PortableSyncReport { - backend: backend.label(), - pulled, - pull, - pushed_tables, - pushed_rows, - archive_format, - }) - } - - /// Synchronize this database with a file-backed portable archive. - pub fn sync_portable_archive_file(&self, path: &std::path::Path) -> Result { - let backend = FilePortableSyncBackend::new(path); - self.sync_portable_archive(&backend) - } - - /// Synchronize this database with the hosted Vestige Cloud managed-sync - /// service. `endpoint` is the base URL (e.g. `https://sync.vestige.dev`) and - /// `sync_key` is the per-user key issued at purchase. Pull-merge-push is - /// identical to file sync — only the transport differs. - /// - /// When `encryption_key` is `Some`, the archive is encrypted client-side - /// (XChaCha20-Poly1305) before upload, so the server only stores ciphertext - /// (zero-knowledge). The passphrase never leaves this process. - #[cfg(feature = "cloud-sync")] - pub fn sync_portable_archive_cloud( - &self, - endpoint: &str, - sync_key: &str, - encryption_key: Option, - ) -> Result { - let backend = super::cloud_sync::HttpPortableSyncBackend::new_with_encryption( - endpoint, - sync_key, - encryption_key, - )?; - self.sync_portable_archive(&backend) - } - - fn merge_portable_table( - tx: &rusqlite::Transaction<'_>, - table_name: &str, - table: &PortableTable, - report: &mut PortableImportReport, - state: &mut PortableMergeState, - ) -> Result<()> { - match table_name { - "sync_tombstones" => Self::merge_sync_tombstones(tx, table, report), - "knowledge_nodes" => Self::merge_knowledge_nodes(tx, table, report, state), - "memory_access_log" - | "state_transitions" - | "consolidation_history" - | "dream_history" - | "retention_snapshots" => Self::merge_append_only_table(tx, table_name, table, report), - "composition_events" | "composition_outcomes" => { - Self::merge_keyed_table(tx, table_name, table, &["id"], report, state) - } - "composition_members" => Self::merge_keyed_table( - tx, - table_name, - table, - &["event_id", "memory_id", "role"], - report, - state, - ), - "node_embeddings" => { - Self::merge_keyed_table(tx, table_name, table, &["node_id"], report, state) - } - "fsrs_cards" | "memory_states" => { - Self::merge_keyed_table(tx, table_name, table, &["memory_id"], report, state) - } - "deletion_tombstones" => Self::merge_deletion_tombstones(tx, table, report), - "memory_connections" => Self::merge_keyed_table( - tx, - table_name, - table, - &["source_id", "target_id"], - report, - state, - ), - "intentions" | "insights" | "sessions" => { - Self::merge_keyed_table(tx, table_name, table, &["id"], report, state) - } - "fsrs_config" => { - Self::merge_keyed_table(tx, table_name, table, &["key"], report, state) - } - _ => { - report.tables_skipped += 1; - Ok(()) - } - } - } - - fn merge_knowledge_nodes( - tx: &rusqlite::Transaction<'_>, - table: &PortableTable, - report: &mut PortableImportReport, - state: &mut PortableMergeState, - ) -> Result<()> { - for row in &table.rows { - let Some(id) = Self::portable_text(table, row, "id") else { - report.rows_skipped += 1; - continue; - }; - let incoming_updated = Self::portable_timestamp(table, row, "updated_at"); - - if let Some(deleted_at) = Self::tombstone_timestamp(tx, "knowledge_nodes", id)? - && incoming_updated.is_some_and(|updated| deleted_at >= updated) - { - report.conflicts_kept_local += 1; - report.rows_skipped += 1; - continue; - } - - let existing_updated: Option = tx - .query_row( - "SELECT updated_at FROM knowledge_nodes WHERE id = ?1", - params![id], - |row| row.get(0), - ) - .optional()?; - - if let (Some(existing), Some(incoming)) = ( - existing_updated - .as_deref() - .and_then(Self::parse_rfc3339_opt), - incoming_updated, - ) && existing > incoming - { - state.locally_newer_nodes.insert(id.to_string()); - report.conflicts_kept_local += 1; - report.rows_skipped += 1; - continue; - } - - let affected = Self::insert_or_replace_row(tx, "knowledge_nodes", table, row)?; - report.rows_imported += 1; - if affected == MergeWrite::Inserted { - report.rows_inserted += 1; - } else { - report.rows_updated += 1; - } - } - Ok(()) - } - - fn merge_sync_tombstones( - tx: &rusqlite::Transaction<'_>, - table: &PortableTable, - report: &mut PortableImportReport, - ) -> Result<()> { - for row in &table.rows { - let Some(table_name) = Self::portable_text(table, row, "table_name") else { - report.rows_skipped += 1; - continue; - }; - let Some(row_id) = Self::portable_text(table, row, "row_id") else { - report.rows_skipped += 1; - continue; - }; - let incoming_deleted_at = Self::portable_timestamp(table, row, "deleted_at"); - let incoming_reason = Self::portable_text(table, row, "reason").map(ToOwned::to_owned); - - let existing_tombstone: Option<(String, Option)> = tx - .query_row( - "SELECT deleted_at, reason FROM sync_tombstones WHERE table_name = ?1 AND row_id = ?2", - params![table_name, row_id], - |row| Ok((row.get(0)?, row.get(1)?)), - ) - .optional()?; - let existing_deleted_at = existing_tombstone - .as_ref() - .and_then(|(deleted_at, _)| Self::parse_rfc3339_opt(deleted_at)); - let incoming_wins = match (existing_deleted_at, incoming_deleted_at) { - (Some(existing), Some(incoming)) => incoming >= existing, - (Some(_), None) => false, - (None, _) => true, - }; - - let (effective_deleted_at, effective_reason) = if incoming_wins { - let affected = Self::insert_or_replace_row(tx, "sync_tombstones", table, row)?; - report.rows_imported += 1; - if affected == MergeWrite::Inserted { - report.rows_inserted += 1; - } else { - report.rows_updated += 1; - } - (incoming_deleted_at, incoming_reason) - } else { - report.rows_skipped += 1; - ( - existing_deleted_at, - existing_tombstone.and_then(|(_, reason)| reason), - ) - }; - - if table_name == "knowledge_nodes" { - let local_updated: Option = tx - .query_row( - "SELECT updated_at FROM knowledge_nodes WHERE id = ?1", - params![row_id], - |row| row.get(0), - ) - .optional()?; - let should_delete = match ( - local_updated.as_deref().and_then(Self::parse_rfc3339_opt), - effective_deleted_at, - ) { - (Some(local), Some(deleted)) => { - effective_reason.as_deref() == Some("purge_node") || deleted >= local - } - (Some(_), None) => true, - (None, _) => false, - }; - if should_delete { - tx.execute( - "UPDATE composition_members SET preview = NULL WHERE memory_id = ?1", - params![row_id], - )?; - let deleted = - tx.execute("DELETE FROM knowledge_nodes WHERE id = ?1", params![row_id])?; - report.rows_deleted += deleted; - } - } - } - Ok(()) - } - - fn merge_deletion_tombstones( - tx: &rusqlite::Transaction<'_>, - table: &PortableTable, - report: &mut PortableImportReport, - ) -> Result<()> { - for row in &table.rows { - let Some(memory_id) = Self::portable_text(table, row, "memory_id") else { - report.rows_skipped += 1; - continue; - }; - let incoming_deleted_at = Self::portable_timestamp(table, row, "deleted_at"); - let existing_deleted_at: Option = tx - .query_row( - "SELECT deleted_at FROM deletion_tombstones WHERE memory_id = ?1", - params![memory_id], - |row| row.get(0), - ) - .optional()?; - - if let (Some(existing), Some(incoming)) = ( - existing_deleted_at - .as_deref() - .and_then(Self::parse_rfc3339_opt), - incoming_deleted_at, - ) && existing > incoming - { - report.rows_skipped += 1; - continue; - } - - let affected = Self::insert_or_replace_row(tx, "deletion_tombstones", table, row)?; - report.rows_imported += 1; - if affected == MergeWrite::Inserted { - report.rows_inserted += 1; - } else { - report.rows_updated += 1; - } - } - Ok(()) - } - - fn merge_keyed_table( - tx: &rusqlite::Transaction<'_>, - table_name: &str, - table: &PortableTable, - key_columns: &[&str], - report: &mut PortableImportReport, - state: &PortableMergeState, - ) -> Result<()> { - for row in &table.rows { - if !Self::parent_rows_exist(tx, table_name, table, row)? { - report.rows_skipped += 1; - continue; - } - if key_columns - .iter() - .any(|column| Self::portable_value(table, row, column).is_none()) - { - report.rows_skipped += 1; - continue; - } - if Self::row_references_locally_newer_node(table_name, table, row, state) { - report.conflicts_kept_local += 1; - report.rows_skipped += 1; - continue; - } - let affected = Self::insert_or_replace_row(tx, table_name, table, row)?; - report.rows_imported += 1; - if affected == MergeWrite::Inserted { - report.rows_inserted += 1; - } else { - report.rows_updated += 1; - } - } - Ok(()) - } - - fn row_references_locally_newer_node( - table_name: &str, - table: &PortableTable, - row: &[PortableValue], - state: &PortableMergeState, - ) -> bool { - match table_name { - "node_embeddings" => Self::portable_text(table, row, "node_id") - .is_some_and(|id| state.locally_newer_nodes.contains(id)), - "fsrs_cards" | "memory_states" => Self::portable_text(table, row, "memory_id") - .is_some_and(|id| state.locally_newer_nodes.contains(id)), - "memory_connections" => { - Self::portable_text(table, row, "source_id") - .is_some_and(|id| state.locally_newer_nodes.contains(id)) - || Self::portable_text(table, row, "target_id") - .is_some_and(|id| state.locally_newer_nodes.contains(id)) - } - _ => false, - } - } - - fn merge_append_only_table( - tx: &rusqlite::Transaction<'_>, - table_name: &str, - table: &PortableTable, - report: &mut PortableImportReport, - ) -> Result<()> { - for row in &table.rows { - if !Self::parent_rows_exist(tx, table_name, table, row)? { - report.rows_skipped += 1; - continue; - } - - let insert_columns: Vec = table - .columns - .iter() - .filter(|column| column.as_str() != "id") - .cloned() - .collect(); - if insert_columns.is_empty() { - report.rows_skipped += 1; - continue; - } - - let values = Self::row_values_for_columns(table, row, &insert_columns)?; - if Self::row_exists_by_values(tx, table_name, &insert_columns, &values)? { - report.rows_skipped += 1; - continue; - } - - Self::insert_row_with_columns(tx, table_name, &insert_columns, values)?; - report.rows_imported += 1; - report.rows_inserted += 1; - } - Ok(()) - } - - fn parent_rows_exist( - tx: &rusqlite::Transaction<'_>, - table_name: &str, - table: &PortableTable, - row: &[PortableValue], - ) -> Result { - match table_name { - "node_embeddings" | "memory_access_log" => Self::portable_text(table, row, "node_id") - .map(|id| Self::node_exists(tx, id)) - .transpose() - .map(|v| v.unwrap_or(false)), - "fsrs_cards" | "memory_states" | "state_transitions" => { - Self::portable_text(table, row, "memory_id") - .map(|id| Self::node_exists(tx, id)) - .transpose() - .map(|v| v.unwrap_or(false)) - } - "memory_connections" => { - let source_exists = Self::portable_text(table, row, "source_id") - .map(|id| Self::node_exists(tx, id)) - .transpose()? - .unwrap_or(false); - let target_exists = Self::portable_text(table, row, "target_id") - .map(|id| Self::node_exists(tx, id)) - .transpose()? - .unwrap_or(false); - Ok(source_exists && target_exists) - } - "composition_members" => { - let event_exists = Self::portable_text(table, row, "event_id") - .map(|id| Self::composition_event_exists(tx, id)) - .transpose()? - .unwrap_or(false); - Ok(event_exists) - } - "composition_outcomes" => { - let event_exists = Self::portable_text(table, row, "event_id") - .map(|id| Self::composition_event_exists(tx, id)) - .transpose()? - .unwrap_or(false); - Ok(event_exists) - } - _ => Ok(true), - } - } - - fn insert_or_replace_row( - tx: &rusqlite::Transaction<'_>, - table_name: &str, - table: &PortableTable, - row: &[PortableValue], - ) -> Result { - let key_exists = Self::merge_row_exists(tx, table_name, table, row)?; - let values = Self::row_values_for_columns(table, row, &table.columns)?; - Self::upsert_row_with_columns(tx, table_name, &table.columns, values)?; - Ok(if key_exists { - MergeWrite::Updated - } else { - MergeWrite::Inserted - }) - } - - fn merge_key_columns(table_name: &str) -> &'static [&'static str] { - match table_name { - "knowledge_nodes" | "intentions" | "insights" | "sessions" => &["id"], - "composition_events" | "composition_outcomes" => &["id"], - "composition_members" => &["event_id", "memory_id", "role"], - "node_embeddings" => &["node_id"], - "fsrs_cards" | "memory_states" | "deletion_tombstones" => &["memory_id"], - "memory_connections" => &["source_id", "target_id"], - "fsrs_config" => &["key"], - "sync_tombstones" => &["table_name", "row_id"], - _ => &[], - } - } - - fn upsert_row_with_columns( - tx: &rusqlite::Transaction<'_>, - table_name: &str, - columns: &[String], - values: Vec, - ) -> Result<()> { - let key_columns = Self::merge_key_columns(table_name); - if key_columns.is_empty() { - return Self::insert_row_with_columns(tx, table_name, columns, values); - } - - let quoted_table = Self::quote_ident(table_name); - let quoted_columns = columns - .iter() - .map(|column| Self::quote_ident(column)) - .collect::>() - .join(", "); - let placeholders = std::iter::repeat_n("?", columns.len()) - .collect::>() - .join(", "); - let conflict_target = key_columns - .iter() - .map(|column| Self::quote_ident(column)) - .collect::>() - .join(", "); - let update_columns = columns - .iter() - .filter(|column| !key_columns.iter().any(|key| key == &column.as_str())) - .map(|column| { - let quoted = Self::quote_ident(column); - format!("{quoted} = excluded.{quoted}") - }) - .collect::>(); - - let conflict_action = if update_columns.is_empty() { - "DO NOTHING".to_string() - } else { - format!("DO UPDATE SET {}", update_columns.join(", ")) - }; - - let sql = format!( - "INSERT INTO {} ({}) VALUES ({}) ON CONFLICT({}) {}", - quoted_table, quoted_columns, placeholders, conflict_target, conflict_action - ); - tx.execute(&sql, params_from_iter(values))?; - Ok(()) - } - - fn insert_row_with_columns( - tx: &rusqlite::Transaction<'_>, - table_name: &str, - columns: &[String], - values: Vec, - ) -> Result<()> { - let quoted_table = Self::quote_ident(table_name); - let quoted_columns = columns - .iter() - .map(|column| Self::quote_ident(column)) - .collect::>() - .join(", "); - let placeholders = std::iter::repeat_n("?", columns.len()) - .collect::>() - .join(", "); - let sql = format!( - "INSERT OR REPLACE INTO {} ({}) VALUES ({})", - quoted_table, quoted_columns, placeholders - ); - tx.execute(&sql, params_from_iter(values))?; - Ok(()) - } - - fn merge_row_exists( - tx: &rusqlite::Transaction<'_>, - table_name: &str, - table: &PortableTable, - row: &[PortableValue], - ) -> Result { - let key_columns = Self::merge_key_columns(table_name); - if key_columns.is_empty() { - return Ok(false); - } - let mut columns = Vec::new(); - for key in key_columns { - columns.push((*key).to_string()); - } - let values = Self::row_values_for_columns(table, row, &columns)?; - Self::row_exists_by_values(tx, table_name, &columns, &values) - } - - fn row_exists_by_values( - tx: &rusqlite::Transaction<'_>, - table_name: &str, - columns: &[String], - values: &[Value], - ) -> Result { - let quoted_table = Self::quote_ident(table_name); - let where_clause = columns - .iter() - .map(|column| format!("{} IS ?", Self::quote_ident(column))) - .collect::>() - .join(" AND "); - let sql = format!( - "SELECT COUNT(*) FROM {} WHERE {}", - quoted_table, where_clause - ); - let count: i64 = tx.query_row(&sql, params_from_iter(values.iter()), |row| row.get(0))?; - Ok(count > 0) - } - - fn row_values_for_columns( - table: &PortableTable, - row: &[PortableValue], - columns: &[String], - ) -> Result> { - columns - .iter() - .map(|column| { - Self::portable_value(table, row, column) - .ok_or_else(|| { - StorageError::Init(format!( - "Portable archive row in table '{}' is missing column '{}'", - table.name, column - )) - })? - .to_sql_value() - .map_err(|e| StorageError::Init(format!("Invalid portable value: {}", e))) - }) - .collect() - } - - fn portable_value<'a>( - table: &PortableTable, - row: &'a [PortableValue], - column: &str, - ) -> Option<&'a PortableValue> { - table - .columns - .iter() - .position(|name| name == column) - .and_then(|idx| row.get(idx)) - } - - fn portable_text<'a>( - table: &PortableTable, - row: &'a [PortableValue], - column: &str, - ) -> Option<&'a str> { - match Self::portable_value(table, row, column) { - Some(PortableValue::Text(value)) => Some(value.as_str()), - _ => None, - } - } - - fn portable_timestamp( - table: &PortableTable, - row: &[PortableValue], - column: &str, - ) -> Option> { - Self::portable_text(table, row, column).and_then(Self::parse_rfc3339_opt) - } - - fn parse_rfc3339_opt(value: &str) -> Option> { - DateTime::parse_from_rfc3339(value) - .map(|dt| dt.with_timezone(&Utc)) - .ok() - } - - fn tombstone_timestamp( - tx: &rusqlite::Transaction<'_>, - table_name: &str, - row_id: &str, - ) -> Result>> { - let deleted_at: Option = tx - .query_row( - "SELECT deleted_at FROM sync_tombstones WHERE table_name = ?1 AND row_id = ?2", - params![table_name, row_id], - |row| row.get(0), - ) - .optional()?; - Ok(deleted_at.as_deref().and_then(Self::parse_rfc3339_opt)) - } - - fn current_schema_version(conn: &Connection) -> Result { - let version: i64 = conn.query_row( - "SELECT COALESCE(MAX(version), 0) FROM schema_version", - [], - |row| row.get(0), - )?; - Ok(version as u32) - } - - fn ensure_portable_import_target_empty(conn: &Connection) -> Result<()> { - for table_name in PORTABLE_USER_DATA_TABLES { - if Self::table_exists(conn, table_name)? { - let count = Self::table_row_count(conn, table_name)?; - if count > 0 { - return Err(StorageError::Init(format!( - "Portable import requires an empty target database; table '{}' has {} rows", - table_name, count - ))); - } - } - } - Ok(()) - } - - fn table_exists(conn: &Connection, table_name: &str) -> Result { - let exists: i64 = conn.query_row( - "SELECT COUNT(*) FROM sqlite_master WHERE type IN ('table', 'view') AND name = ?1", - params![table_name], - |row| row.get(0), - )?; - Ok(exists > 0) - } - - fn table_row_count(conn: &Connection, table_name: &str) -> Result { - let sql = format!("SELECT COUNT(*) FROM {}", Self::quote_ident(table_name)); - Ok(conn.query_row(&sql, [], |row| row.get(0))?) - } - - fn table_columns(conn: &Connection, table_name: &str) -> Result> { - let sql = format!("PRAGMA table_info({})", Self::quote_ident(table_name)); - let mut stmt = conn.prepare(&sql)?; - let rows = stmt.query_map([], |row| row.get::<_, String>(1))?; - - let mut columns = Vec::new(); - for row in rows { - columns.push(row?); - } - Ok(columns) - } - - fn portable_value_from_ref(value: ValueRef<'_>) -> rusqlite::Result { - Ok(match value { - ValueRef::Null => PortableValue::Null, - ValueRef::Integer(value) => PortableValue::Integer(value), - ValueRef::Real(value) => PortableValue::Real(value), - ValueRef::Text(value) => PortableValue::Text( - std::str::from_utf8(value) - .map_err(|e| { - rusqlite::Error::FromSqlConversionFailure(0, Type::Text, Box::new(e)) - })? - .to_string(), - ), - ValueRef::Blob(value) => PortableValue::Blob(encode_hex(value)), - }) - } - - fn quote_ident(identifier: &str) -> String { - format!("\"{}\"", identifier.replace('"', "\"\"")) - } - - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - fn embedding_model_matches_active(stored_model: &str, active_model: &str) -> bool { - if stored_model == active_model { - return true; - } - - let stored = stored_model.to_ascii_lowercase(); - let active = active_model.to_ascii_lowercase(); - - if active.contains("qwen3") { - return stored.contains("qwen3"); - } - - if active.contains("nomic-embed-text-v1.5") { - return stored.contains("nomic") && stored.contains("v1.5"); - } - - false - } - - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - fn embedding_model_supports_matryoshka(model_name: &str) -> bool { - let model = model_name.to_ascii_lowercase(); - model.contains("nomic") || model.contains("qwen3") - } - - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - fn embedding_vector_for_active_model( - embedding_bytes: &[u8], - stored_model: &str, - active_model: &str, - ) -> Option> { - if !Self::embedding_model_matches_active(stored_model, active_model) { - return None; - } - - let embedding = Embedding::from_bytes(embedding_bytes)?; - if embedding.dimensions == EMBEDDING_DIMENSIONS { - Some(embedding.vector) - } else if Self::embedding_model_supports_matryoshka(stored_model) { - Some(matryoshka_truncate(embedding.vector)) - } else { - None - } - } - - #[cfg(feature = "embeddings")] - fn active_embedding_model_like_pattern(active_model: &str) -> String { - let active = active_model.to_ascii_lowercase(); - if active.contains("qwen3") { - "%qwen3%".to_string() - } else if active.contains("nomic-embed-text-v1.5") { - "%nomic%v1.5%".to_string() - } else { - active_model.to_string() - } - } - // ======================================================================== // STATE TRANSITIONS (Audit Trail) // ======================================================================== @@ -7526,13 +3953,6 @@ impl SqliteMemoryStore { .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; // VACUUM INTO doesn't support parameterized queries; escape single quotes reader.execute_batch(&format!("VACUUM INTO '{}'", path_str.replace('\'', "''")))?; - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let mut perms = std::fs::metadata(path)?.permissions(); - perms.set_mode(0o600); - std::fs::set_permissions(path, perms)?; - } Ok(()) } @@ -7657,7 +4077,8 @@ impl SqliteMemoryStore { pub fn gc_below_retention(&self, threshold: f64, min_age_days: i64) -> Result { let cutoff = (Utc::now() - Duration::days(min_age_days)).to_rfc3339(); - // Collect IDs first for sync tombstones and vector index cleanup. + // Collect IDs first for vector index cleanup + #[cfg(all(feature = "embeddings", feature = "vector-search"))] let doomed_ids: Vec = { let reader = self .reader @@ -7675,9 +4096,6 @@ impl SqliteMemoryStore { .writer .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; - for id in &doomed_ids { - Self::record_sync_tombstone(&writer, "knowledge_nodes", id, "gc_below_retention")?; - } let deleted = writer.execute( "DELETE FROM knowledge_nodes WHERE retention_strength < ?1 AND created_at < ?2", params![threshold, cutoff], @@ -7687,8 +4105,7 @@ impl SqliteMemoryStore { // Clean up vector index #[cfg(all(feature = "embeddings", feature = "vector-search"))] if deleted > 0 - && let Some(index) = self.vector_index.as_ref() - && let Ok(mut index) = index.lock() + && let Ok(mut index) = self.vector_index.lock() { for id in &doomed_ids { let _ = index.remove(id); @@ -7895,2353 +4312,6 @@ impl SqliteMemoryStore { } Ok(result) } - - // ======================================================================== - // Merge / Supersede controls (Phase 3 — v2.1.25) - // - // Diff-previewed, confidence-gated, reversible, self-explaining - // combine/dedupe/supersede on a never-delete (bitemporal) store. - // Pure scoring/plan/op types live in `advanced::merge_supersede`. - // ======================================================================== - - /// Mark a memory protected (pinned) or unprotected. A protected memory can - /// never be auto-merged, superseded, or garbage-collected. - pub fn set_protected(&self, id: &str, protected: bool) -> Result<()> { - let writer = self - .writer - .lock() - .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; - let affected = writer.execute( - "UPDATE knowledge_nodes SET protected = ?1 WHERE id = ?2", - params![if protected { 1 } else { 0 }, id], - )?; - if affected == 0 { - return Err(StorageError::NotFound(id.to_string())); - } - Ok(()) - } - - /// Is this memory protected (pinned)? - pub fn is_protected(&self, id: &str) -> Result { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let v: Option = reader - .query_row( - "SELECT protected FROM knowledge_nodes WHERE id = ?1", - params![id], - |row| row.get(0), - ) - .optional()?; - match v { - Some(p) => Ok(p != 0), - None => Err(StorageError::NotFound(id.to_string())), - } - } - - /// Read the per-project merge policy (two Fellegi-Sunter thresholds + - /// auto_apply). Persisted in `fsrs_config` so it survives restarts without a - /// new table; falls back to defaults (env-overridable) when unset. - pub fn get_merge_policy(&self) -> Result { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let read_key = |key: &str| -> Option { - reader - .query_row( - "SELECT value FROM fsrs_config WHERE key = ?1", - params![key], - |row| row.get::<_, f64>(0), - ) - .optional() - .ok() - .flatten() - }; - let default = crate::advanced::MergePolicy::default(); - let env_f32 = |name: &str, fallback: f32| -> f32 { - std::env::var(name) - .ok() - .and_then(|v| v.parse::().ok()) - .unwrap_or(fallback) - }; - let match_threshold = read_key("merge_match_threshold") - .map(|v| v as f32) - .unwrap_or_else(|| env_f32("VESTIGE_MERGE_MATCH_THRESHOLD", default.match_threshold)); - let possible_threshold = read_key("merge_possible_threshold") - .map(|v| v as f32) - .unwrap_or_else(|| { - env_f32( - "VESTIGE_MERGE_POSSIBLE_THRESHOLD", - default.possible_threshold, - ) - }); - let auto_apply = match read_key("merge_auto_apply") { - Some(v) => v != 0.0, - None => std::env::var("VESTIGE_MERGE_AUTO_APPLY") - .ok() - .map(|v| v == "1" || v.eq_ignore_ascii_case("true")) - .unwrap_or(default.auto_apply), - }; - Ok(crate::advanced::MergePolicy::new( - match_threshold, - possible_threshold, - auto_apply, - )) - } - - /// Persist the per-project merge policy into `fsrs_config`. - pub fn set_merge_policy(&self, policy: crate::advanced::MergePolicy) -> Result<()> { - let writer = self - .writer - .lock() - .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; - let now = Utc::now().to_rfc3339(); - let put = |key: &str, value: f64| -> Result<()> { - writer.execute( - "INSERT OR REPLACE INTO fsrs_config (key, value, updated_at) VALUES (?1, ?2, ?3)", - params![key, value, now], - )?; - Ok(()) - }; - put("merge_match_threshold", policy.match_threshold as f64)?; - put("merge_possible_threshold", policy.possible_threshold as f64)?; - put( - "merge_auto_apply", - if policy.auto_apply { 1.0 } else { 0.0 }, - )?; - Ok(()) - } - - /// Surface likely duplicate/overlapping memory clusters with confidence - /// scores and the signals behind each (Fellegi-Sunter classified). - /// - /// Only clusters whose weakest pair scores at or above the policy's - /// `possible_threshold` are returned. Protected members are flagged so the - /// caller never auto-merges a pin. - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - pub fn merge_candidates( - &self, - policy: crate::advanced::MergePolicy, - limit: usize, - tag_filter: &[String], - ) -> Result> { - use crate::advanced::{MatchClass, MergeCandidate, score_pair}; - - let all_embeddings = self.get_all_embeddings()?; - if all_embeddings.is_empty() { - return Ok(vec![]); - } - - // Load nodes for metadata. Exclude already-superseded nodes — they are - // historical and must not be re-offered for merge. - let mut node_map: std::collections::HashMap = - std::collections::HashMap::new(); - let superseded: std::collections::HashSet = self.superseded_node_ids()?; - let protected: std::collections::HashSet = self.protected_node_ids()?; - - let mut offset = 0; - loop { - let batch = self.get_all_nodes(500, offset)?; - let n = batch.len(); - for node in batch { - node_map.insert(node.id.clone(), node); - } - if n < 500 { - break; - } - offset += 500; - } - - // Candidate embeddings, filtered by tag and excluding superseded. - let items: Vec<(String, Vec)> = all_embeddings - .into_iter() - .filter(|(id, _)| !superseded.contains(id)) - .filter(|(id, _)| { - if tag_filter.is_empty() { - return true; - } - node_map - .get(id) - .map(|n| tag_filter.iter().any(|t| n.tags.contains(t))) - .unwrap_or(false) - }) - .collect(); - - let n = items.len(); - if n > 2000 { - return Err(StorageError::Init(format!( - "Too many memories to scan ({n} with embeddings). Filter by tags to reduce scope." - ))); - } - - // Union-find clustering over pairs above the possible threshold. - let mut parent: Vec = (0..n).collect(); - fn find(parent: &mut [usize], x: usize) -> usize { - let mut root = x; - while parent[root] != root { - root = parent[root]; - } - let mut cur = x; - while parent[cur] != root { - let next = parent[cur]; - parent[cur] = root; - cur = next; - } - root - } - - // Best pair score per resulting cluster member, for the explanation. - let mut pair_score: std::collections::HashMap< - (usize, usize), - crate::advanced::MatchSignals, - > = std::collections::HashMap::new(); - - for i in 0..n { - for j in (i + 1)..n { - let sim = crate::cosine_similarity(&items[i].1, &items[j].1); - let (a_node, b_node) = (node_map.get(&items[i].0), node_map.get(&items[j].0)); - let signals = score_pair( - sim, - a_node.map(|n| n.tags.as_slice()).unwrap_or(&[]), - b_node.map(|n| n.tags.as_slice()).unwrap_or(&[]), - a_node.map(|n| n.content.as_str()).unwrap_or(""), - b_node.map(|n| n.content.as_str()).unwrap_or(""), - ); - if signals.combined_score >= policy.possible_threshold { - let ri = find(&mut parent, i); - let rj = find(&mut parent, j); - if ri != rj { - parent[ri] = rj; - } - pair_score.insert((i, j), signals); - } - } - } - - // Group indices by root. - let mut clusters: std::collections::HashMap> = - std::collections::HashMap::new(); - for i in 0..n { - let r = find(&mut parent, i); - clusters.entry(r).or_default().push(i); - } - - let mut out: Vec = Vec::new(); - for members in clusters.into_values() { - if members.len() < 2 { - continue; - } - // Cluster confidence = weakest recorded pair (the loosest link). - let mut min_score = 1.0f32; - let mut best_signals: Option = None; - for a in 0..members.len() { - for b in (a + 1)..members.len() { - let key = (members[a].min(members[b]), members[a].max(members[b])); - if let Some(sig) = pair_score.get(&key) { - if sig.combined_score < min_score { - min_score = sig.combined_score; - } - if best_signals - .as_ref() - .map(|s| sig.combined_score > s.combined_score) - .unwrap_or(true) - { - best_signals = Some(sig.clone()); - } - } - } - } - let signals = match best_signals { - Some(s) => s, - None => continue, - }; - - // Survivor = highest retention member. - let mut member_ids: Vec = - members.iter().map(|&idx| items[idx].0.clone()).collect(); - member_ids.sort_by(|a, b| { - let ra = node_map.get(a).map(|n| n.retention_strength).unwrap_or(0.0); - let rb = node_map.get(b).map(|n| n.retention_strength).unwrap_or(0.0); - rb.partial_cmp(&ra).unwrap_or(std::cmp::Ordering::Equal) - }); - let survivor_id = member_ids[0].clone(); - let has_protected_member = member_ids.iter().any(|id| protected.contains(id)); - let previews: Vec = member_ids - .iter() - .map(|id| { - node_map - .get(id) - .map(|n| preview(&n.content, 120)) - .unwrap_or_default() - }) - .collect(); - - let classification = match policy.classify(min_score) { - MatchClass::NonMatch => continue, - c => c, - }; - - out.push(MergeCandidate { - member_ids, - previews, - survivor_id, - confidence: min_score, - classification, - signals, - has_protected_member, - }); - } - - out.sort_by(|a, b| { - b.confidence - .partial_cmp(&a.confidence) - .unwrap_or(std::cmp::Ordering::Equal) - }); - out.truncate(limit); - Ok(out) - } - - /// IDs of nodes that have been bitemporally superseded (kept, but invalid). - pub fn superseded_node_ids(&self) -> Result> { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let mut stmt = - reader.prepare("SELECT id FROM knowledge_nodes WHERE superseded_by IS NOT NULL")?; - let rows = stmt.query_map([], |row| row.get::<_, String>(0))?; - let mut set = std::collections::HashSet::new(); - for r in rows { - set.insert(r?); - } - Ok(set) - } - - /// IDs of protected (pinned) nodes. - pub fn protected_node_ids(&self) -> Result> { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let mut stmt = reader.prepare("SELECT id FROM knowledge_nodes WHERE protected = 1")?; - let rows = stmt.query_map([], |row| row.get::<_, String>(0))?; - let mut set = std::collections::HashSet::new(); - for r in rows { - set.insert(r?); - } - Ok(set) - } - - /// Build a previewable MERGE plan (a diff) WITHOUT applying it. - /// - /// The survivor is the first id (or highest retention if unspecified). The - /// plan is persisted to `merge_plans` with status `pending` and returned for - /// inspection. Nothing about the nodes changes until `apply_plan`. - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - pub fn plan_merge( - &self, - member_ids: &[String], - survivor_id: Option<&str>, - policy: crate::advanced::MergePolicy, - ) -> Result { - use crate::advanced::{ - MatchClass, PlanKind, compose_merged_content, compose_merged_tags, score_pair, - }; - - if member_ids.len() < 2 { - return Err(StorageError::Init( - "plan_merge needs at least 2 member ids".into(), - )); - } - - let mut nodes: Vec = Vec::new(); - for id in member_ids { - let node = self - .get_node(id)? - .ok_or_else(|| StorageError::NotFound(id.clone()))?; - nodes.push(node); - } - - // Protected nodes can never be absorbed. They may only be the survivor. - let survivor = match survivor_id { - Some(s) => s.to_string(), - None => { - // highest retention - nodes - .iter() - .max_by(|a, b| { - a.retention_strength - .partial_cmp(&b.retention_strength) - .unwrap_or(std::cmp::Ordering::Equal) - }) - .map(|n| n.id.clone()) - .unwrap_or_else(|| member_ids[0].clone()) - } - }; - // The survivor MUST be one of the members. A caller-supplied survivor_id - // that isn't in member_ids (a typo/mixup through the plan_merge tool) - // otherwise sails through and panics at the `.find(...).unwrap()` below, - // taking down the request. Reject it with a clear error instead. - if !nodes.iter().any(|n| n.id == survivor) { - return Err(StorageError::Init(format!( - "survivor_id {survivor} is not among the member_ids being merged" - ))); - } - - for node in &nodes { - if node.id != survivor && self.is_protected(&node.id)? { - return Err(StorageError::Init(format!( - "Memory {} is protected and cannot be merged away. Unprotect it first or make it the survivor.", - node.id - ))); - } - } - - // Order: survivor first, then others. - nodes.sort_by_key(|n| if n.id == survivor { 0 } else { 1 }); - - let members: Vec<(String, String)> = nodes - .iter() - .map(|n| (n.id.clone(), n.content.clone())) - .collect(); - let result_content = compose_merged_content(&members); - let result_tags = - compose_merged_tags(&nodes.iter().map(|n| n.tags.clone()).collect::>()); - let result_source = nodes - .iter() - .find(|n| n.id == survivor) - .and_then(|n| n.source.clone()); - let invalidated_ids: Vec = nodes - .iter() - .filter(|n| n.id != survivor) - .map(|n| n.id.clone()) - .collect(); - - // Confidence = weakest pair survivor↔absorbed. - let survivor_node = nodes.iter().find(|n| n.id == survivor).unwrap(); - let mut min_score = 1.0f32; - let mut best_signals = score_pair( - 1.0, - &survivor_node.tags, - &survivor_node.tags, - &survivor_node.content, - &survivor_node.content, - ); - for node in nodes.iter().filter(|n| n.id != survivor) { - let sim = self.pair_similarity(&survivor, &node.id)?; - let sig = score_pair( - sim, - &survivor_node.tags, - &node.tags, - &survivor_node.content, - &node.content, - ); - if sig.combined_score < min_score { - min_score = sig.combined_score; - best_signals = sig; - } - } - let classification = policy.classify(min_score); - - let plan = crate::advanced::MergePlan { - id: uuid::Uuid::new_v4().to_string(), - kind: PlanKind::Merge, - survivor_id: survivor.clone(), - member_ids: member_ids.to_vec(), - result_content, - result_tags, - result_source, - invalidated_ids, - confidence: min_score, - classification, - signals: best_signals, - explanation: format!( - "Merge {} memories into {survivor} ({}). {} memory(ies) will be bitemporally invalidated (kept for audit, marked superseded_by={survivor}).", - member_ids.len(), - match classification { - MatchClass::Match => "strong duplicate", - MatchClass::Possible => "possible duplicate — review advised", - MatchClass::NonMatch => "weak match — review strongly advised", - }, - member_ids.len() - 1 - ), - }; - - self.persist_plan(&plan)?; - Ok(plan) - } - - /// Build a previewable SUPERSEDE plan: invalidate `old_id` in favour of - /// `new_id` (bitemporal, audit-preserving) WITHOUT applying it. - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - pub fn plan_supersede( - &self, - old_id: &str, - new_id: &str, - policy: crate::advanced::MergePolicy, - ) -> Result { - use crate::advanced::{PlanKind, score_pair}; - - let old = self - .get_node(old_id)? - .ok_or_else(|| StorageError::NotFound(old_id.to_string()))?; - let new = self - .get_node(new_id)? - .ok_or_else(|| StorageError::NotFound(new_id.to_string()))?; - - if self.is_protected(old_id)? { - return Err(StorageError::Init(format!( - "Memory {old_id} is protected and cannot be superseded. Unprotect it first." - ))); - } - - let sim = self.pair_similarity(old_id, new_id)?; - let signals = score_pair(sim, &old.tags, &new.tags, &old.content, &new.content); - let classification = policy.classify(signals.combined_score); - - let plan = crate::advanced::MergePlan { - id: uuid::Uuid::new_v4().to_string(), - kind: PlanKind::Supersede, - survivor_id: new_id.to_string(), - member_ids: vec![old_id.to_string(), new_id.to_string()], - result_content: new.content.clone(), - result_tags: new.tags.clone(), - result_source: new.source.clone(), - invalidated_ids: vec![old_id.to_string()], - confidence: signals.combined_score, - classification, - signals, - explanation: format!( - "Supersede {old_id} with {new_id}. {old_id} is kept and remains queryable for audit, but stamped valid_until=now and superseded_by={new_id} (invalidate, don't delete)." - ), - }; - - self.persist_plan(&plan)?; - Ok(plan) - } - - /// Cosine similarity between two nodes' stored embeddings (0 if missing). - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - fn pair_similarity(&self, a: &str, b: &str) -> Result { - let ea = self.get_node_embedding(a)?; - let eb = self.get_node_embedding(b)?; - match (ea, eb) { - (Some(ea), Some(eb)) => Ok(crate::cosine_similarity(&ea, &eb)), - _ => Ok(0.0), - } - } - - /// Persist a plan row (status pending). Idempotent on plan id. - fn persist_plan(&self, plan: &crate::advanced::MergePlan) -> Result<()> { - let writer = self - .writer - .lock() - .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; - let payload = serde_json::to_string(plan) - .map_err(|e| StorageError::Init(format!("plan serialize failed: {e}")))?; - let member_ids = serde_json::to_string(&plan.member_ids).unwrap_or_else(|_| "[]".into()); - writer.execute( - "INSERT OR REPLACE INTO merge_plans - (id, kind, status, created_at, applied_at, survivor_id, member_ids, confidence, classification, payload) - VALUES (?1, ?2, 'pending', ?3, NULL, ?4, ?5, ?6, ?7, ?8)", - params![ - plan.id, - plan.kind.as_str(), - Utc::now().to_rfc3339(), - plan.survivor_id, - member_ids, - plan.confidence as f64, - plan.classification.as_str(), - payload, - ], - )?; - Ok(()) - } - - /// Fetch a stored plan by id. - pub fn get_plan(&self, plan_id: &str) -> Result> { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let row: Option<(String, String)> = reader - .query_row( - "SELECT status, payload FROM merge_plans WHERE id = ?1", - params![plan_id], - |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)), - ) - .optional()?; - match row { - Some((_status, payload)) => { - let plan: crate::advanced::MergePlan = serde_json::from_str(&payload) - .map_err(|e| StorageError::Init(format!("plan deserialize failed: {e}")))?; - Ok(Some(plan)) - } - None => Ok(None), - } - } - - /// Plan status string (pending | applied | cancelled), if the plan exists. - pub fn plan_status(&self, plan_id: &str) -> Result> { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let status: Option = reader - .query_row( - "SELECT status FROM merge_plans WHERE id = ?1", - params![plan_id], - |row| row.get(0), - ) - .optional()?; - Ok(status) - } - - /// Execute a previously-generated plan by id. Everything it does is recorded - /// as a reversible [`MergeOperation`] in `merge_operations`. Returns the - /// recorded operation id. - /// - /// - **merge**: survivor content/tags are rewritten to the merged result; - /// each absorbed node is bitemporally invalidated (valid_until=now, - /// superseded_by=survivor) and kept queryable. - /// - **supersede**: old node is bitemporally invalidated in favour of new. - /// - /// `auto_apply` must be true in the policy to apply a `Match` plan without an - /// explicit `confirm`; non-`Match` plans always require `confirm=true`. - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - pub fn apply_plan( - &self, - plan_id: &str, - confirm: bool, - ) -> Result { - use crate::advanced::{MatchClass, PlanKind}; - - let plan = self - .get_plan(plan_id)? - .ok_or_else(|| StorageError::NotFound(format!("plan {plan_id}")))?; - - match self.plan_status(plan_id)?.as_deref() { - Some("applied") => { - return Err(StorageError::Init(format!( - "plan {plan_id} was already applied" - ))); - } - Some("cancelled") => { - return Err(StorageError::Init(format!("plan {plan_id} was cancelled"))); - } - _ => {} - } - - // Confirmation gate: only auto-applyable Match plans may skip confirm. - let needs_confirm = !(plan.classification == MatchClass::Match); - if needs_confirm && !confirm { - return Err(StorageError::Init(format!( - "plan {plan_id} is classified '{}' (confidence {:.3}) and requires confirm=true to apply", - plan.classification.as_str(), - plan.confidence - ))); - } - - let now = Utc::now(); - let op_id = uuid::Uuid::new_v4().to_string(); - - // Snapshot everything we need to undo, BEFORE mutating. - let mut undo = serde_json::Map::new(); - undo.insert("plan_id".into(), serde_json::json!(plan_id)); - undo.insert("kind".into(), serde_json::json!(plan.kind.as_str())); - undo.insert("survivor_id".into(), serde_json::json!(plan.survivor_id)); - - match plan.kind { - PlanKind::Merge => { - let survivor = self - .get_node(&plan.survivor_id)? - .ok_or_else(|| StorageError::NotFound(plan.survivor_id.clone()))?; - undo.insert( - "survivor_prev_content".into(), - serde_json::json!(survivor.content), - ); - undo.insert( - "survivor_prev_tags".into(), - serde_json::json!(survivor.tags), - ); - - // Capture prior valid_until / superseded_by of each absorbed node. - let mut absorbed = Vec::new(); - for id in &plan.invalidated_ids { - let (vu, sb) = self.read_bitemporal(id)?; - absorbed.push(serde_json::json!({ - "id": id, - "prev_valid_until": vu, - "prev_superseded_by": sb, - })); - } - undo.insert("absorbed".into(), serde_json::json!(absorbed)); - - // Apply: rewrite survivor, invalidate absorbed. - self.rewrite_survivor(&plan.survivor_id, &plan.result_content, &plan.result_tags)?; - for id in &plan.invalidated_ids { - self.invalidate_node(id, &plan.survivor_id, now)?; - } - } - PlanKind::Supersede => { - let old_id = &plan.member_ids[0]; - let (vu, sb) = self.read_bitemporal(old_id)?; - undo.insert( - "absorbed".into(), - serde_json::json!([{ - "id": old_id, - "prev_valid_until": vu, - "prev_superseded_by": sb, - }]), - ); - self.invalidate_node(old_id, &plan.survivor_id, now)?; - } - } - - // Record the reversible operation. - let affected: Vec = { - let mut v = vec![plan.survivor_id.clone()]; - v.extend(plan.invalidated_ids.clone()); - v - }; - let signals = serde_json::to_string(&plan.signals).unwrap_or_else(|_| "{}".into()); - { - let writer = self - .writer - .lock() - .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; - writer.execute( - "INSERT INTO merge_operations - (id, plan_id, op_type, status, created_at, reverted_at, reverts_op_id, - survivor_id, affected_ids, confidence, signals, reason, undo_payload) - VALUES (?1, ?2, ?3, 'applied', ?4, NULL, NULL, ?5, ?6, ?7, ?8, ?9, ?10)", - params![ - op_id, - plan_id, - plan.kind.as_str(), - now.to_rfc3339(), - plan.survivor_id, - serde_json::to_string(&affected).unwrap_or_else(|_| "[]".into()), - plan.confidence as f64, - signals, - plan.explanation, - serde_json::Value::Object(undo).to_string(), - ], - )?; - writer.execute( - "UPDATE merge_plans SET status = 'applied', applied_at = ?1 WHERE id = ?2", - params![now.to_rfc3339(), plan_id], - )?; - } - - self.read_operation(&op_id)? - .ok_or_else(|| StorageError::Init("operation vanished after insert".into())) - } - - /// Reverse a prior merge/supersede operation by id (the "memory reflog"). - /// Restores survivor content/tags and clears the bitemporal invalidation on - /// every node the operation touched, then records a compensating `undo` op. - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - pub fn merge_undo(&self, op_id: &str) -> Result { - let op = self - .read_operation(op_id)? - .ok_or_else(|| StorageError::NotFound(format!("operation {op_id}")))?; - if op.status == "reverted" { - return Err(StorageError::Init(format!( - "operation {op_id} was already reverted" - ))); - } - if op.op_type == "undo" { - return Err(StorageError::Init("cannot undo an undo operation".into())); - } - - let undo: serde_json::Value = { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let payload: String = reader.query_row( - "SELECT undo_payload FROM merge_operations WHERE id = ?1", - params![op_id], - |row| row.get(0), - )?; - serde_json::from_str(&payload) - .map_err(|e| StorageError::Init(format!("undo payload parse failed: {e}")))? - }; - - let kind = undo.get("kind").and_then(|v| v.as_str()).unwrap_or(""); - let survivor_id = undo - .get("survivor_id") - .and_then(|v| v.as_str()) - .unwrap_or_default() - .to_string(); - - // Restore survivor content/tags if this was a merge. - if kind == "merge" - && let (Some(content), Some(tags)) = ( - undo.get("survivor_prev_content").and_then(|v| v.as_str()), - undo.get("survivor_prev_tags").and_then(|v| v.as_array()), - ) - { - let tags: Vec = tags - .iter() - .filter_map(|t| t.as_str().map(|s| s.to_string())) - .collect(); - self.rewrite_survivor(&survivor_id, content, &tags)?; - } - - // Clear invalidation on every absorbed node, restoring prior values. - if let Some(absorbed) = undo.get("absorbed").and_then(|v| v.as_array()) { - for entry in absorbed { - let id = entry.get("id").and_then(|v| v.as_str()).unwrap_or_default(); - if id.is_empty() { - continue; - } - let prev_vu = entry.get("prev_valid_until").and_then(|v| v.as_str()); - let prev_sb = entry.get("prev_superseded_by").and_then(|v| v.as_str()); - self.restore_bitemporal(id, prev_vu, prev_sb)?; - } - } - - let now = Utc::now(); - let new_op_id = uuid::Uuid::new_v4().to_string(); - { - let writer = self - .writer - .lock() - .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; - // Mark original reverted. - writer.execute( - "UPDATE merge_operations SET status = 'reverted', reverted_at = ?1 WHERE id = ?2", - params![now.to_rfc3339(), op_id], - )?; - // Re-open the plan so it could be re-applied if desired. - if let Some(plan_id) = op.plan_id.as_deref() { - writer.execute( - "UPDATE merge_plans SET status = 'pending', applied_at = NULL WHERE id = ?1", - params![plan_id], - )?; - } - // Record compensating undo op. - writer.execute( - "INSERT INTO merge_operations - (id, plan_id, op_type, status, created_at, reverted_at, reverts_op_id, - survivor_id, affected_ids, confidence, signals, reason, undo_payload) - VALUES (?1, ?2, 'undo', 'applied', ?3, NULL, ?4, ?5, ?6, NULL, NULL, ?7, '{}')", - params![ - new_op_id, - op.plan_id, - now.to_rfc3339(), - op_id, - survivor_id, - serde_json::to_string(&op.affected_ids).unwrap_or_else(|_| "[]".into()), - format!("Reverted {} operation {op_id}", op.op_type), - ], - )?; - } - - self.read_operation(&new_op_id)? - .ok_or_else(|| StorageError::Init("undo operation vanished after insert".into())) - } - - /// List recent merge/supersede operations (the reflog), newest first. - pub fn list_merge_operations( - &self, - limit: usize, - ) -> Result> { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let mut stmt = reader.prepare( - "SELECT id, plan_id, op_type, status, created_at, reverted_at, reverts_op_id, - survivor_id, affected_ids, confidence, reason - FROM merge_operations ORDER BY created_at DESC LIMIT ?1", - )?; - let rows = stmt.query_map(params![limit as i64], Self::row_to_operation)?; - let mut out = Vec::new(); - for r in rows { - out.push(r?); - } - Ok(out) - } - - /// Read a single operation by id. - fn read_operation(&self, op_id: &str) -> Result> { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let op = reader - .query_row( - "SELECT id, plan_id, op_type, status, created_at, reverted_at, reverts_op_id, - survivor_id, affected_ids, confidence, reason - FROM merge_operations WHERE id = ?1", - params![op_id], - Self::row_to_operation, - ) - .optional()?; - Ok(op) - } - - fn row_to_operation(row: &rusqlite::Row) -> rusqlite::Result { - let affected: String = row.get("affected_ids")?; - let affected_ids: Vec = serde_json::from_str(&affected).unwrap_or_default(); - Ok(crate::advanced::MergeOperation { - id: row.get("id")?, - plan_id: row.get("plan_id").ok().flatten(), - op_type: row.get("op_type")?, - status: row.get("status")?, - created_at: row.get("created_at")?, - reverted_at: row.get("reverted_at").ok().flatten(), - reverts_op_id: row.get("reverts_op_id").ok().flatten(), - survivor_id: row.get("survivor_id").ok().flatten(), - affected_ids, - confidence: row - .get::<_, Option>("confidence") - .ok() - .flatten() - .map(|v| v as f32), - reason: row.get("reason").ok().flatten(), - }) - } - - /// Read (valid_until, superseded_by) for a node. - fn read_bitemporal(&self, id: &str) -> Result<(Option, Option)> { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let res = reader - .query_row( - "SELECT valid_until, superseded_by FROM knowledge_nodes WHERE id = ?1", - params![id], - |row| { - Ok(( - row.get::<_, Option>(0)?, - row.get::<_, Option>(1)?, - )) - }, - ) - .optional()?; - res.ok_or_else(|| StorageError::NotFound(id.to_string())) - } - - /// Bitemporally invalidate a node: stamp valid_until=now and superseded_by, - /// keeping the row fully queryable (Graphiti-style invalidate, don't delete). - fn invalidate_node(&self, id: &str, superseded_by: &str, now: DateTime) -> Result<()> { - let writer = self - .writer - .lock() - .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; - writer.execute( - "UPDATE knowledge_nodes - SET valid_until = ?1, superseded_by = ?2, updated_at = ?1 - WHERE id = ?3", - params![now.to_rfc3339(), superseded_by, id], - )?; - Ok(()) - } - - /// Restore a node's bitemporal columns (used by undo). - fn restore_bitemporal( - &self, - id: &str, - valid_until: Option<&str>, - superseded_by: Option<&str>, - ) -> Result<()> { - let writer = self - .writer - .lock() - .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; - writer.execute( - "UPDATE knowledge_nodes - SET valid_until = ?1, superseded_by = ?2, updated_at = ?3 - WHERE id = ?4", - params![valid_until, superseded_by, Utc::now().to_rfc3339(), id], - )?; - Ok(()) - } - - /// Rewrite a survivor's content and tags (used by merge apply + undo). - /// Content rewrite regenerates the embedding via `update_node_content`. - fn rewrite_survivor(&self, id: &str, content: &str, tags: &[String]) -> Result<()> { - self.update_node_content(id, content)?; - let tags_json = serde_json::to_string(tags).unwrap_or_else(|_| "[]".into()); - let writer = self - .writer - .lock() - .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; - writer.execute( - "UPDATE knowledge_nodes SET tags = ?1, updated_at = ?2 WHERE id = ?3", - params![tags_json, Utc::now().to_rfc3339(), id], - )?; - Ok(()) - } -} - -/// Truncate `content` to `max` chars on a char boundary, collapsing newlines. -fn preview(content: &str, max: usize) -> String { - let c = content.replace('\n', " "); - if c.len() > max { - format!("{}...", &c[..c.floor_char_boundary(max)]) - } else { - c - } -} - -// ============================================================================ -// LOCAL MEMORY STORE TRAIT IMPL -// ============================================================================ - -impl SqliteMemoryStore { - /// Convert a `KnowledgeNode` (plus optional embedding vector read separately) - /// into a `MemoryRecord` for the trait surface. - fn node_to_record( - node: KnowledgeNode, - embedding: Option>, - ) -> crate::storage::memory_store::MemoryRecord { - use crate::storage::memory_store::MemoryRecord; - let id = uuid::Uuid::parse_str(&node.id).unwrap_or_else(|_| uuid::Uuid::new_v4()); - MemoryRecord { - id, - domains: Vec::new(), - domain_scores: std::collections::HashMap::new(), - content: node.content, - node_type: node.node_type, - tags: node.tags, - embedding, - created_at: node.created_at, - updated_at: node.updated_at, - metadata: serde_json::json!({ - "source": node.source, - "stability": node.stability, - "difficulty": node.difficulty, - "reps": node.reps, - "lapses": node.lapses, - "retention_strength": node.retention_strength, - }), - } - } - - /// Read domains and domain_scores JSON columns for a node by id. - fn read_domain_columns( - &self, - id: &str, - ) -> (Vec, std::collections::HashMap) { - let reader = match self.reader.lock() { - Ok(r) => r, - Err(_) => return (Vec::new(), std::collections::HashMap::new()), - }; - let result = reader.query_row( - "SELECT domains, domain_scores FROM knowledge_nodes WHERE id = ?1", - rusqlite::params![id], - |row| { - let d: Option = row.get(0).ok().flatten(); - let ds: Option = row.get(1).ok().flatten(); - Ok((d, ds)) - }, - ); - match result { - Ok((d, ds)) => { - let domains: Vec = d - .and_then(|s| serde_json::from_str(&s).ok()) - .unwrap_or_default(); - let domain_scores: std::collections::HashMap = ds - .and_then(|s| serde_json::from_str(&s).ok()) - .unwrap_or_default(); - (domains, domain_scores) - } - Err(_) => (Vec::new(), std::collections::HashMap::new()), - } - } - - /// Enforce the registered embedding model. Returns `Ok(())` if: - /// - no vector is being written (`incoming.is_none()`) and nothing is registered - /// - the incoming signature matches the registered signature - /// - /// Auto-registers on the first embedded write. - fn enforce_model( - &self, - incoming: Option<&crate::storage::memory_store::ModelSignature>, - ) -> crate::storage::memory_store::MemoryStoreResult<()> { - use crate::storage::memory_store::{MemoryStoreError, ModelSignature}; - let Some(incoming) = incoming else { - return Ok(()); - }; - // Try from cache first - { - let guard = self - .registered_model - .read() - .map_err(|_| MemoryStoreError::Init("registered_model rwlock poisoned".into()))?; - if let Some(ref reg) = *guard { - if reg == incoming { - return Ok(()); - } - return Err(MemoryStoreError::ModelMismatch { - registered_name: reg.name.clone(), - registered_dim: reg.dimension, - registered_hash: reg.hash.clone(), - actual_name: incoming.name.clone(), - actual_dim: incoming.dimension, - actual_hash: incoming.hash.clone(), - }); - } - } - // Not registered yet -- auto-register - let now = Utc::now().to_rfc3339(); - let writer = self - .writer - .lock() - .map_err(|_| MemoryStoreError::Init("Writer lock poisoned".into()))?; - // Try INSERT OR IGNORE - writer.execute( - "INSERT OR IGNORE INTO embedding_model (id, name, dimension, hash, created_at) VALUES (1, ?1, ?2, ?3, ?4)", - rusqlite::params![incoming.name, incoming.dimension as i64, incoming.hash, now], - ).map_err(|e| MemoryStoreError::Backend(e.to_string()))?; - // Read back what was stored - let stored: Option = writer - .query_row( - "SELECT name, dimension, hash FROM embedding_model WHERE id = 1", - [], - |row| { - let name: String = row.get(0)?; - let dim: i64 = row.get(1)?; - let hash: String = row.get(2)?; - Ok(ModelSignature { - name, - dimension: dim as usize, - hash, - }) - }, - ) - .optional() - .map_err(|e| MemoryStoreError::Backend(e.to_string()))?; - drop(writer); - if let Some(stored) = stored { - if stored != *incoming { - return Err(MemoryStoreError::ModelMismatch { - registered_name: stored.name, - registered_dim: stored.dimension, - registered_hash: stored.hash, - actual_name: incoming.name.clone(), - actual_dim: incoming.dimension, - actual_hash: incoming.hash.clone(), - }); - } - // Populate cache - let mut guard = self - .registered_model - .write() - .map_err(|_| MemoryStoreError::Init("registered_model rwlock poisoned".into()))?; - *guard = Some(stored); - } - Ok(()) - } -} - -impl crate::storage::memory_store::MemoryStoreSend for SqliteMemoryStore { - async fn init(&self) -> crate::storage::memory_store::MemoryStoreResult<()> { - // Migrations run in `new`; this is a no-op for the SQLite backend. - Ok(()) - } - - async fn health_check( - &self, - ) -> crate::storage::memory_store::MemoryStoreResult - { - use crate::storage::memory_store::HealthStatus; - let reader = self.reader.lock().map_err(|_| { - crate::storage::memory_store::MemoryStoreError::Init("Reader lock poisoned".into()) - })?; - let ok: rusqlite::Result = reader.query_row("SELECT 1", [], |row| row.get(0)); - if ok.is_ok() { - Ok(HealthStatus::Healthy) - } else { - Ok(HealthStatus::Degraded { - reason: "SQLite connectivity check failed".to_string(), - }) - } - } - - async fn registered_model( - &self, - ) -> crate::storage::memory_store::MemoryStoreResult< - Option, - > { - use crate::storage::memory_store::MemoryStoreError; - // Check cache first - { - let guard = self - .registered_model - .read() - .map_err(|_| MemoryStoreError::Init("registered_model rwlock poisoned".into()))?; - if guard.is_some() { - return Ok(guard.clone()); - } - } - // Fall through to DB read - let reader = self - .reader - .lock() - .map_err(|_| MemoryStoreError::Init("Reader lock poisoned".into()))?; - let stored: Option = reader - .query_row( - "SELECT name, dimension, hash FROM embedding_model WHERE id = 1", - [], - |row| { - let name: String = row.get(0)?; - let dim: i64 = row.get(1)?; - let hash: String = row.get(2)?; - Ok(crate::storage::memory_store::ModelSignature { - name, - dimension: dim as usize, - hash, - }) - }, - ) - .optional() - .map_err(|e| MemoryStoreError::Backend(e.to_string()))?; - drop(reader); - // Populate cache if we read something - if stored.is_some() { - let mut guard = self - .registered_model - .write() - .map_err(|_| MemoryStoreError::Init("registered_model rwlock poisoned".into()))?; - *guard = stored.clone(); - } - Ok(stored) - } - - async fn register_model( - &self, - sig: &crate::storage::memory_store::ModelSignature, - ) -> crate::storage::memory_store::MemoryStoreResult<()> { - self.enforce_model(Some(sig)) - } - - async fn insert( - &self, - record: &crate::storage::memory_store::MemoryRecord, - ) -> crate::storage::memory_store::MemoryStoreResult { - use crate::storage::memory_store::{MemoryStoreError, ModelSignature}; - // Enforce model registry if embedding is provided - if let Some(vec) = &record.embedding { - // Derive a signature from metadata if present, or use a generic sentinel - let sig: Option = record - .metadata - .get("model_name") - .and_then(|v| v.as_str()) - .zip( - record - .metadata - .get("model_dim") - .and_then(|v| v.as_u64()) - .map(|d| d as usize), - ) - .zip(record.metadata.get("model_hash").and_then(|v| v.as_str())) - .map(|((name, dim), hash)| ModelSignature { - name: name.to_string(), - dimension: dim, - hash: hash.to_string(), - }); - if let Some(ref s) = sig { - self.enforce_model(Some(s))?; - if vec.len() != s.dimension { - return Err(MemoryStoreError::InvalidInput(format!( - "embedding length {} != registered dimension {}", - vec.len(), - s.dimension - ))); - } - } - } - // Insert directly using the record's own id so the caller-supplied UUID is - // preserved (unlike ingest() which always generates a fresh UUID). - let id_str = record.id.to_string(); - let now = chrono::Utc::now(); - let tags_json = serde_json::to_string(&record.tags).unwrap_or_else(|_| "[]".to_string()); - let domains_json = - serde_json::to_string(&record.domains).unwrap_or_else(|_| "[]".to_string()); - let scores_json = - serde_json::to_string(&record.domain_scores).unwrap_or_else(|_| "{}".to_string()); - let source: Option = record - .metadata - .get("source") - .and_then(|v| v.as_str()) - .map(str::to_string); - { - let writer = self - .writer - .lock() - .map_err(|_| MemoryStoreError::Init("Writer lock poisoned".into()))?; - writer - .execute( - "INSERT INTO knowledge_nodes ( - id, content, node_type, created_at, updated_at, last_accessed, - stability, difficulty, reps, lapses, learning_state, - storage_strength, retrieval_strength, retention_strength, - sentiment_score, sentiment_magnitude, next_review, scheduled_days, - source, tags, has_embedding, embedding_model, - domains, domain_scores - ) VALUES ( - ?1, ?2, ?3, ?4, ?5, ?6, - 1.0, 0.3, 0, 0, 'new', - 1.0, 1.0, 1.0, - 0.0, 0.0, ?7, 1, - ?8, ?9, 0, NULL, - ?10, ?11 - )", - rusqlite::params![ - id_str, - record.content, - record.node_type, - record.created_at.to_rfc3339(), - record.updated_at.to_rfc3339(), - now.to_rfc3339(), - (now + chrono::Duration::days(1)).to_rfc3339(), - source, - tags_json, - domains_json, - scores_json, - ], - ) - .map_err(|e| MemoryStoreError::Backend(e.to_string()))?; - } - Ok(record.id) - } - - async fn get( - &self, - id: uuid::Uuid, - ) -> crate::storage::memory_store::MemoryStoreResult< - Option, - > { - use crate::storage::memory_store::MemoryStoreError; - let node = self - .get_node(&id.to_string()) - .map_err(MemoryStoreError::from)?; - let Some(node) = node else { - return Ok(None); - }; - let (domains, domain_scores) = self.read_domain_columns(&id.to_string()); - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - let embedding = self.get_node_embedding(&id.to_string()).ok().flatten(); - #[cfg(not(all(feature = "embeddings", feature = "vector-search")))] - let embedding: Option> = None; - let mut rec = Self::node_to_record(node, embedding); - rec.domains = domains; - rec.domain_scores = domain_scores; - Ok(Some(rec)) - } - - async fn update( - &self, - record: &crate::storage::memory_store::MemoryRecord, - ) -> crate::storage::memory_store::MemoryStoreResult<()> { - use crate::storage::memory_store::MemoryStoreError; - self.update_node_content(&record.id.to_string(), &record.content) - .map_err(MemoryStoreError::from)?; - // Update domains/domain_scores - let domains_json = - serde_json::to_string(&record.domains).unwrap_or_else(|_| "[]".to_string()); - let scores_json = - serde_json::to_string(&record.domain_scores).unwrap_or_else(|_| "{}".to_string()); - let writer = self - .writer - .lock() - .map_err(|_| MemoryStoreError::Init("Writer lock poisoned".into()))?; - writer - .execute( - "UPDATE knowledge_nodes SET domains = ?1, domain_scores = ?2 WHERE id = ?3", - rusqlite::params![domains_json, scores_json, record.id.to_string()], - ) - .map_err(|e| MemoryStoreError::Backend(e.to_string()))?; - Ok(()) - } - - async fn delete(&self, id: uuid::Uuid) -> crate::storage::memory_store::MemoryStoreResult<()> { - use crate::storage::memory_store::MemoryStoreError; - self.delete_node(&id.to_string()) - .map_err(MemoryStoreError::from)?; - Ok(()) - } - - async fn search( - &self, - query: &crate::storage::memory_store::SearchQuery, - ) -> crate::storage::memory_store::MemoryStoreResult< - Vec, - > { - use crate::storage::memory_store::{MemoryStoreError, SearchResult}; - // For Phase 1 we delegate to hybrid_search or keyword_search based on what is provided. - let limit = if query.limit == 0 { 10 } else { query.limit }; - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - { - if let Some(ref text) = query.text { - let results = self - .hybrid_search(text, limit as i32, 0.3, 0.7) - .map_err(MemoryStoreError::from)?; - let out = results - .into_iter() - .map(|r| { - let (domains, domain_scores) = self.read_domain_columns(&r.node.id); - let mut rec = Self::node_to_record(r.node, None); - rec.domains = domains; - rec.domain_scores = domain_scores; - SearchResult { - score: r.combined_score as f64, - fts_score: r.keyword_score.map(|s| s as f64), - vector_score: r.semantic_score.map(|s| s as f64), - record: rec, - } - }) - .collect(); - return Ok(out); - } - } - #[cfg(not(all(feature = "embeddings", feature = "vector-search")))] - { - if let Some(ref text) = query.text { - // Use individual-term matching so multi-word queries find documents - // where all words appear anywhere (not necessarily as a phrase). - let nodes = self - .search_terms(text, limit as i32) - .map_err(MemoryStoreError::from)?; - let out = nodes - .into_iter() - .map(|node| { - let (domains, domain_scores) = self.read_domain_columns(&node.id); - let mut rec = Self::node_to_record(node, None); - rec.domains = domains; - rec.domain_scores = domain_scores; - SearchResult { - record: rec, - score: 1.0, - fts_score: Some(1.0), - vector_score: None, - } - }) - .collect(); - return Ok(out); - } - } - Ok(vec![]) - } - - async fn fts_search( - &self, - text: &str, - limit: usize, - ) -> crate::storage::memory_store::MemoryStoreResult< - Vec, - > { - use crate::storage::memory_store::{MemoryStoreError, SearchResult}; - // Use individual-term matching so multi-word queries find documents - // where all words appear anywhere (not necessarily as a phrase). - let nodes = self - .search_terms(text, limit as i32) - .map_err(MemoryStoreError::from)?; - let out = nodes - .into_iter() - .map(|node| { - let (domains, domain_scores) = self.read_domain_columns(&node.id); - let mut rec = Self::node_to_record(node, None); - rec.domains = domains; - rec.domain_scores = domain_scores; - SearchResult { - record: rec, - score: 1.0, - fts_score: Some(1.0), - vector_score: None, - } - }) - .collect(); - Ok(out) - } - - async fn vector_search( - &self, - embedding: &[f32], - limit: usize, - ) -> crate::storage::memory_store::MemoryStoreResult< - Vec, - > { - use crate::storage::memory_store::{MemoryStoreError, SearchResult}; - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - { - let Some(index) = self.vector_index.as_ref() else { - return Ok(vec![]); - }; - let index = index - .lock() - .map_err(|_| MemoryStoreError::Init("Vector index lock poisoned".into()))?; - let raw_results = index - .search_with_threshold(embedding, limit, 0.0_f32) - .map_err(|e| MemoryStoreError::Backend(e.to_string()))?; - drop(index); - let out = raw_results - .into_iter() - .filter_map(|(node_id, score)| { - let node = self.get_node(&node_id).ok().flatten()?; - let (domains, domain_scores) = self.read_domain_columns(&node_id); - let mut rec = Self::node_to_record(node, None); - rec.domains = domains; - rec.domain_scores = domain_scores; - Some(SearchResult { - record: rec, - score: score as f64, - fts_score: None, - vector_score: Some(score as f64), - }) - }) - .collect(); - Ok(out) - } - #[cfg(not(all(feature = "embeddings", feature = "vector-search")))] - { - let _ = (embedding, limit); - Ok(vec![]) - } - } - - async fn get_scheduling( - &self, - memory_id: uuid::Uuid, - ) -> crate::storage::memory_store::MemoryStoreResult< - Option, - > { - use crate::storage::memory_store::{MemoryStoreError, SchedulingState}; - let node = self - .get_node(&memory_id.to_string()) - .map_err(MemoryStoreError::from)?; - let Some(node) = node else { - return Ok(None); - }; - Ok(Some(SchedulingState { - memory_id, - stability: node.stability, - difficulty: node.difficulty, - retrievability: node.retention_strength, - last_review: Some(node.last_accessed), - next_review: node.next_review, - reps: node.reps as u32, - lapses: node.lapses as u32, - })) - } - - async fn update_scheduling( - &self, - state: &crate::storage::memory_store::SchedulingState, - ) -> crate::storage::memory_store::MemoryStoreResult<()> { - use crate::storage::memory_store::MemoryStoreError; - let writer = self - .writer - .lock() - .map_err(|_| MemoryStoreError::Init("Writer lock poisoned".into()))?; - let next_review_str = state.next_review.map(|dt| dt.to_rfc3339()); - let last_review_str = state.last_review.map(|dt| dt.to_rfc3339()); - writer - .execute( - "UPDATE knowledge_nodes SET stability=?1, difficulty=?2, retention_strength=?3, - last_accessed=?4, next_review=?5, reps=?6, lapses=?7 - WHERE id=?8", - rusqlite::params![ - state.stability, - state.difficulty, - state.retrievability, - last_review_str.as_deref().unwrap_or(""), - next_review_str, - state.reps as i64, - state.lapses as i64, - state.memory_id.to_string(), - ], - ) - .map_err(|e| MemoryStoreError::Backend(e.to_string()))?; - Ok(()) - } - - async fn get_due_memories( - &self, - before: chrono::DateTime, - limit: usize, - ) -> crate::storage::memory_store::MemoryStoreResult< - Vec<( - crate::storage::memory_store::MemoryRecord, - crate::storage::memory_store::SchedulingState, - )>, - > { - use crate::storage::memory_store::{MemoryStoreError, SchedulingState}; - let reader = self - .reader - .lock() - .map_err(|_| MemoryStoreError::Init("Reader lock poisoned".into()))?; - let before_str = before.to_rfc3339(); - let mut stmt = reader - .prepare( - "SELECT * FROM knowledge_nodes WHERE next_review <= ?1 ORDER BY next_review ASC LIMIT ?2", - ) - .map_err(|e| MemoryStoreError::Backend(e.to_string()))?; - let nodes: Vec = stmt - .query_map( - rusqlite::params![before_str, limit as i64], - Self::row_to_node, - ) - .map_err(|e| MemoryStoreError::Backend(e.to_string()))? - .collect::, _>>() - .map_err(|e| MemoryStoreError::Backend(e.to_string()))?; - drop(stmt); - drop(reader); - let out = nodes - .into_iter() - .map(|node| { - let id_str = node.id.clone(); - let (domains, domain_scores) = self.read_domain_columns(&id_str); - let id_uuid = - uuid::Uuid::parse_str(&id_str).unwrap_or_else(|_| uuid::Uuid::new_v4()); - let state = SchedulingState { - memory_id: id_uuid, - stability: node.stability, - difficulty: node.difficulty, - retrievability: node.retention_strength, - last_review: Some(node.last_accessed), - next_review: node.next_review, - reps: node.reps as u32, - lapses: node.lapses as u32, - }; - let mut rec = Self::node_to_record(node, None); - rec.domains = domains; - rec.domain_scores = domain_scores; - (rec, state) - }) - .collect(); - Ok(out) - } - - async fn add_edge( - &self, - edge: &crate::storage::memory_store::MemoryEdge, - ) -> crate::storage::memory_store::MemoryStoreResult<()> { - use crate::storage::memory_store::MemoryStoreError; - let conn = ConnectionRecord { - source_id: edge.source_id.to_string(), - target_id: edge.target_id.to_string(), - strength: edge.weight, - link_type: edge.edge_type.clone(), - created_at: edge.created_at, - last_activated: edge.created_at, - activation_count: 0, - }; - self.save_connection(&conn).map_err(MemoryStoreError::from) - } - - async fn get_edges( - &self, - node_id: uuid::Uuid, - edge_type: Option<&str>, - ) -> crate::storage::memory_store::MemoryStoreResult< - Vec, - > { - use crate::storage::memory_store::{MemoryEdge, MemoryStoreError}; - let conns = self - .get_connections_for_memory(&node_id.to_string()) - .map_err(MemoryStoreError::from)?; - let edges = conns - .into_iter() - .filter(|c| edge_type.is_none_or(|t| c.link_type == t)) - .filter_map(|c| { - let src = uuid::Uuid::parse_str(&c.source_id).ok()?; - let tgt = uuid::Uuid::parse_str(&c.target_id).ok()?; - Some(MemoryEdge { - source_id: src, - target_id: tgt, - edge_type: c.link_type, - weight: c.strength, - created_at: c.created_at, - }) - }) - .collect(); - Ok(edges) - } - - async fn remove_edge( - &self, - source: uuid::Uuid, - target: uuid::Uuid, - ) -> crate::storage::memory_store::MemoryStoreResult<()> { - use crate::storage::memory_store::MemoryStoreError; - let writer = self - .writer - .lock() - .map_err(|_| MemoryStoreError::Init("Writer lock poisoned".into()))?; - writer - .execute( - "DELETE FROM memory_connections WHERE source_id = ?1 AND target_id = ?2", - rusqlite::params![source.to_string(), target.to_string()], - ) - .map_err(|e| MemoryStoreError::Backend(e.to_string()))?; - Ok(()) - } - - async fn get_neighbors( - &self, - node_id: uuid::Uuid, - depth: usize, - ) -> crate::storage::memory_store::MemoryStoreResult< - Vec<(crate::storage::memory_store::MemoryRecord, f64)>, - > { - use crate::storage::memory_store::MemoryStoreError; - // Depth 0: return just the node itself if it exists. - if depth == 0 { - let node = self - .get_node(&node_id.to_string()) - .map_err(MemoryStoreError::from)? - .ok_or_else(|| MemoryStoreError::NotFound(node_id.to_string()))?; - let (domains, domain_scores) = self.read_domain_columns(&node_id.to_string()); - let mut rec = Self::node_to_record(node, None); - rec.domains = domains; - rec.domain_scores = domain_scores; - return Ok(vec![(rec, 1.0)]); - } - // BFS up to `depth` levels, capped at 256 nodes. - const MAX_NODES: usize = 256; - let mut visited: std::collections::HashMap = - std::collections::HashMap::new(); - let mut frontier: Vec<(uuid::Uuid, f64)> = vec![(node_id, 1.0)]; - visited.insert(node_id, 1.0); - for _ in 0..depth { - if visited.len() >= MAX_NODES { - break; - } - let mut next_frontier = Vec::new(); - for (current, current_weight) in frontier.iter() { - let conns = self - .get_connections_for_memory(¤t.to_string()) - .unwrap_or_default(); - for conn in conns { - let neighbor_id_str = if conn.source_id == current.to_string() { - conn.target_id - } else { - conn.source_id - }; - let Ok(nid) = uuid::Uuid::parse_str(&neighbor_id_str) else { - continue; - }; - if let std::collections::hash_map::Entry::Vacant(e) = visited.entry(nid) { - let w = current_weight * conn.strength; - e.insert(w); - next_frontier.push((nid, w)); - if visited.len() >= MAX_NODES { - break; - } - } - } - } - frontier = next_frontier; - if frontier.is_empty() { - break; - } - } - let mut result = Vec::with_capacity(visited.len()); - for (nid, weight) in visited { - let Some(node) = self.get_node(&nid.to_string()).ok().flatten() else { - continue; - }; - let (domains, domain_scores) = self.read_domain_columns(&nid.to_string()); - let mut rec = Self::node_to_record(node, None); - rec.domains = domains; - rec.domain_scores = domain_scores; - result.push((rec, weight)); - } - Ok(result) - } - - async fn list_domains( - &self, - ) -> crate::storage::memory_store::MemoryStoreResult> - { - use crate::storage::memory_store::{Domain, MemoryStoreError}; - let reader = self - .reader - .lock() - .map_err(|_| MemoryStoreError::Init("Reader lock poisoned".into()))?; - let mut stmt = reader - .prepare("SELECT id, label, centroid, top_terms, memory_count, created_at FROM domains ORDER BY created_at ASC") - .map_err(|e| MemoryStoreError::Backend(e.to_string()))?; - let rows = stmt - .query_map([], |row| { - let id: String = row.get(0)?; - let label: String = row.get(1)?; - let centroid_bytes: Option> = row.get(2)?; - let top_terms_json: String = row.get(3)?; - let memory_count: i64 = row.get(4)?; - let created_at_str: String = row.get(5)?; - Ok(( - id, - label, - centroid_bytes, - top_terms_json, - memory_count, - created_at_str, - )) - }) - .map_err(|e| MemoryStoreError::Backend(e.to_string()))?; - let mut result = Vec::new(); - for row in rows { - let (id, label, centroid_bytes, top_terms_json, memory_count, created_at_str) = - row.map_err(|e| MemoryStoreError::Backend(e.to_string()))?; - let centroid: Vec = centroid_bytes - .map(|b| { - b.chunks_exact(4) - .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]])) - .collect() - }) - .unwrap_or_default(); - let top_terms: Vec = serde_json::from_str(&top_terms_json).unwrap_or_default(); - let created_at = chrono::DateTime::parse_from_rfc3339(&created_at_str) - .map(|dt| dt.with_timezone(&chrono::Utc)) - .unwrap_or_else(|_| Utc::now()); - result.push(Domain { - id, - label, - centroid, - top_terms, - memory_count: memory_count as usize, - created_at, - }); - } - Ok(result) - } - - async fn get_domain( - &self, - id: &str, - ) -> crate::storage::memory_store::MemoryStoreResult> - { - use crate::storage::memory_store::{Domain, MemoryStoreError}; - type DomainRow = (String, String, Option>, String, i64, String); - let reader = self - .reader - .lock() - .map_err(|_| MemoryStoreError::Init("Reader lock poisoned".into()))?; - let result: Option = reader - .query_row( - "SELECT id, label, centroid, top_terms, memory_count, created_at FROM domains WHERE id = ?1", - rusqlite::params![id], - |row| { - Ok(( - row.get(0)?, - row.get(1)?, - row.get(2)?, - row.get(3)?, - row.get(4)?, - row.get(5)?, - )) - }, - ) - .optional() - .map_err(|e| MemoryStoreError::Backend(e.to_string()))?; - let Some((id, label, centroid_bytes, top_terms_json, memory_count, created_at_str)) = - result - else { - return Ok(None); - }; - let centroid: Vec = centroid_bytes - .map(|b| { - b.chunks_exact(4) - .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]])) - .collect() - }) - .unwrap_or_default(); - let top_terms: Vec = serde_json::from_str(&top_terms_json).unwrap_or_default(); - let created_at = chrono::DateTime::parse_from_rfc3339(&created_at_str) - .map(|dt| dt.with_timezone(&chrono::Utc)) - .unwrap_or_else(|_| Utc::now()); - Ok(Some(Domain { - id, - label, - centroid, - top_terms, - memory_count: memory_count as usize, - created_at, - })) - } - - async fn upsert_domain( - &self, - domain: &crate::storage::memory_store::Domain, - ) -> crate::storage::memory_store::MemoryStoreResult<()> { - use crate::storage::memory_store::MemoryStoreError; - let centroid_bytes: Vec = domain - .centroid - .iter() - .flat_map(|f| f.to_le_bytes()) - .collect(); - let top_terms_json = - serde_json::to_string(&domain.top_terms).unwrap_or_else(|_| "[]".to_string()); - let writer = self - .writer - .lock() - .map_err(|_| MemoryStoreError::Init("Writer lock poisoned".into()))?; - writer - .execute( - "INSERT INTO domains (id, label, centroid, top_terms, memory_count, created_at) - VALUES (?1, ?2, ?3, ?4, ?5, ?6) - ON CONFLICT(id) DO UPDATE SET - label = excluded.label, - centroid = excluded.centroid, - top_terms = excluded.top_terms, - memory_count = excluded.memory_count", - rusqlite::params![ - domain.id, - domain.label, - centroid_bytes, - top_terms_json, - domain.memory_count as i64, - domain.created_at.to_rfc3339(), - ], - ) - .map_err(|e| MemoryStoreError::Backend(e.to_string()))?; - Ok(()) - } - - async fn delete_domain(&self, id: &str) -> crate::storage::memory_store::MemoryStoreResult<()> { - use crate::storage::memory_store::MemoryStoreError; - let writer = self - .writer - .lock() - .map_err(|_| MemoryStoreError::Init("Writer lock poisoned".into()))?; - writer - .execute("DELETE FROM domains WHERE id = ?1", rusqlite::params![id]) - .map_err(|e| MemoryStoreError::Backend(e.to_string()))?; - Ok(()) - } - - async fn classify( - &self, - _embedding: &[f32], - ) -> crate::storage::memory_store::MemoryStoreResult> { - // Phase 1 stub: no centroids yet. Phase 4 wires the full soft-assignment pass. - Ok(vec![]) - } - - async fn count(&self) -> crate::storage::memory_store::MemoryStoreResult { - use crate::storage::memory_store::MemoryStoreError; - let reader = self - .reader - .lock() - .map_err(|_| MemoryStoreError::Init("Reader lock poisoned".into()))?; - let n: i64 = reader - .query_row("SELECT COUNT(*) FROM knowledge_nodes", [], |row| row.get(0)) - .map_err(|e| MemoryStoreError::Backend(e.to_string()))?; - Ok(n as usize) - } - - async fn get_stats( - &self, - ) -> crate::storage::memory_store::MemoryStoreResult - { - use crate::storage::memory_store::{MemoryStoreError, StoreStats}; - let reader = self - .reader - .lock() - .map_err(|_| MemoryStoreError::Init("Reader lock poisoned".into()))?; - let total: i64 = reader - .query_row("SELECT COUNT(*) FROM knowledge_nodes", [], |row| row.get(0)) - .map_err(|e| MemoryStoreError::Backend(e.to_string()))?; - let with_emb: i64 = reader - .query_row( - "SELECT COUNT(*) FROM knowledge_nodes WHERE has_embedding = 1", - [], - |row| row.get(0), - ) - .map_err(|e| MemoryStoreError::Backend(e.to_string()))?; - let total_edges: i64 = reader - .query_row("SELECT COUNT(*) FROM memory_connections", [], |row| { - row.get(0) - }) - .unwrap_or(0); - let total_domains: i64 = reader - .query_row("SELECT COUNT(*) FROM domains", [], |row| row.get(0)) - .unwrap_or(0); - let model_row: Option<(String, i64)> = reader - .query_row( - "SELECT name, dimension FROM embedding_model WHERE id = 1", - [], - |row| Ok((row.get(0)?, row.get(1)?)), - ) - .optional() - .map_err(|e| MemoryStoreError::Backend(e.to_string()))?; - let (model_name, model_dim) = model_row - .map(|(n, d)| (Some(n), Some(d as usize))) - .unwrap_or((None, None)); - Ok(StoreStats { - total_memories: total as usize, - memories_with_embeddings: with_emb as usize, - total_edges: total_edges as usize, - total_domains: total_domains as usize, - registered_model_name: model_name, - registered_model_dim: model_dim, - }) - } - - async fn vacuum(&self) -> crate::storage::memory_store::MemoryStoreResult<()> { - use crate::storage::memory_store::MemoryStoreError; - let writer = self - .writer - .lock() - .map_err(|_| MemoryStoreError::Init("Writer lock poisoned".into()))?; - writer - .execute_batch("VACUUM;") - .map_err(|e| MemoryStoreError::Backend(e.to_string()))?; - Ok(()) - } -} - -// ============================================================================ -// CONNECTOR SYNC (#57) — idempotent external-source ingestion -// ============================================================================ - -/// What `upsert_by_source` did with one external record. Drives the -/// created/updated/unchanged/tombstoned counts a connector reports. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SourceUpsertOutcome { - /// No memory existed for this `(source_system, source_id)` — inserted. - Created, - /// A memory existed and the `content_hash` changed — body + envelope updated - /// and the embedding regenerated. - Updated, - /// A memory existed with the same `content_hash` — nothing rewritten except - /// `synced_at` (so an incremental re-scan is free). - Unchanged, -} - -/// Result of one `upsert_by_source` call. -#[derive(Debug, Clone)] -pub struct SourceUpsertResult { - pub outcome: SourceUpsertOutcome, - /// Memory id of the affected node (new or existing). - pub node_id: String, -} - -/// Incremental-sync checkpoint for one `(source_system, scope)`. -#[derive(Debug, Clone, Default)] -pub struct ConnectorCursor { - pub source_system: String, - pub scope: String, - /// High-water mark on the source's update timestamp. `None` on first sync. - pub cursor_updated_at: Option>, - pub last_synced_at: Option>, - pub last_full_reconcile_at: Option>, - pub records_seen: i64, -} - -/// Outcome of a tombstone reconciliation pass. -#[derive(Debug, Clone, Default)] -pub struct ReconcileReport { - /// Memory ids that were tombstoned (no longer visible upstream). - pub tombstoned: Vec, - /// Number of local records considered for this scope. - pub considered: usize, -} - -impl SqliteMemoryStore { - /// Idempotently upsert one external-source record, keyed on the envelope's - /// `(source_system, source_id)` (#57). - /// - /// This is the core primitive every connector calls per record. It makes - /// re-running a sync safe and cheap: - /// - /// - **No existing memory** for the key → insert (`Created`). - /// - **Existing memory, `content_hash` changed** → update content + envelope, - /// stamp `updated_at`, regenerate the embedding (`Updated`). - /// - **Existing memory, `content_hash` unchanged** → touch only `synced_at` - /// so the reconcile pass knows the record is still live (`Unchanged`). - /// - /// The caller MUST set `source_system`, `source_id`, and `content_hash` on - /// the input's `source_envelope`; otherwise this falls back to a plain - /// `ingest` (an un-keyed record can't be deduplicated). - pub fn upsert_by_source(&self, input: IngestInput) -> Result { - let env = match input.source_envelope.clone() { - Some(e) if e.has_key() => e, - // No idempotency key — behave like a normal create. - _ => { - let node = self.ingest(input)?; - return Ok(SourceUpsertResult { - outcome: SourceUpsertOutcome::Created, - node_id: node.id, - }); - } - }; - - let source_system = env.source_system.clone().unwrap_or_default(); - let source_id = env.source_id.clone().unwrap_or_default(); - // Scope the idempotency key by source_project too: two sources of the - // same system (e.g. github repos octocat/repoA and octocat/repoB, or two - // Redmine instances) reuse bare per-project ids ("5"), so keying on - // (source_system, source_id) alone made repoB's issue #5 overwrite - // repoA's row in place. `IS NOT DISTINCT FROM` matches NULL==NULL so - // legacy rows without a project still resolve. - let source_project = env.source_project.clone(); - let now = Utc::now(); - - // Look up the existing memory for this external record, if any. - let existing: Option<(String, Option)> = { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - reader - .query_row( - "SELECT id, content_hash FROM knowledge_nodes \ - WHERE source_system = ?1 AND source_id = ?2 \ - AND source_project IS ?3 LIMIT 1", - params![source_system, source_id, source_project], - |row| Ok((row.get::<_, String>(0)?, row.get::<_, Option>(1)?)), - ) - .optional()? - }; - - let Some((node_id, stored_hash)) = existing else { - // First time we've seen this record — plain insert carries the - // envelope through the existing ingest path. - let node = self.ingest(input)?; - return Ok(SourceUpsertResult { - outcome: SourceUpsertOutcome::Created, - node_id: node.id, - }); - }; - - let new_hash = env.content_hash.clone(); - let unchanged = match (&stored_hash, &new_hash) { - // Both present and equal → genuinely unchanged. - (Some(a), Some(b)) => a == b, - // Either side missing a hash → be conservative and treat as changed - // so we never silently skip a real update. - _ => false, - }; - - let env_source_updated_at = env.source_updated_at.map(|dt| dt.to_rfc3339()); - let synced_at = now.to_rfc3339(); - - if unchanged { - // Cheapest path: only advance liveness + the source cursor field. - let writer = self - .writer - .lock() - .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; - writer.execute( - // Un-tombstone fully: a reappearing record clears BOTH bitemporal - // markers (valid_until AND superseded_by), otherwise it would be - // resurrected as currently-valid yet still flagged as superseded, - // which permanently excludes it from merge/consolidation. - "UPDATE knowledge_nodes \ - SET synced_at = ?1, source_updated_at = COALESCE(?2, source_updated_at), \ - source_url = COALESCE(?3, source_url), \ - valid_until = NULL, superseded_by = NULL \ - WHERE id = ?4", - params![synced_at, env_source_updated_at, env.source_url, node_id], - )?; - return Ok(SourceUpsertResult { - outcome: SourceUpsertOutcome::Unchanged, - node_id, - }); - } - - // Content changed upstream → update body + full envelope, clear any - // prior tombstone (`valid_until`), then regenerate the embedding. - { - let writer = self - .writer - .lock() - .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; - writer.execute( - // Clear BOTH bitemporal markers on update (see Unchanged branch). - "UPDATE knowledge_nodes SET \ - content = ?1, updated_at = ?2, synced_at = ?3, \ - content_hash = ?4, source_url = ?5, source_updated_at = ?6, \ - source_project = ?7, source_type = ?8, source_author = ?9, \ - valid_until = NULL, superseded_by = NULL \ - WHERE id = ?10", - params![ - input.content, - now.to_rfc3339(), - synced_at, - env.content_hash, - env.source_url, - env_source_updated_at, - env.source_project, - env.source_type, - env.source_author, - node_id, - ], - )?; - } - - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - { - if let Some(index) = self.vector_index.as_ref() - && let Ok(mut index) = index.lock() - { - let _ = index.remove(&node_id); - } - if let Err(e) = self.generate_embedding_for_node(&node_id, &input.content) { - tracing::warn!("Failed to regenerate embedding for {}: {}", node_id, e); - } - } - - Ok(SourceUpsertResult { - outcome: SourceUpsertOutcome::Updated, - node_id, - }) - } - - /// Read the incremental-sync checkpoint for a `(source_system, scope)`. - /// Returns a zeroed cursor (no high-water mark) if none has been saved yet. - pub fn get_connector_cursor( - &self, - source_system: &str, - scope: &str, - ) -> Result { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let row = reader - .query_row( - "SELECT cursor_updated_at, last_synced_at, last_full_reconcile_at, records_seen \ - FROM connector_cursors WHERE source_system = ?1 AND scope = ?2", - params![source_system, scope], - |row| { - Ok(( - row.get::<_, Option>(0)?, - row.get::<_, Option>(1)?, - row.get::<_, Option>(2)?, - row.get::<_, i64>(3)?, - )) - }, - ) - .optional()?; - - let parse = |s: Option| -> Option> { - s.and_then(|s| { - DateTime::parse_from_rfc3339(&s) - .map(|dt| dt.with_timezone(&Utc)) - .ok() - }) - }; - - Ok(match row { - Some((cur, last, recon, seen)) => ConnectorCursor { - source_system: source_system.to_string(), - scope: scope.to_string(), - cursor_updated_at: parse(cur), - last_synced_at: parse(last), - last_full_reconcile_at: parse(recon), - records_seen: seen, - }, - None => ConnectorCursor { - source_system: source_system.to_string(), - scope: scope.to_string(), - ..Default::default() - }, - }) - } - - /// Persist the incremental-sync checkpoint for a `(source_system, scope)`. - pub fn save_connector_cursor(&self, cursor: &ConnectorCursor) -> Result<()> { - let writer = self - .writer - .lock() - .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; - writer.execute( - "INSERT INTO connector_cursors \ - (source_system, scope, cursor_updated_at, last_synced_at, \ - last_full_reconcile_at, records_seen) \ - VALUES (?1, ?2, ?3, ?4, ?5, ?6) \ - ON CONFLICT(source_system, scope) DO UPDATE SET \ - cursor_updated_at = excluded.cursor_updated_at, \ - last_synced_at = excluded.last_synced_at, \ - last_full_reconcile_at = excluded.last_full_reconcile_at, \ - records_seen = excluded.records_seen", - params![ - cursor.source_system, - cursor.scope, - cursor.cursor_updated_at.map(|d| d.to_rfc3339()), - cursor.last_synced_at.map(|d| d.to_rfc3339()), - cursor.last_full_reconcile_at.map(|d| d.to_rfc3339()), - cursor.records_seen, - ], - )?; - Ok(()) - } - - /// Reconcile deletions for a scope: tombstone every local memory in - /// `(source_system, source_project = scope)` whose `source_id` is NOT in the - /// caller-supplied set of currently-live ids (#57). - /// - /// Neither Redmine nor GitHub exposes a deletion feed, so an incremental - /// `updated_at` sync can never see a delete. The connector therefore - /// periodically enumerates the full set of live ids and calls this. We - /// **invalidate, don't purge** (Graphiti-style): the memory keeps its - /// content for audit but gets `valid_until = now`, so it falls out of - /// "currently valid" retrieval without losing history. A record that - /// reappears upstream is un-tombstoned by the next `upsert_by_source` - /// (which clears `valid_until`). - pub fn reconcile_source_tombstones( - &self, - source_system: &str, - scope: &str, - live_ids: &[String], - ) -> Result { - let live: std::collections::HashSet<&str> = live_ids.iter().map(|s| s.as_str()).collect(); - - // All currently-valid local records for this scope. - let local: Vec<(String, String)> = { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let mut stmt = reader.prepare( - "SELECT id, source_id FROM knowledge_nodes \ - WHERE source_system = ?1 AND source_project = ?2 \ - AND source_id IS NOT NULL AND valid_until IS NULL", - )?; - let rows = stmt.query_map(params![source_system, scope], |row| { - Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)) - })?; - rows.filter_map(|r| r.ok()).collect() - }; - - let considered = local.len(); - let now = Utc::now().to_rfc3339(); - let mut tombstoned = Vec::new(); - - { - let writer = self - .writer - .lock() - .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; - for (node_id, source_id) in &local { - if !live.contains(source_id.as_str()) { - writer.execute( - "UPDATE knowledge_nodes SET valid_until = ?1 WHERE id = ?2", - params![now, node_id], - )?; - tombstoned.push(node_id.clone()); - } - } - } - - Ok(ReconcileReport { - tombstoned, - considered, - }) - } } // ============================================================================ @@ -10251,16 +4321,7 @@ impl SqliteMemoryStore { #[cfg(test)] mod tests { use super::*; - use crate::advanced::{MatchClass, MergePolicy}; - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind}; use tempfile::tempdir; - // The public struct was renamed from Storage to SqliteMemoryStore; this - // alias keeps all existing tests compiling without modification. - use SqliteMemoryStore as Storage; - - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - static ENV_LOCK: Mutex<()> = Mutex::new(()); fn create_test_storage() -> Storage { let dir = tempdir().unwrap(); @@ -10268,434 +4329,6 @@ mod tests { Storage::new(Some(db_path)).unwrap() } - fn create_test_storage_at(dir: &tempfile::TempDir, name: &str) -> Storage { - Storage::new(Some(dir.path().join(name))).unwrap() - } - - // ===================== Connector sync (#57) ========================= - - /// Build an `IngestInput` carrying a source envelope for a GitHub-ish issue. - fn source_input(id: &str, content: &str, hash: &str) -> IngestInput { - IngestInput { - content: content.to_string(), - node_type: "fact".to_string(), - source_envelope: Some(crate::memory::SourceEnvelope { - source_system: Some("github".to_string()), - source_id: Some(id.to_string()), - source_url: Some(format!("https://github.com/o/r/issues/{id}")), - content_hash: Some(hash.to_string()), - source_project: Some("o/r".to_string()), - source_type: Some("issue".to_string()), - source_author: Some("octocat".to_string()), - ..Default::default() - }), - ..Default::default() - } - } - - fn node_count(store: &Storage) -> i64 { - // Count rows for our test source so embeddings/other tests don't bleed in. - let reader = store.reader.lock().unwrap(); - reader - .query_row( - "SELECT COUNT(*) FROM knowledge_nodes WHERE source_system = 'github'", - [], - |r| r.get(0), - ) - .unwrap() - } - - #[test] - fn upsert_by_source_is_idempotent_across_reruns() { - let store = create_test_storage(); - - // First sync: a brand-new record → Created. - let r1 = store - .upsert_by_source(source_input("1", "Bug: crash on startup", "hash-a")) - .unwrap(); - assert_eq!(r1.outcome, SourceUpsertOutcome::Created); - assert_eq!(node_count(&store), 1); - - // Re-sync the SAME record with the SAME hash twice → Unchanged, no dupes. - for _ in 0..2 { - let r = store - .upsert_by_source(source_input("1", "Bug: crash on startup", "hash-a")) - .unwrap(); - assert_eq!(r.outcome, SourceUpsertOutcome::Unchanged); - assert_eq!(r.node_id, r1.node_id, "must reuse the same memory id"); - } - assert_eq!( - node_count(&store), - 1, - "idempotent: still exactly one memory" - ); - } - - #[test] - fn upsert_by_source_updates_in_place_when_hash_changes() { - let store = create_test_storage(); - let created = store - .upsert_by_source(source_input("7", "old body", "hash-old")) - .unwrap(); - - // Upstream edit: content + hash change → Updated, same id, new content. - let updated = store - .upsert_by_source(source_input("7", "new edited body", "hash-new")) - .unwrap(); - assert_eq!(updated.outcome, SourceUpsertOutcome::Updated); - assert_eq!(updated.node_id, created.node_id); - assert_eq!(node_count(&store), 1, "update must not duplicate"); - - let node = store.get_node(&created.node_id).unwrap().unwrap(); - assert_eq!(node.content, "new edited body"); - let env = node.source_envelope.expect("envelope persisted"); - assert_eq!(env.content_hash.as_deref(), Some("hash-new")); - assert_eq!(env.source_id.as_deref(), Some("7")); - } - - #[test] - fn upsert_by_source_without_key_falls_back_to_create() { - let store = create_test_storage(); - // Envelope present but missing source_id → not keyed → plain create. - let input = IngestInput { - content: "loose note".to_string(), - node_type: "fact".to_string(), - source_envelope: Some(crate::memory::SourceEnvelope { - source_url: Some("https://example.com/x".to_string()), - ..Default::default() - }), - ..Default::default() - }; - let r = store.upsert_by_source(input).unwrap(); - assert_eq!(r.outcome, SourceUpsertOutcome::Created); - } - - #[test] - fn connector_cursor_round_trips() { - let store = create_test_storage(); - // Unknown scope → zeroed cursor. - let empty = store.get_connector_cursor("github", "o/r").unwrap(); - assert!(empty.cursor_updated_at.is_none()); - assert_eq!(empty.records_seen, 0); - - let ts = Utc::now(); - let cursor = ConnectorCursor { - source_system: "github".to_string(), - scope: "o/r".to_string(), - cursor_updated_at: Some(ts), - last_synced_at: Some(ts), - last_full_reconcile_at: None, - records_seen: 42, - }; - store.save_connector_cursor(&cursor).unwrap(); - - let back = store.get_connector_cursor("github", "o/r").unwrap(); - assert_eq!(back.records_seen, 42); - assert_eq!( - back.cursor_updated_at.map(|d| d.to_rfc3339()), - Some(ts.to_rfc3339()) - ); - - // Upsert semantics: saving again replaces, never duplicates. - let mut c2 = cursor.clone(); - c2.records_seen = 99; - store.save_connector_cursor(&c2).unwrap(); - assert_eq!( - store - .get_connector_cursor("github", "o/r") - .unwrap() - .records_seen, - 99 - ); - } - - #[test] - fn reconcile_tombstones_records_absent_from_live_set() { - let store = create_test_storage(); - // Three synced issues in scope o/r. - for id in ["1", "2", "3"] { - store - .upsert_by_source(source_input(id, &format!("issue {id}"), &format!("h{id}"))) - .unwrap(); - } - - // Reconcile: only 1 and 3 are still visible upstream → 2 is tombstoned. - let report = store - .reconcile_source_tombstones("github", "o/r", &["1".to_string(), "3".to_string()]) - .unwrap(); - assert_eq!(report.considered, 3); - assert_eq!(report.tombstoned.len(), 1, "exactly issue 2 tombstoned"); - - // Issue 2's memory is invalidated (valid_until set) but NOT purged — - // content retained for audit, just no longer currently-valid. - let two = { - let reader = store.reader.lock().unwrap(); - reader - .query_row( - "SELECT id, valid_until FROM knowledge_nodes WHERE source_id = '2'", - [], - |r| Ok((r.get::<_, String>(0)?, r.get::<_, Option>(1)?)), - ) - .unwrap() - }; - assert!( - two.1.is_some(), - "tombstoned record must have valid_until set" - ); - let node = store.get_node(&two.0).unwrap().unwrap(); - assert!( - !node.is_currently_valid(), - "tombstoned node is not valid now" - ); - assert_eq!(node.content, "issue 2", "content retained for audit"); - - // A reappearing record un-tombstones on next upsert (clears valid_until). - store - .upsert_by_source(source_input("2", "issue 2", "h2")) - .unwrap(); - let revived = store.get_node(&two.0).unwrap().unwrap(); - assert!( - revived.is_currently_valid(), - "re-synced record is valid again" - ); - } - - #[test] - fn upsert_clears_superseded_by_when_record_reappears() { - // Regression: un-tombstoning must clear BOTH bitemporal markers. A - // connector node that was superseded/merged (valid_until + superseded_by - // both set) and then re-observed upstream must come back fully clean, - // otherwise it is currently-valid yet still flagged superseded and is - // permanently excluded from merge candidacy. - let store = create_test_storage(); - let created = store - .upsert_by_source(source_input("9", "body v1", "h9a")) - .unwrap(); - - // Simulate the node having been superseded (as merge/supersede would). - { - let writer = store.writer.lock().unwrap(); - writer - .execute( - "UPDATE knowledge_nodes SET valid_until = ?1, superseded_by = 'survivor-id' WHERE id = ?2", - params![Utc::now().to_rfc3339(), created.node_id], - ) - .unwrap(); - } - assert!( - store - .superseded_node_ids() - .unwrap() - .contains(&created.node_id), - "precondition: node is superseded" - ); - - // Re-sync with a content change → Updated branch must clear both markers. - let res = store - .upsert_by_source(source_input("9", "body v2 edited", "h9b")) - .unwrap(); - assert_eq!(res.outcome, SourceUpsertOutcome::Updated); - assert!( - !store - .superseded_node_ids() - .unwrap() - .contains(&created.node_id), - "superseded_by must be cleared on re-sync (no bitemporal zombie)" - ); - let node = store.get_node(&created.node_id).unwrap().unwrap(); - assert!(node.is_currently_valid()); - - // Also exercise the Unchanged branch: supersede again, re-sync same hash. - { - let writer = store.writer.lock().unwrap(); - writer - .execute( - "UPDATE knowledge_nodes SET valid_until = ?1, superseded_by = 'survivor-id' WHERE id = ?2", - params![Utc::now().to_rfc3339(), created.node_id], - ) - .unwrap(); - } - let res2 = store - .upsert_by_source(source_input("9", "body v2 edited", "h9b")) - .unwrap(); - assert_eq!(res2.outcome, SourceUpsertOutcome::Unchanged); - assert!( - !store - .superseded_node_ids() - .unwrap() - .contains(&created.node_id), - "Unchanged branch must also clear superseded_by" - ); - } - - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - fn with_vector_search_disabled(f: impl FnOnce() -> T) -> T { - let _guard = ENV_LOCK.lock().unwrap(); - let previous = std::env::var_os(VESTIGE_DISABLE_VECTOR_SEARCH); - - // Tests serialize access with ENV_LOCK because process environment - // mutation is global and unsafe under Rust 2024. - unsafe { - std::env::set_var(VESTIGE_DISABLE_VECTOR_SEARCH, "1"); - } - - let result = catch_unwind(AssertUnwindSafe(f)); - - unsafe { - if let Some(value) = previous { - std::env::set_var(VESTIGE_DISABLE_VECTOR_SEARCH, value); - } else { - std::env::remove_var(VESTIGE_DISABLE_VECTOR_SEARCH); - } - } - - match result { - Ok(value) => value, - Err(payload) => resume_unwind(payload), - } - } - - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - #[test] - fn test_runtime_vector_gate_env_disables_index_creation() { - with_vector_search_disabled(|| { - assert!(!Storage::vector_search_enabled_by_cpu()); - assert_eq!( - Storage::vector_search_unavailable_reason(), - Some("disabled by VESTIGE_DISABLE_VECTOR_SEARCH") - ); - - let dir = tempdir().unwrap(); - let storage = create_test_storage_at(&dir, "vector-disabled.db"); - - assert!(storage.vector_index.is_none()); - assert!(storage.query_cache.is_none()); - - let stats = storage.get_stats().unwrap(); - assert_eq!(stats.total_nodes, 0); - - let schema = storage.schema_introspection().unwrap(); - assert!(schema.schema_version >= 1); - }); - } - - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - #[test] - fn test_runtime_vector_gate_disabled_hybrid_search_uses_keyword_fallback() { - with_vector_search_disabled(|| { - let dir = tempdir().unwrap(); - let storage = create_test_storage_at(&dir, "vector-disabled-search.db"); - - storage - .ingest(IngestInput { - content: "runtime gate fallback keyword anchor".to_string(), - node_type: "fact".to_string(), - ..Default::default() - }) - .unwrap(); - - let results = storage - .hybrid_search("runtime gate fallback keyword", 10, 0.3, 0.7) - .unwrap(); - - assert_eq!(results.len(), 1); - assert_eq!(results[0].match_type, MatchType::Keyword); - assert!(results[0].semantic_score.is_none()); - assert!( - results[0] - .node - .content - .contains("runtime gate fallback keyword anchor") - ); - }); - } - - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - #[test] - fn test_embedding_model_family_matching() { - assert!(Storage::embedding_model_matches_active( - "nomic-embed-text-v1.5", - "nomic-ai/nomic-embed-text-v1.5", - )); - assert!(Storage::embedding_model_matches_active( - "Qwen/Qwen3-Embedding-0.6B", - "Qwen/Qwen3-Embedding-0.6B", - )); - assert!(!Storage::embedding_model_matches_active( - "nomic-ai/nomic-embed-text-v1.5", - "Qwen/Qwen3-Embedding-0.6B", - )); - - let bytes = Embedding::new(vec![1.0; EMBEDDING_DIMENSIONS]).to_bytes(); - assert!( - Storage::embedding_vector_for_active_model( - &bytes, - "nomic-ai/nomic-embed-text-v1.5", - "Qwen/Qwen3-Embedding-0.6B", - ) - .is_none() - ); - assert!( - Storage::embedding_vector_for_active_model( - &bytes, - "Qwen/Qwen3-Embedding-0.6B", - "Qwen/Qwen3-Embedding-0.6B", - ) - .is_some() - ); - } - - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - #[test] - fn test_embedding_regeneration_candidates_include_entire_mismatched_corpus() { - let storage = create_test_storage(); - let stale_model = "all-MiniLM-L6-v2"; - let stale_embedding = Embedding::new(vec![0.0; EMBEDDING_DIMENSIONS]).to_bytes(); - - for i in 0..125 { - let node = storage - .ingest(IngestInput { - content: format!("legacy embedded memory {}", i), - node_type: "fact".to_string(), - ..Default::default() - }) - .unwrap(); - - let writer = storage.writer.lock().unwrap(); - writer - .execute( - "INSERT OR REPLACE INTO node_embeddings - (node_id, embedding, dimensions, model, created_at) - VALUES (?1, ?2, ?3, ?4, ?5)", - rusqlite::params![ - &node.id, - &stale_embedding, - EMBEDDING_DIMENSIONS as i32, - stale_model, - Utc::now().to_rfc3339() - ], - ) - .unwrap(); - writer - .execute( - "UPDATE knowledge_nodes - SET has_embedding = 1, embedding_model = ?2 - WHERE id = ?1", - rusqlite::params![&node.id, stale_model], - ) - .unwrap(); - } - - let stats = storage.get_stats().unwrap(); - assert_eq!(stats.nodes_with_mismatched_embeddings, 125); - assert_eq!(stats.nodes_with_active_embeddings, 0); - - let candidates = storage - .embedding_regeneration_candidates(None, false) - .unwrap(); - assert_eq!(candidates.len(), 125); - } - #[test] fn test_storage_creation() { let storage = create_test_storage(); @@ -10703,32 +4336,6 @@ mod tests { assert_eq!(stats.total_nodes, 0); } - #[test] - fn test_parse_timestamp_accepts_rfc3339_and_sqlite_native() { - use chrono::TimeZone; - - // Canonical writer: RFC 3339 with fractional seconds + offset. - let rfc = - Storage::parse_timestamp("2026-06-12T15:07:59.730+00:00", "last_accessed").unwrap(); - assert_eq!(rfc.to_rfc3339(), "2026-06-12T15:07:59.730+00:00"); - - // External writer: SQLite-native `datetime('now')` (space separator, - // no timezone, no fraction) — must be tolerated, assumed UTC. - let sqlite = Storage::parse_timestamp("2026-06-12 15:07:59", "last_accessed").unwrap(); - assert_eq!( - sqlite, - Utc.with_ymd_and_hms(2026, 6, 12, 15, 7, 59).unwrap() - ); - - // SQLite-native with fractional seconds. - let sqlite_frac = - Storage::parse_timestamp("2026-06-12 15:07:59.730", "last_accessed").unwrap(); - assert_eq!(sqlite_frac.timestamp_subsec_millis(), 730); - - // Genuinely malformed input still errors. - assert!(Storage::parse_timestamp("not-a-timestamp", "last_accessed").is_err()); - } - #[test] fn test_ingest_and_get() { let storage = create_test_storage(); @@ -10800,622 +4407,6 @@ mod tests { assert!(storage.get_node(&node.id).unwrap().is_none()); } - #[test] - fn test_composition_save_query_outcome_and_never_composed() { - let storage = create_test_storage(); - let first = storage - .ingest(IngestInput { - content: "Oracle drift can break delayed settlement.".to_string(), - node_type: "fact".to_string(), - tags: vec![ - "protocolgate".to_string(), - "boundary-oracle".to_string(), - "settlement".to_string(), - ], - ..Default::default() - }) - .unwrap(); - let second = storage - .ingest(IngestInput { - content: "Withdrawal queues can settle stale claims.".to_string(), - node_type: "pattern".to_string(), - tags: vec![ - "protocolgate".to_string(), - "boundary-queue".to_string(), - "settlement".to_string(), - ], - ..Default::default() - }) - .unwrap(); - let third = storage - .ingest(IngestInput { - content: "Keeper roles can drift from local validation paths.".to_string(), - node_type: "pattern".to_string(), - tags: vec![ - "protocolgate".to_string(), - "boundary-role".to_string(), - "settlement".to_string(), - ], - ..Default::default() - }) - .unwrap(); - - let before = storage - .get_never_composed_candidates(10, Some(&["protocolgate".to_string()])) - .unwrap(); - let first_second_before = before - .iter() - .find(|candidate| { - let pair = Storage::pair_key(&candidate.first_id, &candidate.second_id); - pair == Storage::pair_key(&first.id, &second.id) - }) - .expect("uncomposed first/second pair should be ranked before any event"); - assert!( - first_second_before.bridge_score > 0.0, - "candidate should expose a bridge score" - ); - assert!( - first_second_before.novelty_score > 0.0, - "candidate should expose a novelty score" - ); - assert_eq!( - first_second_before.outcome_signal, "clean", - "new candidate should start without prior outcome context" - ); - assert!( - first_second_before - .composition_question - .contains("composed through"), - "candidate should include a promptable composition question" - ); - - let event = CompositionEventRecord { - id: "composition-test-1".to_string(), - created_at: Utc::now(), - tool: "deep_reference".to_string(), - mode: "bounty".to_string(), - query: Some("oracle drift delayed settlement".to_string()), - query_hash: Some("sha256:test".to_string()), - confidence: Some(0.87), - status: Some("resolved".to_string()), - output_preview: Some("Compose oracle drift with withdrawal queue.".to_string()), - metadata: serde_json::json!({"workflow": "test"}), - }; - let members = vec![ - CompositionMemberRecord { - event_id: event.id.clone(), - memory_id: first.id.clone(), - role: "primary".to_string(), - rank: 0, - trust: Some(0.8), - score: Some(0.9), - preview: Some(preview(&first.content, 120)), - metadata: serde_json::json!({}), - }, - CompositionMemberRecord { - event_id: event.id.clone(), - memory_id: second.id.clone(), - role: "supporting".to_string(), - rank: 1, - trust: Some(0.7), - score: Some(0.75), - preview: Some(preview(&second.content, 120)), - metadata: serde_json::json!({}), - }, - ]; - storage.save_composition(&event, &members, &[]).unwrap(); - - let outcome = CompositionOutcomeRecord { - id: "composition-outcome-1".to_string(), - event_id: event.id.clone(), - outcome_type: "submitted".to_string(), - labeled_at: Utc::now(), - label_source: "test".to_string(), - confidence_delta: Some(0.1), - notes: Some("Report submitted".to_string()), - metadata: serde_json::json!({"severity": "high"}), - }; - storage.record_composition_outcome(&outcome).unwrap(); - - let fetched = storage.get_composition_event(&event.id).unwrap().unwrap(); - assert_eq!(fetched.mode, "bounty"); - assert_eq!(fetched.metadata["workflow"], "test"); - - let fetched_members = storage.get_composition_members(&event.id).unwrap(); - assert_eq!(fetched_members.len(), 2); - assert_eq!(fetched_members[0].role, "primary"); - - let fetched_outcomes = storage.get_composition_outcomes(&event.id).unwrap(); - assert_eq!(fetched_outcomes.len(), 1); - assert_eq!(fetched_outcomes[0].outcome_type, "submitted"); - - let for_memory = storage.get_compositions_for_memory(&first.id, 5).unwrap(); - assert_eq!(for_memory.len(), 1); - assert_eq!(for_memory[0].id, event.id); - - let neighbors = storage.get_composition_neighbors(&first.id, 5).unwrap(); - assert_eq!(neighbors.len(), 1); - assert_eq!(neighbors[0].memory_id, second.id); - - let after = storage - .get_never_composed_candidates(10, Some(&["protocolgate".to_string()])) - .unwrap(); - assert!( - !after.iter().any(|candidate| { - let pair = Storage::pair_key(&candidate.first_id, &candidate.second_id); - pair == Storage::pair_key(&first.id, &second.id) - }), - "already-composed first/second pair should be removed" - ); - assert!( - after.iter().any(|candidate| { - let pair = Storage::pair_key(&candidate.first_id, &candidate.second_id); - pair == Storage::pair_key(&first.id, &third.id) - || pair == Storage::pair_key(&second.id, &third.id) - }), - "other protocolgate pairs should remain candidates" - ); - } - - #[test] - fn test_composition_neighbors_count_distinct_events_not_member_roles() { - let storage = create_test_storage(); - let first = storage - .ingest(IngestInput { - content: "Oracle role appears once in the event.".to_string(), - node_type: "fact".to_string(), - tags: vec!["protocolgate".to_string(), "settlement".to_string()], - ..Default::default() - }) - .unwrap(); - let second = storage - .ingest(IngestInput { - content: "Queue role appears under two evidence roles.".to_string(), - node_type: "fact".to_string(), - tags: vec!["protocolgate".to_string(), "settlement".to_string()], - ..Default::default() - }) - .unwrap(); - - storage - .save_composition( - &CompositionEventRecord { - id: "multi-role-neighbor-event".to_string(), - created_at: Utc::now(), - tool: "deep_reference".to_string(), - mode: "bounty".to_string(), - query: Some("multi role neighbor".to_string()), - query_hash: Some("fnv1a64:neighbor".to_string()), - confidence: Some(0.7), - status: Some("resolved".to_string()), - output_preview: None, - metadata: serde_json::json!({}), - }, - &[ - CompositionMemberRecord { - event_id: "multi-role-neighbor-event".to_string(), - memory_id: first.id.clone(), - role: "primary".to_string(), - rank: 0, - trust: Some(0.8), - score: Some(0.9), - preview: None, - metadata: serde_json::json!({}), - }, - CompositionMemberRecord { - event_id: "multi-role-neighbor-event".to_string(), - memory_id: second.id.clone(), - role: "supporting".to_string(), - rank: 1, - trust: Some(0.7), - score: Some(0.8), - preview: None, - metadata: serde_json::json!({}), - }, - CompositionMemberRecord { - event_id: "multi-role-neighbor-event".to_string(), - memory_id: second.id.clone(), - role: "related".to_string(), - rank: 2, - trust: Some(0.7), - score: Some(0.6), - preview: None, - metadata: serde_json::json!({}), - }, - ], - &[], - ) - .unwrap(); - - let neighbors = storage.get_composition_neighbors(&first.id, 10).unwrap(); - assert_eq!(neighbors.len(), 1); - assert_eq!(neighbors[0].memory_id, second.id); - assert_eq!( - neighbors[0].composed_count, 1, - "one event with multiple member roles should count as one composition" - ); - } - - #[test] - fn test_never_composed_tag_filter_includes_older_tagged_candidates() { - let storage = create_test_storage(); - let first = storage - .ingest(IngestInput { - content: "Older Vestige composition frontier about outcome-shaped recall." - .to_string(), - node_type: "fact".to_string(), - tags: vec!["project:vestige".to_string(), "composition".to_string()], - ..Default::default() - }) - .unwrap(); - let second = storage - .ingest(IngestInput { - content: "Older Vestige composition frontier about never-composed recall." - .to_string(), - node_type: "pattern".to_string(), - tags: vec!["project:vestige".to_string(), "composition".to_string()], - ..Default::default() - }) - .unwrap(); - - for idx in 0..751 { - storage - .ingest(IngestInput { - content: format!("Unrelated recent memory {idx} for scan-window pressure."), - node_type: "fact".to_string(), - tags: vec!["unrelated".to_string()], - ..Default::default() - }) - .unwrap(); - } - - let candidates = storage - .get_never_composed_candidates(10, Some(&["project".to_string()])) - .unwrap(); - assert!( - candidates.iter().any(|candidate| { - let pair = Storage::pair_key(&candidate.first_id, &candidate.second_id); - pair == Storage::pair_key(&first.id, &second.id) - }), - "tag-filtered frontier should include older namespaced-tag memories outside the base scan window" - ); - } - - #[test] - fn test_never_composed_carries_prior_outcome_signal() { - let storage = create_test_storage(); - let first = storage - .ingest(IngestInput { - content: "Oracle drift lane previously looked duplicate-prone.".to_string(), - node_type: "fact".to_string(), - tags: vec![ - "protocolgate".to_string(), - "boundary-oracle".to_string(), - "settlement".to_string(), - ], - ..Default::default() - }) - .unwrap(); - let second = storage - .ingest(IngestInput { - content: "Withdrawal queue lane had weak proof.".to_string(), - node_type: "fact".to_string(), - tags: vec![ - "protocolgate".to_string(), - "boundary-queue".to_string(), - "settlement".to_string(), - ], - ..Default::default() - }) - .unwrap(); - let third = storage - .ingest(IngestInput { - content: "Keeper settlement lane has not been composed with oracle drift." - .to_string(), - node_type: "pattern".to_string(), - tags: vec![ - "protocolgate".to_string(), - "boundary-role".to_string(), - "settlement".to_string(), - ], - ..Default::default() - }) - .unwrap(); - - let event = CompositionEventRecord { - id: "prior-outcome-composition".to_string(), - created_at: Utc::now(), - tool: "deep_reference".to_string(), - mode: "bounty".to_string(), - query: Some("oracle withdrawal duplicate risk".to_string()), - query_hash: Some("fnv1a64:prior".to_string()), - confidence: Some(0.4), - status: Some("closed".to_string()), - output_preview: Some("Prior composition was labeled duplicate risk.".to_string()), - metadata: serde_json::json!({}), - }; - storage - .save_composition( - &event, - &[ - CompositionMemberRecord { - event_id: event.id.clone(), - memory_id: first.id.clone(), - role: "primary".to_string(), - rank: 0, - trust: Some(0.7), - score: Some(0.8), - preview: None, - metadata: serde_json::json!({}), - }, - CompositionMemberRecord { - event_id: event.id.clone(), - memory_id: second.id.clone(), - role: "supporting".to_string(), - rank: 1, - trust: Some(0.7), - score: Some(0.8), - preview: None, - metadata: serde_json::json!({}), - }, - ], - &[CompositionOutcomeRecord { - id: "prior-outcome-label".to_string(), - event_id: event.id.clone(), - outcome_type: "duplicate_risk".to_string(), - labeled_at: Utc::now(), - label_source: "test".to_string(), - confidence_delta: Some(-0.2), - notes: Some("Duplicate family in prior lane.".to_string()), - metadata: serde_json::json!({}), - }], - ) - .unwrap(); - - let candidates = storage - .get_never_composed_candidates(10, Some(&["protocolgate".to_string()])) - .unwrap(); - let candidate = candidates - .iter() - .find(|candidate| { - let pair = Storage::pair_key(&candidate.first_id, &candidate.second_id); - pair == Storage::pair_key(&first.id, &third.id) - }) - .expect("untried first/third pair should remain a frontier candidate"); - - assert!( - candidate - .prior_outcomes - .iter() - .any(|outcome| outcome == "duplicate_risk"), - "frontier candidate should expose prior outcome labels from either member" - ); - assert_eq!(candidate.outcome_signal, "prior_duplicate_risk"); - assert!( - candidate.outcome_score_adjustment < 0.0, - "duplicate-risk history should reduce but not hide the untried lane" - ); - } - - #[test] - fn test_never_composed_marks_mixed_prior_outcomes() { - let storage = create_test_storage(); - let successful = storage - .ingest(IngestInput { - content: "Accepted release lane linked rollback evidence to install telemetry." - .to_string(), - node_type: "decision".to_string(), - tags: vec![ - "project:vestige".to_string(), - "release".to_string(), - "telemetry".to_string(), - ], - ..Default::default() - }) - .unwrap(); - let closed = storage - .ingest(IngestInput { - content: "Closed release lane linked install telemetry to out-of-scope claims." - .to_string(), - node_type: "incident".to_string(), - tags: vec![ - "project:vestige".to_string(), - "release".to_string(), - "telemetry".to_string(), - ], - ..Default::default() - }) - .unwrap(); - let success_helper = storage - .ingest(IngestInput { - content: "Helper memory for an accepted release composition.".to_string(), - node_type: "fact".to_string(), - tags: vec!["project:vestige".to_string(), "release".to_string()], - ..Default::default() - }) - .unwrap(); - let closed_helper = storage - .ingest(IngestInput { - content: "Helper memory for a closed release composition.".to_string(), - node_type: "fact".to_string(), - tags: vec!["project:vestige".to_string(), "release".to_string()], - ..Default::default() - }) - .unwrap(); - - storage - .save_composition( - &CompositionEventRecord { - id: "prior-success-composition".to_string(), - created_at: Utc::now(), - tool: "deep_reference".to_string(), - mode: "release".to_string(), - query: Some("accepted release lane".to_string()), - query_hash: Some("fnv1a64:success".to_string()), - confidence: Some(0.9), - status: Some("resolved".to_string()), - output_preview: None, - metadata: serde_json::json!({}), - }, - &[ - CompositionMemberRecord { - event_id: "prior-success-composition".to_string(), - memory_id: successful.id.clone(), - role: "primary".to_string(), - rank: 0, - trust: Some(0.9), - score: Some(0.9), - preview: None, - metadata: serde_json::json!({}), - }, - CompositionMemberRecord { - event_id: "prior-success-composition".to_string(), - memory_id: success_helper.id, - role: "supporting".to_string(), - rank: 1, - trust: Some(0.7), - score: Some(0.6), - preview: None, - metadata: serde_json::json!({}), - }, - ], - &[CompositionOutcomeRecord { - id: "prior-success-label".to_string(), - event_id: "prior-success-composition".to_string(), - outcome_type: "accepted".to_string(), - labeled_at: Utc::now(), - label_source: "test".to_string(), - confidence_delta: Some(0.2), - notes: None, - metadata: serde_json::json!({}), - }], - ) - .unwrap(); - - storage - .save_composition( - &CompositionEventRecord { - id: "prior-closed-composition".to_string(), - created_at: Utc::now(), - tool: "deep_reference".to_string(), - mode: "release".to_string(), - query: Some("closed release lane".to_string()), - query_hash: Some("fnv1a64:closed".to_string()), - confidence: Some(0.3), - status: Some("closed".to_string()), - output_preview: None, - metadata: serde_json::json!({}), - }, - &[ - CompositionMemberRecord { - event_id: "prior-closed-composition".to_string(), - memory_id: closed.id.clone(), - role: "primary".to_string(), - rank: 0, - trust: Some(0.8), - score: Some(0.7), - preview: None, - metadata: serde_json::json!({}), - }, - CompositionMemberRecord { - event_id: "prior-closed-composition".to_string(), - memory_id: closed_helper.id, - role: "supporting".to_string(), - rank: 1, - trust: Some(0.7), - score: Some(0.6), - preview: None, - metadata: serde_json::json!({}), - }, - ], - &[CompositionOutcomeRecord { - id: "prior-closed-label".to_string(), - event_id: "prior-closed-composition".to_string(), - outcome_type: "closed_by_scope".to_string(), - labeled_at: Utc::now(), - label_source: "test".to_string(), - confidence_delta: Some(-0.3), - notes: None, - metadata: serde_json::json!({}), - }], - ) - .unwrap(); - - let candidates = storage - .get_never_composed_candidates(10, Some(&["project".to_string()])) - .unwrap(); - let candidate = candidates - .iter() - .find(|candidate| { - let pair = Storage::pair_key(&candidate.first_id, &candidate.second_id); - pair == Storage::pair_key(&successful.id, &closed.id) - }) - .expect("untried success/closed pair should remain a frontier candidate"); - - assert_eq!(candidate.outcome_signal, "mixed_prior_outcomes"); - assert!( - candidate - .prior_outcomes - .iter() - .any(|outcome| outcome == "accepted") - ); - assert!( - candidate - .prior_outcomes - .iter() - .any(|outcome| outcome == "closed_by_scope") - ); - } - - #[test] - fn test_never_composed_surfaces_weak_tie_shared_terms_without_shared_tags() { - let storage = create_test_storage(); - let incident = storage - .ingest(IngestInput { - content: - "OpenCode handshake stalls when embedding startup blocks stdio negotiation." - .to_string(), - node_type: "incident".to_string(), - tags: vec!["opencode".to_string(), "startup".to_string()], - ..Default::default() - }) - .unwrap(); - let mitigation = storage - .ingest(IngestInput { - content: "JetBrains startup should keep embedding backfill behind the handshake." - .to_string(), - node_type: "mitigation".to_string(), - tags: vec!["jetbrains".to_string(), "background-work".to_string()], - ..Default::default() - }) - .unwrap(); - - let candidates = storage.get_never_composed_candidates(10, None).unwrap(); - let candidate = candidates - .iter() - .find(|candidate| { - let pair = Storage::pair_key(&candidate.first_id, &candidate.second_id); - pair == Storage::pair_key(&incident.id, &mitigation.id) - }) - .expect("shared terms should surface a weak-tie candidate without shared tags"); - - assert!( - candidate.shared_tags.is_empty(), - "test fixture intentionally has no shared tags" - ); - assert!( - candidate - .shared_terms - .iter() - .any(|term| term == "embedding" || term == "startup" || term == "handshake"), - "shared terms should explain the candidate" - ); - assert!( - candidate.bridge_score > 0.5, - "different tags and node types should create a bridge signal" - ); - } - #[test] fn test_dream_history_save_and_get_last() { let storage = create_test_storage(); @@ -11478,733 +4469,6 @@ mod tests { assert_eq!(count_future, 0); } - #[test] - fn test_portable_archive_exact_round_trip() { - let source_dir = tempdir().unwrap(); - let target_dir = tempdir().unwrap(); - let source = create_test_storage_at(&source_dir, "source.db"); - - let first = source - .ingest(IngestInput { - content: "Portable archive alpha memory".to_string(), - node_type: "fact".to_string(), - tags: vec!["portable".to_string()], - source: Some("test".to_string()), - ..Default::default() - }) - .unwrap(); - let second = source - .ingest(IngestInput { - content: "Portable archive beta memory".to_string(), - node_type: "fact".to_string(), - ..Default::default() - }) - .unwrap(); - source.mark_reviewed(&first.id, Rating::Good).unwrap(); - source - .save_connection(&ConnectionRecord { - source_id: first.id.clone(), - target_id: second.id.clone(), - strength: 0.75, - link_type: "semantic".to_string(), - created_at: Utc::now(), - last_activated: Utc::now(), - activation_count: 1, - }) - .unwrap(); - source - .save_composition( - &CompositionEventRecord { - id: "portable-composition-1".to_string(), - created_at: Utc::now(), - tool: "deep_reference".to_string(), - mode: "bounty".to_string(), - query: Some("portable composition".to_string()), - query_hash: Some("sha256:portable".to_string()), - confidence: Some(0.9), - status: Some("resolved".to_string()), - output_preview: Some("Portable composition event".to_string()), - metadata: serde_json::json!({}), - }, - &[ - CompositionMemberRecord { - event_id: "portable-composition-1".to_string(), - memory_id: first.id.clone(), - role: "primary".to_string(), - rank: 0, - trust: Some(0.9), - score: Some(1.0), - preview: Some("alpha".to_string()), - metadata: serde_json::json!({}), - }, - CompositionMemberRecord { - event_id: "portable-composition-1".to_string(), - memory_id: second.id.clone(), - role: "supporting".to_string(), - rank: 1, - trust: Some(0.8), - score: Some(0.8), - preview: Some("beta".to_string()), - metadata: serde_json::json!({}), - }, - ], - &[CompositionOutcomeRecord { - id: "portable-composition-outcome-1".to_string(), - event_id: "portable-composition-1".to_string(), - outcome_type: "helpful".to_string(), - labeled_at: Utc::now(), - label_source: "test".to_string(), - confidence_delta: None, - notes: None, - metadata: serde_json::json!({}), - }], - ) - .unwrap(); - - let archive = source.export_portable_archive().unwrap(); - assert_eq!(archive.archive_format, PORTABLE_ARCHIVE_FORMAT); - assert!(archive.total_rows() >= 3); - assert!( - archive - .tables - .iter() - .any(|table| table.name == "knowledge_nodes" && table.rows.len() == 2) - ); - for table_name in [ - "composition_events", - "composition_members", - "composition_outcomes", - ] { - assert!( - archive.tables.iter().any(|table| table.name == table_name), - "{table_name} must be included in portable archive" - ); - } - - let target = create_test_storage_at(&target_dir, "target.db"); - let report = target - .import_portable_archive(&archive, PortableImportMode::EmptyOnly) - .unwrap(); - assert!(report.rows_imported >= 3); - assert!(report.fts_rebuilt); - - let restored = target.get_node(&first.id).unwrap().unwrap(); - assert_eq!(restored.id, first.id); - assert_eq!(restored.content, first.content); - assert_eq!(restored.tags, first.tags); - assert_eq!(restored.reps, 1); - - let connections = target.get_connections_for_memory(&first.id).unwrap(); - assert_eq!(connections.len(), 1); - assert_eq!(connections[0].target_id, second.id); - - let composition = target - .get_composition_event("portable-composition-1") - .unwrap() - .unwrap(); - assert_eq!(composition.mode, "bounty"); - assert_eq!( - target - .get_composition_members("portable-composition-1") - .unwrap() - .len(), - 2 - ); - assert_eq!( - target - .get_composition_outcomes("portable-composition-1") - .unwrap() - .len(), - 1 - ); - - let results = target.search("alpha", 10).unwrap(); - assert_eq!(results.len(), 1); - assert_eq!(results[0].id, first.id); - } - - #[test] - fn test_portable_import_rejects_non_empty_target() { - let source_dir = tempdir().unwrap(); - let target_dir = tempdir().unwrap(); - let source = create_test_storage_at(&source_dir, "source.db"); - source - .ingest(IngestInput { - content: "Source memory".to_string(), - node_type: "fact".to_string(), - ..Default::default() - }) - .unwrap(); - let archive = source.export_portable_archive().unwrap(); - - let target = create_test_storage_at(&target_dir, "target.db"); - target - .ingest(IngestInput { - content: "Existing target memory".to_string(), - node_type: "fact".to_string(), - ..Default::default() - }) - .unwrap(); - - let err = target - .import_portable_archive(&archive, PortableImportMode::EmptyOnly) - .unwrap_err(); - assert!( - err.to_string() - .contains("requires an empty target database") - ); - } - - #[test] - fn test_portable_import_rejects_unknown_mode() { - let source_dir = tempdir().unwrap(); - let target_dir = tempdir().unwrap(); - let source = create_test_storage_at(&source_dir, "source.db"); - source - .ingest(IngestInput { - content: "Source memory".to_string(), - node_type: "fact".to_string(), - ..Default::default() - }) - .unwrap(); - let mut archive = source.export_portable_archive().unwrap(); - archive.mode = "merge".to_string(); - - let target = create_test_storage_at(&target_dir, "target.db"); - let err = target - .import_portable_archive(&archive, PortableImportMode::EmptyOnly) - .unwrap_err(); - assert!( - err.to_string() - .contains("Unsupported portable archive mode") - ); - } - - #[test] - fn test_portable_import_rejects_malformed_table_list() { - let source_dir = tempdir().unwrap(); - let target_dir = tempdir().unwrap(); - let source = create_test_storage_at(&source_dir, "source.db"); - source - .ingest(IngestInput { - content: "Source memory".to_string(), - node_type: "fact".to_string(), - ..Default::default() - }) - .unwrap(); - - let mut duplicate_archive = source.export_portable_archive().unwrap(); - let duplicate_table = duplicate_archive - .tables - .iter() - .find(|table| table.name == "knowledge_nodes") - .unwrap() - .clone(); - duplicate_archive.tables.push(duplicate_table); - - let target = create_test_storage_at(&target_dir, "target-duplicate.db"); - let err = target - .import_portable_archive(&duplicate_archive, PortableImportMode::EmptyOnly) - .unwrap_err(); - assert!( - err.to_string() - .contains("Portable archive contains duplicate table") - ); - - let mut unknown_archive = source.export_portable_archive().unwrap(); - unknown_archive.tables.push(PortableTable { - name: "sqlite_sequence".to_string(), - columns: vec!["name".to_string(), "seq".to_string()], - rows: vec![], - }); - - let target = create_test_storage_at(&target_dir, "target-unknown.db"); - let err = target - .import_portable_archive(&unknown_archive, PortableImportMode::EmptyOnly) - .unwrap_err(); - assert!( - err.to_string() - .contains("Portable archive contains unsupported table") - ); - } - - #[test] - fn test_portable_merge_import_combines_non_empty_databases() { - let source_dir = tempdir().unwrap(); - let target_dir = tempdir().unwrap(); - let source = create_test_storage_at(&source_dir, "source.db"); - let target = create_test_storage_at(&target_dir, "target.db"); - - let source_node = source - .ingest(IngestInput { - content: "Source sync memory".to_string(), - node_type: "fact".to_string(), - tags: vec!["sync".to_string()], - ..Default::default() - }) - .unwrap(); - let target_node = target - .ingest(IngestInput { - content: "Target local memory".to_string(), - node_type: "fact".to_string(), - tags: vec!["local".to_string()], - ..Default::default() - }) - .unwrap(); - - let archive = source.export_portable_archive().unwrap(); - let report = target - .import_portable_archive(&archive, PortableImportMode::Merge) - .unwrap(); - - assert!(report.rows_inserted > 0); - assert!(target.get_node(&source_node.id).unwrap().is_some()); - assert!(target.get_node(&target_node.id).unwrap().is_some()); - } - - #[test] - fn test_portable_merge_import_keeps_newer_local_memory() { - let source_dir = tempdir().unwrap(); - let target_dir = tempdir().unwrap(); - let source = create_test_storage_at(&source_dir, "source.db"); - let target = create_test_storage_at(&target_dir, "target.db"); - - let node = source - .ingest(IngestInput { - content: "Original shared memory".to_string(), - node_type: "fact".to_string(), - ..Default::default() - }) - .unwrap(); - let archive = source.export_portable_archive().unwrap(); - target - .import_portable_archive(&archive, PortableImportMode::EmptyOnly) - .unwrap(); - - let newer = (Utc::now() + Duration::hours(1)).to_rfc3339(); - { - let writer = target.writer.lock().unwrap(); - writer - .execute( - "UPDATE knowledge_nodes SET content = ?1, updated_at = ?2 WHERE id = ?3", - params!["Newer local edit", newer, &node.id], - ) - .unwrap(); - } - - let report = target - .import_portable_archive(&archive, PortableImportMode::Merge) - .unwrap(); - - assert!(report.conflicts_kept_local >= 1); - let restored = target.get_node(&node.id).unwrap().unwrap(); - assert_eq!(restored.content, "Newer local edit"); - } - - #[test] - fn test_portable_merge_import_keeps_children_for_newer_local_memory() { - let source_dir = tempdir().unwrap(); - let target_dir = tempdir().unwrap(); - let source = create_test_storage_at(&source_dir, "source.db"); - let target = create_test_storage_at(&target_dir, "target.db"); - - let node = source - .ingest(IngestInput { - content: "Shared parent with child rows".to_string(), - node_type: "fact".to_string(), - ..Default::default() - }) - .unwrap(); - - let source_time = Utc::now().to_rfc3339(); - { - let writer = source.writer.lock().unwrap(); - writer - .execute( - "INSERT OR REPLACE INTO node_embeddings - (node_id, embedding, dimensions, model, created_at) - VALUES (?1, ?2, ?3, ?4, ?5)", - params![&node.id, vec![1_u8, 2, 3, 4], 4, "test-model", &source_time], - ) - .unwrap(); - writer - .execute( - "INSERT OR REPLACE INTO fsrs_cards - (memory_id, difficulty, stability, state, reps, lapses) - VALUES (?1, ?2, ?3, ?4, ?5, ?6)", - params![&node.id, 3.0_f64, 2.0_f64, "review", 2_i64, 0_i64], - ) - .unwrap(); - writer - .execute( - "INSERT OR REPLACE INTO memory_states - (memory_id, state, last_access, access_count, state_entered_at) - VALUES (?1, ?2, ?3, ?4, ?5)", - params![&node.id, "active", &source_time, 1_i64, &source_time], - ) - .unwrap(); - } - - let archive = source.export_portable_archive().unwrap(); - target - .import_portable_archive(&archive, PortableImportMode::EmptyOnly) - .unwrap(); - - let local_time = (Utc::now() + Duration::hours(1)).to_rfc3339(); - { - let writer = target.writer.lock().unwrap(); - writer - .execute( - "UPDATE knowledge_nodes SET content = ?1, updated_at = ?2 WHERE id = ?3", - params!["Newer local parent edit", &local_time, &node.id], - ) - .unwrap(); - writer - .execute( - "INSERT OR REPLACE INTO node_embeddings - (node_id, embedding, dimensions, model, created_at) - VALUES (?1, ?2, ?3, ?4, ?5)", - params![&node.id, vec![9_u8, 8, 7, 6], 4, "test-model", &local_time], - ) - .unwrap(); - writer - .execute( - "INSERT OR REPLACE INTO fsrs_cards - (memory_id, difficulty, stability, state, reps, lapses) - VALUES (?1, ?2, ?3, ?4, ?5, ?6)", - params![&node.id, 9.0_f64, 8.0_f64, "review", 9_i64, 1_i64], - ) - .unwrap(); - writer - .execute( - "INSERT OR REPLACE INTO memory_states - (memory_id, state, last_access, access_count, state_entered_at) - VALUES (?1, ?2, ?3, ?4, ?5)", - params![&node.id, "silent", &local_time, 42_i64, &local_time], - ) - .unwrap(); - } - - let report = target - .import_portable_archive(&archive, PortableImportMode::Merge) - .unwrap(); - - assert!(report.conflicts_kept_local >= 4); - let restored = target.get_node(&node.id).unwrap().unwrap(); - assert_eq!(restored.content, "Newer local parent edit"); - - let reader = target.reader.lock().unwrap(); - let embedding: Vec = reader - .query_row( - "SELECT embedding FROM node_embeddings WHERE node_id = ?1", - params![&node.id], - |row| row.get(0), - ) - .unwrap(); - assert_eq!(embedding, vec![9_u8, 8, 7, 6]); - - let difficulty: f64 = reader - .query_row( - "SELECT difficulty FROM fsrs_cards WHERE memory_id = ?1", - params![&node.id], - |row| row.get(0), - ) - .unwrap(); - assert_eq!(difficulty, 9.0); - - let (state, access_count): (String, i64) = reader - .query_row( - "SELECT state, access_count FROM memory_states WHERE memory_id = ?1", - params![&node.id], - |row| Ok((row.get(0)?, row.get(1)?)), - ) - .unwrap(); - assert_eq!(state, "silent"); - assert_eq!(access_count, 42); - } - - #[test] - fn test_portable_merge_import_keeps_composition_members_for_newer_local_memory() { - let source_dir = tempdir().unwrap(); - let target_dir = tempdir().unwrap(); - let source = create_test_storage_at(&source_dir, "source.db"); - let target = create_test_storage_at(&target_dir, "target.db"); - - let node = source - .ingest(IngestInput { - content: "Shared memory with historical composition".to_string(), - node_type: "fact".to_string(), - tags: vec!["protocolgate".to_string()], - ..Default::default() - }) - .unwrap(); - source - .save_composition( - &CompositionEventRecord { - id: "merge-composition-1".to_string(), - created_at: Utc::now(), - tool: "deep_reference".to_string(), - mode: "bounty".to_string(), - query: Some("historical composition".to_string()), - query_hash: Some("sha256:historical".to_string()), - confidence: Some(0.7), - status: Some("resolved".to_string()), - output_preview: Some("Historical composition survives merge".to_string()), - metadata: serde_json::json!({}), - }, - &[CompositionMemberRecord { - event_id: "merge-composition-1".to_string(), - memory_id: node.id.clone(), - role: "primary".to_string(), - rank: 0, - trust: Some(0.8), - score: Some(0.9), - preview: Some("historical".to_string()), - metadata: serde_json::json!({}), - }], - &[], - ) - .unwrap(); - - let archive = source.export_portable_archive().unwrap(); - target - .import_portable_archive(&archive, PortableImportMode::EmptyOnly) - .unwrap(); - - let local_time = (Utc::now() + Duration::hours(1)).to_rfc3339(); - { - let writer = target.writer.lock().unwrap(); - writer - .execute( - "DELETE FROM composition_members WHERE event_id = ?1", - params!["merge-composition-1"], - ) - .unwrap(); - writer - .execute( - "UPDATE knowledge_nodes SET content = ?1, updated_at = ?2 WHERE id = ?3", - params!["Newer local content", &local_time, &node.id], - ) - .unwrap(); - } - - target - .import_portable_archive(&archive, PortableImportMode::Merge) - .unwrap(); - - let restored = target.get_node(&node.id).unwrap().unwrap(); - assert_eq!(restored.content, "Newer local content"); - let members = target - .get_composition_members("merge-composition-1") - .unwrap(); - assert_eq!(members.len(), 1); - assert_eq!(members[0].memory_id, node.id); - } - - #[test] - fn test_portable_merge_import_applies_delete_tombstones() { - let source_dir = tempdir().unwrap(); - let target_dir = tempdir().unwrap(); - let source = create_test_storage_at(&source_dir, "source.db"); - let target = create_test_storage_at(&target_dir, "target.db"); - - let node = source - .ingest(IngestInput { - content: "Memory deleted on source".to_string(), - node_type: "fact".to_string(), - ..Default::default() - }) - .unwrap(); - let archive = source.export_portable_archive().unwrap(); - target - .import_portable_archive(&archive, PortableImportMode::EmptyOnly) - .unwrap(); - assert!(target.get_node(&node.id).unwrap().is_some()); - - source.delete_node(&node.id).unwrap(); - let delete_archive = source.export_portable_archive().unwrap(); - let report = target - .import_portable_archive(&delete_archive, PortableImportMode::Merge) - .unwrap(); - - assert!(report.rows_deleted >= 1); - assert!(target.get_node(&node.id).unwrap().is_none()); - } - - #[test] - fn test_portable_merge_import_preserves_purge_tombstones() { - let source_dir = tempdir().unwrap(); - let target_dir = tempdir().unwrap(); - let source = create_test_storage_at(&source_dir, "source.db"); - let target = create_test_storage_at(&target_dir, "target.db"); - - let node = source - .ingest(IngestInput { - content: "Memory purged on source".to_string(), - node_type: "fact".to_string(), - tags: vec!["sync".to_string()], - ..Default::default() - }) - .unwrap(); - source - .save_composition( - &CompositionEventRecord { - id: "portable-purge-composition".to_string(), - created_at: Utc::now(), - tool: "deep_reference".to_string(), - mode: "sync".to_string(), - query: Some("portable purge preview".to_string()), - query_hash: Some("fnv1a64:portable-purge".to_string()), - confidence: Some(0.7), - status: Some("resolved".to_string()), - output_preview: None, - metadata: serde_json::json!({}), - }, - &[CompositionMemberRecord { - event_id: "portable-purge-composition".to_string(), - memory_id: node.id.clone(), - role: "primary".to_string(), - rank: 0, - trust: Some(0.8), - score: Some(0.8), - preview: Some("Portable purge composition preview leak".to_string()), - metadata: serde_json::json!({}), - }], - &[], - ) - .unwrap(); - let archive = source.export_portable_archive().unwrap(); - target - .import_portable_archive(&archive, PortableImportMode::EmptyOnly) - .unwrap(); - assert!(target.get_node(&node.id).unwrap().is_some()); - assert_eq!( - target - .get_composition_members("portable-purge-composition") - .unwrap()[0] - .preview - .as_deref(), - Some("Portable purge composition preview leak") - ); - - source - .purge_node(&node.id, Some("sync purge test")) - .unwrap(); - let purge_archive = source.export_portable_archive().unwrap(); - assert!( - !serde_json::to_string(&purge_archive) - .unwrap() - .contains("Portable purge composition preview leak"), - "source portable archive should not retain purged composition previews" - ); - let report = target - .import_portable_archive(&purge_archive, PortableImportMode::Merge) - .unwrap(); - - assert!(report.rows_deleted >= 1); - assert!(target.get_node(&node.id).unwrap().is_none()); - assert!( - target - .get_composition_members("portable-purge-composition") - .unwrap()[0] - .preview - .is_none(), - "portable purge merge should scrub target composition previews" - ); - - let writer = target.writer.lock().unwrap(); - let tombstone_count: i64 = writer - .query_row( - "SELECT COUNT(*) FROM deletion_tombstones WHERE memory_id = ?1 AND reason = ?2", - params![node.id, "sync purge test"], - |row| row.get(0), - ) - .unwrap(); - assert_eq!(tombstone_count, 1); - } - - #[test] - fn test_portable_merge_import_purge_wins_over_newer_local_edit() { - let source_dir = tempdir().unwrap(); - let target_dir = tempdir().unwrap(); - let source = create_test_storage_at(&source_dir, "source.db"); - let target = create_test_storage_at(&target_dir, "target.db"); - - let node = source - .ingest(IngestInput { - content: "Memory that will be purged on source".to_string(), - node_type: "fact".to_string(), - tags: vec!["sync".to_string()], - ..Default::default() - }) - .unwrap(); - let archive = source.export_portable_archive().unwrap(); - target - .import_portable_archive(&archive, PortableImportMode::EmptyOnly) - .unwrap(); - - let newer = (Utc::now() + Duration::hours(1)).to_rfc3339(); - { - let writer = target.writer.lock().unwrap(); - writer - .execute( - "UPDATE knowledge_nodes SET content = ?1, updated_at = ?2 WHERE id = ?3", - params!["Newer local edit before purge arrives", newer, &node.id], - ) - .unwrap(); - } - - source - .purge_node(&node.id, Some("hard purge wins sync conflict")) - .unwrap(); - let purge_archive = source.export_portable_archive().unwrap(); - let report = target - .import_portable_archive(&purge_archive, PortableImportMode::Merge) - .unwrap(); - - assert!(report.rows_deleted >= 1); - assert!(target.get_node(&node.id).unwrap().is_none()); - } - - #[test] - fn test_file_portable_sync_round_trips_between_devices() { - let sync_dir = tempdir().unwrap(); - let first_dir = tempdir().unwrap(); - let second_dir = tempdir().unwrap(); - let sync_path = sync_dir.path().join("vestige-sync.json"); - let first = create_test_storage_at(&first_dir, "first.db"); - let second = create_test_storage_at(&second_dir, "second.db"); - - let first_node = first - .ingest(IngestInput { - content: "First device memory".to_string(), - node_type: "fact".to_string(), - tags: vec!["sync".to_string()], - ..Default::default() - }) - .unwrap(); - let first_push = first.sync_portable_archive_file(&sync_path).unwrap(); - assert!(!first_push.pulled); - assert!(sync_path.exists()); - - let second_node = second - .ingest(IngestInput { - content: "Second device memory".to_string(), - node_type: "fact".to_string(), - tags: vec!["sync".to_string()], - ..Default::default() - }) - .unwrap(); - let second_sync = second.sync_portable_archive_file(&sync_path).unwrap(); - assert!(second_sync.pulled); - assert!(second.get_node(&first_node.id).unwrap().is_some()); - - let first_sync = first.sync_portable_archive_file(&sync_path).unwrap(); - assert!(first_sync.pulled); - assert!(first.get_node(&second_node.id).unwrap().is_some()); - assert!(first_sync.pushed_rows >= 2); - } - #[test] fn test_get_last_backup_timestamp_no_panic() { // Static method should not panic even if no backups exist @@ -12362,1560 +4626,4 @@ mod tests { assert!(!results.is_empty()); assert!(results[0].node.content.contains("Neurons")); } - - #[test] - fn test_concrete_search_literal_identifier_lands_first() { - let storage = create_test_storage(); - - storage - .ingest(IngestInput { - content: "General OpenAI API setup notes without the exact env var".to_string(), - node_type: "fact".to_string(), - ..Default::default() - }) - .unwrap(); - let target = storage - .ingest(IngestInput { - content: "Set OPENAI_API_KEY before running the release smoke tests".to_string(), - node_type: "fact".to_string(), - ..Default::default() - }) - .unwrap(); - storage - .ingest(IngestInput { - content: "API keys should be handled carefully in shell profiles".to_string(), - node_type: "fact".to_string(), - ..Default::default() - }) - .unwrap(); - - let results = storage - .concrete_search_filtered("OPENAI_API_KEY", 10, None, None) - .unwrap(); - - assert!(!results.is_empty()); - assert_eq!(results[0].node.id, target.id); - assert_eq!(results[0].match_type, MatchType::Keyword); - assert!(results[0].semantic_score.is_none()); - } - - #[test] - fn test_purge_scrubs_insight_json_orphans_children_and_writes_tombstone() { - let storage = create_test_storage(); - let doomed = storage - .ingest(IngestInput { - content: "Sensitive purge target memory".to_string(), - node_type: "fact".to_string(), - tags: vec!["sensitive".to_string()], - ..Default::default() - }) - .unwrap(); - let other_a = storage - .ingest(IngestInput { - content: "Other source memory A".to_string(), - node_type: "fact".to_string(), - ..Default::default() - }) - .unwrap(); - let other_b = storage - .ingest(IngestInput { - content: "Other source memory B".to_string(), - node_type: "fact".to_string(), - ..Default::default() - }) - .unwrap(); - let child = storage - .ingest(IngestInput { - content: "Temporal summary child".to_string(), - node_type: "summary".to_string(), - ..Default::default() - }) - .unwrap(); - - { - let writer = storage.writer.lock().unwrap(); - writer - .execute( - "INSERT INTO memory_connections ( - source_id, target_id, strength, link_type, created_at, last_activated, activation_count - ) VALUES (?1, ?2, 0.9, 'semantic', ?3, ?3, 0)", - params![doomed.id, other_a.id, Utc::now().to_rfc3339()], - ) - .unwrap(); - writer - .execute( - "INSERT INTO insights ( - id, insight, source_memories, confidence, novelty_score, insight_type, generated_at - ) VALUES (?1, 'drop me', ?2, 0.9, 0.2, 'synthesis', ?3)", - params![ - Uuid::new_v4().to_string(), - serde_json::to_string(&vec![doomed.id.clone(), other_a.id.clone()]).unwrap(), - Utc::now().to_rfc3339() - ], - ) - .unwrap(); - writer - .execute( - "INSERT INTO insights ( - id, insight, source_memories, confidence, novelty_score, insight_type, generated_at - ) VALUES (?1, 'rewrite me', ?2, 0.9, 0.2, 'synthesis', ?3)", - params![ - Uuid::new_v4().to_string(), - serde_json::to_string(&vec![ - doomed.id.clone(), - other_a.id.clone(), - other_b.id.clone() - ]) - .unwrap(), - Utc::now().to_rfc3339() - ], - ) - .unwrap(); - writer - .execute( - "UPDATE knowledge_nodes SET summary_parent_id = ?1 WHERE id = ?2", - params![doomed.id, child.id], - ) - .unwrap(); - } - - storage - .save_composition( - &CompositionEventRecord { - id: "purge-composition-preview-test".to_string(), - created_at: Utc::now(), - tool: "deep_reference".to_string(), - mode: "audit".to_string(), - query: Some("purge preview leak".to_string()), - query_hash: Some("fnv1a64:purge".to_string()), - confidence: Some(0.7), - status: Some("resolved".to_string()), - output_preview: None, - metadata: serde_json::json!({}), - }, - &[CompositionMemberRecord { - event_id: "purge-composition-preview-test".to_string(), - memory_id: doomed.id.clone(), - role: "primary".to_string(), - rank: 0, - trust: Some(0.8), - score: Some(0.9), - preview: Some("Sensitive purge target memory preview leak".to_string()), - metadata: serde_json::json!({}), - }], - &[], - ) - .unwrap(); - - let report = storage - .purge_node(&doomed.id, Some("user requested hard purge")) - .unwrap(); - assert!(report.deleted); - assert_eq!(report.edges_pruned, 1); - assert_eq!(report.insights_deleted, 1); - assert_eq!(report.insights_rewritten, 1); - assert_eq!(report.children_orphaned, 1); - assert!(storage.get_node(&doomed.id).unwrap().is_none()); - - let writer = storage.writer.lock().unwrap(); - let remaining_refs: Vec = writer - .prepare("SELECT source_memories FROM insights") - .unwrap() - .query_map([], |row| row.get(0)) - .unwrap() - .filter_map(|row| row.ok()) - .collect(); - assert_eq!(remaining_refs.len(), 1); - assert!(!remaining_refs[0].contains(&doomed.id)); - - let child_parent: Option = writer - .query_row( - "SELECT summary_parent_id FROM knowledge_nodes WHERE id = ?1", - params![child.id], - |row| row.get(0), - ) - .unwrap(); - assert!(child_parent.is_none()); - - let tombstone_count: i64 = writer - .query_row( - "SELECT COUNT(*) FROM deletion_tombstones WHERE memory_id = ?1 AND reason = ?2", - params![doomed.id, "user requested hard purge"], - |row| row.get(0), - ) - .unwrap(); - assert_eq!(tombstone_count, 1); - - let members = storage - .get_composition_members("purge-composition-preview-test") - .unwrap(); - assert_eq!(members.len(), 1); - assert!( - members[0].preview.is_none(), - "purge should scrub composition member previews for the purged memory" - ); - let archive_json = - serde_json::to_string(&storage.export_portable_archive().unwrap()).unwrap(); - assert!( - !archive_json.contains("Sensitive purge target memory preview leak"), - "portable archive should not retain purged memory content through composition previews" - ); - - let has_content_column: i64 = writer - .query_row( - "SELECT COUNT(*) FROM pragma_table_info('deletion_tombstones') WHERE name = 'content'", - [], - |row| row.get(0), - ) - .unwrap(); - assert_eq!(has_content_column, 0); - } - - // ======================================================================== - // Merge / Supersede controls (Phase 3 — v2.1.25) - // - // These exercise the full lifecycle without the live embedding model by - // seeding the `node_embeddings` table directly with the ACTIVE model name, - // so `get_all_embeddings` / `get_node_embedding` accept them. - // ======================================================================== - - /// Ingest a node and seed it with a controllable embedding under the active - /// model so similarity is deterministic in tests. - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - fn seed_node(storage: &Storage, content: &str, tags: &[&str], vector: Vec) -> String { - let node = storage - .ingest(IngestInput { - content: content.to_string(), - node_type: "fact".to_string(), - tags: tags.iter().map(|t| t.to_string()).collect(), - ..Default::default() - }) - .unwrap(); - let bytes = Embedding::new(vector).to_bytes(); - let active = storage.embedding_service.model_name().to_string(); - let writer = storage.writer.lock().unwrap(); - writer - .execute( - "INSERT OR REPLACE INTO node_embeddings - (node_id, embedding, dimensions, model, created_at) - VALUES (?1, ?2, ?3, ?4, ?5)", - rusqlite::params![ - &node.id, - &bytes, - EMBEDDING_DIMENSIONS as i32, - active, - Utc::now().to_rfc3339() - ], - ) - .unwrap(); - writer - .execute( - "UPDATE knowledge_nodes SET has_embedding = 1 WHERE id = ?1", - rusqlite::params![&node.id], - ) - .unwrap(); - node.id - } - - /// A near-unit vector pointing mostly along `axis`, so two nodes sharing an - /// axis are highly similar and nodes on different axes are not. - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - fn axis_vector(axis: usize, jitter: f32) -> Vec { - let mut v = vec![0.0f32; EMBEDDING_DIMENSIONS]; - v[axis % EMBEDDING_DIMENSIONS] = 1.0; - v[(axis + 1) % EMBEDDING_DIMENSIONS] = jitter; - v - } - - // ========================================================================= - // Phase 1 trait-method unit tests - // ========================================================================= - use crate::storage::memory_store::{ - MemoryEdge, MemoryRecord, MemoryStore, MemoryStoreError, ModelSignature, SchedulingState, - }; - - fn make_record(content: &str) -> MemoryRecord { - MemoryRecord { - id: uuid::Uuid::new_v4(), - domains: vec![], - domain_scores: Default::default(), - content: content.to_string(), - node_type: "fact".to_string(), - tags: vec!["test".to_string()], - embedding: None, - created_at: chrono::Utc::now(), - updated_at: chrono::Utc::now(), - metadata: serde_json::json!({}), - } - } - - fn rt() -> tokio::runtime::Runtime { - tokio::runtime::Runtime::new().unwrap() - } - - #[test] - fn trait_init_is_idempotent() { - let s = create_test_storage(); - let rt = rt(); - rt.block_on(async { - s.init().await.unwrap(); - s.init().await.unwrap(); - }); - } - - #[test] - fn trait_health_check_reports_healthy_on_fresh_db() { - let s = create_test_storage(); - let rt = rt(); - rt.block_on(async { - let h = s.health_check().await.unwrap(); - assert!(matches!( - h, - crate::storage::memory_store::HealthStatus::Healthy - )); - }); - } - - #[test] - fn trait_register_model_first_write_succeeds() { - let s = create_test_storage(); - let sig = ModelSignature { - name: "test-model".to_string(), - dimension: 256, - hash: "a".repeat(64), - }; - let rt = rt(); - rt.block_on(async { - s.register_model(&sig).await.unwrap(); - let got = s.registered_model().await.unwrap(); - assert_eq!(got, Some(sig)); - }); - } - - #[test] - fn trait_register_model_mismatched_write_refused() { - let s = create_test_storage(); - let sig = ModelSignature { - name: "model-a".to_string(), - dimension: 256, - hash: "a".repeat(64), - }; - let sig2 = ModelSignature { - name: "model-b".to_string(), - dimension: 256, - hash: "b".repeat(64), - }; - let rt = rt(); - rt.block_on(async { - s.register_model(&sig).await.unwrap(); - let err = s.register_model(&sig2).await.unwrap_err(); - assert!(matches!(err, MemoryStoreError::ModelMismatch { .. })); - }); - } - - #[test] - fn trait_register_model_same_signature_idempotent() { - let s = create_test_storage(); - let sig = ModelSignature { - name: "test-model".to_string(), - dimension: 256, - hash: "a".repeat(64), - }; - let rt = rt(); - rt.block_on(async { - s.register_model(&sig).await.unwrap(); - s.register_model(&sig).await.unwrap(); // second call must not error - }); - } - - #[test] - fn trait_insert_returns_uuid() { - let s = create_test_storage(); - let rec = make_record("test content"); - let expected_id = rec.id; - let rt = rt(); - rt.block_on(async { - let got = s.insert(&rec).await.unwrap(); - assert_eq!(got, expected_id); - }); - } - - #[test] - fn trait_get_missing_returns_none() { - let s = create_test_storage(); - let rt = rt(); - rt.block_on(async { - let got = s.get(uuid::Uuid::new_v4()).await.unwrap(); - assert!(got.is_none()); - }); - } - - #[test] - fn trait_get_after_insert_round_trip() { - let s = create_test_storage(); - let rec = make_record("round trip content"); - let id = rec.id; - let rt = rt(); - rt.block_on(async { - s.insert(&rec).await.unwrap(); - let got = s.get(id).await.unwrap().unwrap(); - assert_eq!(got.content, "round trip content"); - assert_eq!(got.node_type, "fact"); - assert!(got.domains.is_empty()); - assert!(got.domain_scores.is_empty()); - }); - } - - #[test] - fn trait_update_modifies_content() { - let s = create_test_storage(); - let rec = make_record("original content"); - let id = rec.id; - let rt = rt(); - rt.block_on(async { - s.insert(&rec).await.unwrap(); - let mut updated = s.get(id).await.unwrap().unwrap(); - updated.content = "updated content".to_string(); - s.update(&updated).await.unwrap(); - let got = s.get(id).await.unwrap().unwrap(); - assert_eq!(got.content, "updated content"); - }); - } - - #[test] - fn trait_delete_removes_record() { - let s = create_test_storage(); - let rec = make_record("to be deleted"); - let id = rec.id; - let rt = rt(); - rt.block_on(async { - s.insert(&rec).await.unwrap(); - s.delete(id).await.unwrap(); - let got = s.get(id).await.unwrap(); - assert!(got.is_none()); - }); - } - - #[test] - fn trait_fts_search_returns_tokens_match() { - let s = create_test_storage(); - let rt = rt(); - rt.block_on(async { - let rec = make_record("mitochondria powerhouse cell energy"); - s.insert(&rec).await.unwrap(); - let results = s.fts_search("mitochondria", 10).await.unwrap(); - assert!(!results.is_empty()); - }); - } - - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - #[test] - fn test_merge_candidates_threshold_classification() { - let storage = create_test_storage(); - // Two near-identical (same axis) — should be offered as a candidate. - let a = seed_node( - &storage, - "Use tokio runtime for async Rust services", - &["rust", "async"], - axis_vector(3, 0.02), - ); - let b = seed_node( - &storage, - "Use the tokio runtime for async Rust services", - &["rust", "async"], - axis_vector(3, 0.01), - ); - // One unrelated (different axis) — must not join the cluster. - let _c = seed_node( - &storage, - "Prefer postgres for relational data", - &["db"], - axis_vector(200, 0.0), - ); - - let policy = MergePolicy::default(); - let candidates = storage.merge_candidates(policy, 20, &[]).unwrap(); - assert_eq!(candidates.len(), 1, "exactly one duplicate cluster"); - let cluster = &candidates[0]; - assert_eq!(cluster.member_ids.len(), 2); - assert!(cluster.member_ids.contains(&a)); - assert!(cluster.member_ids.contains(&b)); - assert!( - cluster.confidence >= policy.possible_threshold, - "confidence above possible threshold" - ); - assert!(!cluster.has_protected_member); - } - - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - #[test] - fn test_plan_merge_is_preview_only_no_mutation() { - let storage = create_test_storage(); - let a = seed_node( - &storage, - "Fact A about caching", - &["perf"], - axis_vector(5, 0.02), - ); - let b = seed_node( - &storage, - "Fact A about caching, expanded", - &["perf", "cache"], - axis_vector(5, 0.01), - ); - - let plan = storage - .plan_merge(&[a.clone(), b.clone()], None, MergePolicy::default()) - .unwrap(); - - // Plan diff is populated... - assert!(plan.result_content.contains("Fact A about caching")); - assert!(plan.result_tags.contains(&"cache".to_string())); - assert_eq!(plan.invalidated_ids.len(), 1); - - // ...but NOTHING changed: both nodes still valid, content untouched. - let na = storage.get_node(&a).unwrap().unwrap(); - let nb = storage.get_node(&b).unwrap().unwrap(); - assert_eq!(na.content, "Fact A about caching"); - assert_eq!(nb.content, "Fact A about caching, expanded"); - let (vu_a, sb_a) = storage.read_bitemporal(&a).unwrap(); - let (vu_b, sb_b) = storage.read_bitemporal(&b).unwrap(); - assert!(vu_a.is_none() && sb_a.is_none()); - assert!(vu_b.is_none() && sb_b.is_none()); - - // Plan persisted as pending. - assert_eq!( - storage.plan_status(&plan.id).unwrap().as_deref(), - Some("pending") - ); - } - - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - #[test] - fn test_apply_then_undo_merge_is_reversible() { - let storage = create_test_storage(); - let survivor = seed_node( - &storage, - "Keep this canonical note", - &["x"], - axis_vector(7, 0.02), - ); - let absorbed = seed_node( - &storage, - "Extra detail to fold in", - &["x", "y"], - axis_vector(7, 0.01), - ); - - let plan = storage - .plan_merge( - &[survivor.clone(), absorbed.clone()], - Some(&survivor), - MergePolicy::default(), - ) - .unwrap(); - let op = storage.apply_plan(&plan.id, true).unwrap(); - assert_eq!(op.op_type, "merge"); - - // After apply: survivor content merged, absorbed bitemporally invalidated - // but STILL QUERYABLE (never deleted). - let surv = storage.get_node(&survivor).unwrap().unwrap(); - assert!(surv.content.contains("Keep this canonical note")); - assert!(surv.content.contains("Extra detail to fold in")); - assert!(surv.tags.contains(&"y".to_string())); - - let (vu, sb) = storage.read_bitemporal(&absorbed).unwrap(); - assert!(vu.is_some(), "absorbed node stamped valid_until"); - assert_eq!(sb.as_deref(), Some(survivor.as_str())); - // Old node is still fully retrievable for audit. - assert!( - storage.get_node(&absorbed).unwrap().is_some(), - "superseded node remains queryable" - ); - assert!(storage.superseded_node_ids().unwrap().contains(&absorbed)); - - // Undo restores everything. - let undo = storage.merge_undo(&op.id).unwrap(); - assert_eq!(undo.op_type, "undo"); - let surv_after = storage.get_node(&survivor).unwrap().unwrap(); - assert_eq!(surv_after.content, "Keep this canonical note"); - let (vu2, sb2) = storage.read_bitemporal(&absorbed).unwrap(); - assert!( - vu2.is_none() && sb2.is_none(), - "invalidation cleared on undo" - ); - assert!(!storage.superseded_node_ids().unwrap().contains(&absorbed)); - - // The original op is now marked reverted; double-undo is rejected. - assert!(storage.merge_undo(&op.id).is_err()); - } - - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - #[test] - fn test_supersede_invalidates_old_but_keeps_it_queryable() { - let storage = create_test_storage(); - let old = seed_node(&storage, "LR should be 1e-4", &["ml"], axis_vector(9, 0.02)); - let new = seed_node( - &storage, - "Correction: LR should be 3e-4", - &["ml"], - axis_vector(9, 0.01), - ); - - let plan = storage - .plan_supersede(&old, &new, MergePolicy::default()) - .unwrap(); - // Preview did not mutate. - let (vu0, _) = storage.read_bitemporal(&old).unwrap(); - assert!(vu0.is_none()); - - let op = storage.apply_plan(&plan.id, true).unwrap(); - assert_eq!(op.op_type, "supersede"); - - let (vu, sb) = storage.read_bitemporal(&old).unwrap(); - assert!(vu.is_some(), "old stamped valid_until"); - assert_eq!(sb.as_deref(), Some(new.as_str())); - // New node untouched and valid. - let (vu_new, sb_new) = storage.read_bitemporal(&new).unwrap(); - assert!(vu_new.is_none() && sb_new.is_none()); - // Old still queryable for audit (invalidate, don't delete). - let old_node = storage.get_node(&old).unwrap().unwrap(); - assert_eq!(old_node.content, "LR should be 1e-4"); - - // And reversible. - storage.merge_undo(&op.id).unwrap(); - let (vu_r, sb_r) = storage.read_bitemporal(&old).unwrap(); - assert!(vu_r.is_none() && sb_r.is_none()); - } - - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - #[test] - fn test_protect_blocks_merge_away() { - let storage = create_test_storage(); - let pinned = seed_node( - &storage, - "Load-bearing fact", - &["pin"], - axis_vector(11, 0.02), - ); - let other = seed_node( - &storage, - "Load-bearing fact restated", - &["pin"], - axis_vector(11, 0.01), - ); - storage.set_protected(&pinned, true).unwrap(); - assert!(storage.is_protected(&pinned).unwrap()); - - // Protected node may not be merged AWAY (survivor=other). - let err = storage.plan_merge( - &[other.clone(), pinned.clone()], - Some(&other), - MergePolicy::default(), - ); - assert!(err.is_err(), "merging a protected node away must fail"); - - // But it CAN be the survivor. - let ok = storage.plan_merge( - &[pinned.clone(), other.clone()], - Some(&pinned), - MergePolicy::default(), - ); - assert!(ok.is_ok(), "protected node can be the survivor"); - - // Supersede of a protected node is also blocked. - assert!( - storage - .plan_supersede(&pinned, &other, MergePolicy::default()) - .is_err(), - "superseding a protected node must fail" - ); - - // merge_candidates flags the protected member. - let cands = storage - .merge_candidates(MergePolicy::default(), 20, &[]) - .unwrap(); - assert!(cands.iter().all(|c| c.has_protected_member)); - } - - // ======================================================================== - // Auto-consolidation merge: opt-out gate + protected-pin exclusion (#142) - // - // These exercise `auto_dedup_consolidation` directly — the unattended, - // no-audit pass the 6h background consolidation cycle runs. seed_node/ - // axis_vector give deterministic same-axis clusters (cosine ~1.0 >> the 0.85 - // threshold); set_retention pins down which node wins the keeper tiebreak. - // ======================================================================== - - /// Force a node's retention_strength so the keeper tiebreak in - /// `auto_dedup_consolidation` is deterministic regardless of insertion order. - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - fn set_retention(storage: &Storage, id: &str, value: f64) { - let writer = storage.writer.lock().unwrap(); - writer - .execute( - "UPDATE knowledge_nodes SET retention_strength = ?1 WHERE id = ?2", - rusqlite::params![value, id], - ) - .unwrap(); - } - - /// Run `f` with VESTIGE_AUTO_CONSOLIDATE_MERGE pinned to `value` - /// (None = pinned-unset, i.e. the documented ON default), serialized via - /// ENV_LOCK and restored afterward (process env is global + unsafe under - /// Rust 2024). Sibling of `with_vector_search_disabled`. - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - fn with_auto_merge_env(value: Option<&str>, f: impl FnOnce() -> T) -> T { - const KEY: &str = "VESTIGE_AUTO_CONSOLIDATE_MERGE"; - let _guard = ENV_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner); - let previous = std::env::var_os(KEY); - unsafe { - match value { - Some(v) => std::env::set_var(KEY, v), - None => std::env::remove_var(KEY), - } - } - let result = catch_unwind(AssertUnwindSafe(f)); - unsafe { - if let Some(prev) = previous { - std::env::set_var(KEY, prev); - } else { - std::env::remove_var(KEY); - } - } - match result { - Ok(value) => value, - Err(payload) => resume_unwind(payload), - } - } - - // --- A. Default (flag unset): near-duplicates still merge (regression) --- - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - #[test] - fn test_auto_dedup_default_on_merges_near_duplicates() { - with_auto_merge_env(None, || { - let storage = create_test_storage(); - let keeper = seed_node( - &storage, - "Rate limiting uses a token bucket per client API key", - &["api"], - axis_vector(21, 0.02), - ); - let dup = seed_node( - &storage, - "Rate limiting uses a token-bucket algorithm per client API key, refilled steadily", - &["api"], - axis_vector(21, 0.01), - ); - // Make `keeper` win the retention tiebreak deterministically. - set_retention(&storage, &keeper, 0.9); - set_retention(&storage, &dup, 0.3); - - let merged = storage.auto_dedup_consolidation().unwrap(); - assert_eq!(merged, 1, "one weak node folded into the keeper"); - assert!( - storage.get_node(&dup).unwrap().is_none(), - "weak duplicate is hard-deleted" - ); - let survivor = storage.get_node(&keeper).unwrap().unwrap(); - assert!( - survivor.content.contains("[MERGED]"), - "keeper carries the folded-in [MERGED] block" - ); - }); - } - - // --- B. Flag off suppresses the merge (parametrized) --------------------- - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - #[test] - fn test_auto_dedup_env_off_suppresses_merge() { - // trimmed + case-insensitive false/off/no/0 all disable. - for value in ["false", "off", "no", "0", " OFF ", "False"] { - with_auto_merge_env(Some(value), || { - let storage = create_test_storage(); - let a = seed_node( - &storage, - "Prometheus scrapes its targets every 15 seconds", - &["obs"], - axis_vector(23, 0.02), - ); - let b = seed_node( - &storage, - "Prometheus scrapes its configured targets every 15s by default", - &["obs"], - axis_vector(23, 0.01), - ); - - let merged = storage.auto_dedup_consolidation().unwrap(); - assert_eq!(merged, 0, "value {value:?} must suppress the merge"); - // Both nodes survive, content byte-identical (no [MERGED] block). - assert_eq!( - storage.get_node(&a).unwrap().unwrap().content, - "Prometheus scrapes its targets every 15 seconds" - ); - assert_eq!( - storage.get_node(&b).unwrap().unwrap().content, - "Prometheus scrapes its configured targets every 15s by default" - ); - }); - } - } - - // --- B (cont). A malformed value fails OPEN to the ON default ------------ - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - #[test] - fn test_auto_dedup_env_garbage_fails_open_and_merges() { - with_auto_merge_env(Some("banana"), || { - let storage = create_test_storage(); - let keeper = seed_node( - &storage, - "Cache entries expire after a five minute TTL", - &["cache"], - axis_vector(25, 0.02), - ); - let dup = seed_node( - &storage, - "Cache entries expire after a five-minute TTL window by default", - &["cache"], - axis_vector(25, 0.01), - ); - set_retention(&storage, &keeper, 0.9); - set_retention(&storage, &dup, 0.3); - - let merged = storage.auto_dedup_consolidation().unwrap(); - assert_eq!(merged, 1, "malformed value fails open to the ON default"); - assert!(storage.get_node(&dup).unwrap().is_none()); - }); - } - - // --- C(a). Protected would-be keeper: untouched; others merge ----------- - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - #[test] - fn test_auto_dedup_protected_would_be_keeper_untouched_others_merge() { - with_auto_merge_env(None, || { - let storage = create_test_storage(); - // P has the highest retention, so absent protection it would be the - // keeper. Protected → skipped entirely; the two unprotected merge alone. - let pinned = seed_node( - &storage, - "Deploys are gated on a green CI run and one approval", - &["ci"], - axis_vector(27, 0.02), - ); - let keeper = seed_node( - &storage, - "Deploys are gated on a green CI pipeline plus one reviewer approval", - &["ci"], - axis_vector(27, 0.01), - ); - let member = seed_node( - &storage, - "Deploys require a green CI run and at least one approving review", - &["ci"], - axis_vector(27, 0.015), - ); - set_retention(&storage, &pinned, 0.95); - set_retention(&storage, &keeper, 0.80); - set_retention(&storage, &member, 0.30); - storage.set_protected(&pinned, true).unwrap(); - let pinned_content = storage.get_node(&pinned).unwrap().unwrap().content; - - let merged = storage.auto_dedup_consolidation().unwrap(); - assert_eq!( - merged, - 1, - "the two unprotected near-dups merge among themselves" - ); - - // Protected node byte-for-byte untouched and still protected. - let p = storage.get_node(&pinned).unwrap().unwrap(); - assert_eq!(p.content, pinned_content, "protected keeper not absorbed"); - assert!(!p.content.contains("[MERGED]")); - assert!(storage.is_protected(&pinned).unwrap()); - // Unprotected pair merged: `member` gone, `keeper` carries [MERGED]. - assert!(storage.get_node(&member).unwrap().is_none()); - let keeper_node = storage.get_node(&keeper).unwrap().unwrap(); - assert!(keeper_node.content.contains("[MERGED]")); - }); - } - - // --- C(b) / Regression (#142): protected weak member is never absorbed -- - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - #[test] - fn auto_dedup_regression_142_protected_weak_member_not_absorbed() { - with_auto_merge_env(None, || { - let storage = create_test_storage(); - // Regression (#142): before the fix this pinned node — the weaker - // member of the cluster — was silently absorbed into the stronger - // unprotected keeper and hard-deleted by the unattended pass. The - // PINNED-CANARY-142 marker makes accidental absorption detectable. - let pinned = seed_node( - &storage, - "Feature flags default to off in production PINNED-CANARY-142", - &["flags"], - axis_vector(29, 0.02), - ); - let keeper = seed_node( - &storage, - "Feature flags default to off in the production environment", - &["flags"], - axis_vector(29, 0.01), - ); - let member = seed_node( - &storage, - "Feature flags are off by default in production deployments", - &["flags"], - axis_vector(29, 0.015), - ); - // Pinned is the LOWEST-retention member — pre-fix it would land in - // weak_ids and be deleted + absorbed by the keeper. - set_retention(&storage, &pinned, 0.10); - set_retention(&storage, &keeper, 0.80); - set_retention(&storage, &member, 0.30); - storage.set_protected(&pinned, true).unwrap(); - let pinned_content = storage.get_node(&pinned).unwrap().unwrap().content; - - let merged = storage.auto_dedup_consolidation().unwrap(); - assert_eq!(merged, 1, "only the two unprotected near-dups merge"); - - // Invariant 1: the protected node still exists, byte-identical. - let p = storage.get_node(&pinned).unwrap(); - assert!(p.is_some(), "protected node must not be deleted"); - assert_eq!( - p.unwrap().content, - pinned_content, - "protected node not absorbed" - ); - assert!(storage.is_protected(&pinned).unwrap()); - - // Invariant 2: the keeper did NOT gain the protected node's content. - let keeper_node = storage.get_node(&keeper).unwrap().unwrap(); - assert!( - !keeper_node.content.contains("PINNED-CANARY-142"), - "keeper must not absorb the protected node's content" - ); - // The legitimate unprotected pair still merged (member folded in). - assert!(storage.get_node(&member).unwrap().is_none()); - assert!(keeper_node.content.contains("[MERGED]")); - }); - } - - // --- C(c). Two protected near-dups: neither merges ---------------------- - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - #[test] - fn test_auto_dedup_two_protected_near_dups_neither_merges() { - with_auto_merge_env(None, || { - let storage = create_test_storage(); - let a = seed_node( - &storage, - "Backups run nightly and are retained for thirty days", - &["backup"], - axis_vector(31, 0.02), - ); - let b = seed_node( - &storage, - "Backups run every night and are kept for thirty days", - &["backup"], - axis_vector(31, 0.01), - ); - storage.set_protected(&a, true).unwrap(); - storage.set_protected(&b, true).unwrap(); - let (ca, cb) = ( - storage.get_node(&a).unwrap().unwrap().content, - storage.get_node(&b).unwrap().unwrap().content, - ); - - let merged = storage.auto_dedup_consolidation().unwrap(); - assert_eq!(merged, 0, "two protected near-dups: nothing merges"); - assert_eq!(storage.get_node(&a).unwrap().unwrap().content, ca); - assert_eq!(storage.get_node(&b).unwrap().unwrap().content, cb); - }); - } - - // --- C(d). Protected + a single unprotected near-dup: no merge ---------- - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - #[test] - fn test_auto_dedup_protected_plus_single_unprotected_no_merge() { - with_auto_merge_env(None, || { - let storage = create_test_storage(); - let pinned = seed_node( - &storage, - "Secrets are stored in the vault, never in the repo", - &["sec"], - axis_vector(33, 0.02), - ); - let other = seed_node( - &storage, - "Secrets live in the vault and are never committed to the repo", - &["sec"], - axis_vector(33, 0.01), - ); - storage.set_protected(&pinned, true).unwrap(); - let (cp, co) = ( - storage.get_node(&pinned).unwrap().unwrap().content, - storage.get_node(&other).unwrap().unwrap().content, - ); - - let merged = storage.auto_dedup_consolidation().unwrap(); - assert_eq!(merged, 0, "a lone unprotected node cannot form a cluster"); - assert_eq!(storage.get_node(&pinned).unwrap().unwrap().content, cp); - assert_eq!(storage.get_node(&other).unwrap().unwrap().content, co); - }); - } - - // --- D. Liveness: protected + two unprotected → the two merge ----------- - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - #[test] - fn test_auto_dedup_protected_plus_two_unprotected_liveness() { - with_auto_merge_env(None, || { - let storage = create_test_storage(); - // The pin exclusion must not block a legitimate merge of the others. - let pinned = seed_node( - &storage, - "The API returns ISO-8601 timestamps in UTC", - &["api"], - axis_vector(35, 0.02), - ); - let keeper = seed_node( - &storage, - "The API returns ISO 8601 timestamps in UTC by convention", - &["api"], - axis_vector(35, 0.01), - ); - let member = seed_node( - &storage, - "All API timestamps are returned as ISO-8601 in the UTC timezone", - &["api"], - axis_vector(35, 0.015), - ); - set_retention(&storage, &pinned, 0.50); - set_retention(&storage, &keeper, 0.80); - set_retention(&storage, &member, 0.30); - storage.set_protected(&pinned, true).unwrap(); - let pinned_content = storage.get_node(&pinned).unwrap().unwrap().content; - - let merged = storage.auto_dedup_consolidation().unwrap(); - assert_eq!(merged, 1, "the two unprotected near-dups still merge"); - assert!(storage.get_node(&member).unwrap().is_none()); - let keeper_node = storage.get_node(&keeper).unwrap().unwrap(); - assert!(keeper_node.content.contains("[MERGED]")); - // Protected node untouched. - assert_eq!( - storage.get_node(&pinned).unwrap().unwrap().content, - pinned_content - ); - assert!(storage.is_protected(&pinned).unwrap()); - }); - } - - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - #[test] - fn test_apply_requires_confirm_for_low_confidence() { - let storage = create_test_storage(); - // Tighten thresholds so a moderate pair lands in 'possible' (needs confirm). - let strict = MergePolicy::new(0.99, 0.5, false); - storage.set_merge_policy(strict).unwrap(); - - let a = seed_node(&storage, "Topic alpha note", &["t"], axis_vector(13, 0.30)); - let b = seed_node(&storage, "Topic alpha aside", &["t"], axis_vector(13, 0.60)); - let plan = storage - .plan_merge(&[a, b], None, storage.get_merge_policy().unwrap()) - .unwrap(); - assert_ne!(plan.classification, MatchClass::Match); - - // Without confirm => rejected. - assert!(storage.apply_plan(&plan.id, false).is_err()); - // With confirm => applied. - assert!(storage.apply_plan(&plan.id, true).is_ok()); - // Re-applying an applied plan => rejected. - assert!(storage.apply_plan(&plan.id, true).is_err()); - } - - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - #[test] - fn test_merge_policy_roundtrip_persists() { - let storage = create_test_storage(); - let p = MergePolicy::new(0.9, 0.6, true); - storage.set_merge_policy(p).unwrap(); - let got = storage.get_merge_policy().unwrap(); - assert!((got.match_threshold - 0.9).abs() < 1e-6); - assert!((got.possible_threshold - 0.6).abs() < 1e-6); - assert!(got.auto_apply); - } - - #[test] - fn test_set_protected_unknown_node_errors() { - let storage = create_test_storage(); - assert!(storage.set_protected("does-not-exist", true).is_err()); - } - - #[test] - fn trait_hybrid_search_multi_word_via_insert() { - // Verify that hybrid_search finds records inserted via the trait insert() - // even when no embedding is present (keyword path via terms matching). - let s = create_test_storage(); - let rt = rt(); - rt.block_on(async { - let rec = make_record("quantum entanglement superposition physics"); - s.insert(&rec).await.unwrap(); - let results = s.hybrid_search("quantum physics", 10, 0.3, 0.7).unwrap(); - assert!( - !results.is_empty(), - "hybrid_search must find record containing 'quantum' and 'physics'" - ); - }); - } - - #[test] - fn trait_scheduling_round_trip() { - let s = create_test_storage(); - let rec = make_record("fsrs scheduling test"); - let id = rec.id; - let rt = rt(); - rt.block_on(async { - s.insert(&rec).await.unwrap(); - let state = SchedulingState { - memory_id: id, - stability: 5.0, - difficulty: 0.4, - retrievability: 0.8, - last_review: Some(chrono::Utc::now()), - next_review: Some(chrono::Utc::now() + chrono::Duration::days(7)), - reps: 3, - lapses: 1, - }; - s.update_scheduling(&state).await.unwrap(); - let got = s.get_scheduling(id).await.unwrap().unwrap(); - assert!((got.stability - 5.0).abs() < 0.01); - }); - } - - #[test] - fn trait_get_scheduling_missing_returns_none() { - let s = create_test_storage(); - let rt = rt(); - rt.block_on(async { - let got = s.get_scheduling(uuid::Uuid::new_v4()).await.unwrap(); - assert!(got.is_none()); - }); - } - - #[test] - fn trait_get_due_memories_returns_in_order() { - let s = create_test_storage(); - let rt = rt(); - rt.block_on(async { - for i in 0..3usize { - let rec = make_record(&format!("due memory {i}")); - let id = rec.id; - s.insert(&rec).await.unwrap(); - let state = SchedulingState { - memory_id: id, - stability: 1.0, - difficulty: 0.3, - retrievability: 0.5, - last_review: Some(chrono::Utc::now()), - next_review: Some(chrono::Utc::now() - chrono::Duration::days(3 - i as i64)), - reps: 1, - lapses: 0, - }; - s.update_scheduling(&state).await.unwrap(); - } - let due = s.get_due_memories(chrono::Utc::now(), 10).await.unwrap(); - assert_eq!(due.len(), 3); - }); - } - - #[test] - fn trait_add_edge_is_idempotent() { - let s = create_test_storage(); - let rt = rt(); - rt.block_on(async { - let rec_a = make_record("node a"); - let rec_b = make_record("node b"); - let id_a = rec_a.id; - let id_b = rec_b.id; - s.insert(&rec_a).await.unwrap(); - s.insert(&rec_b).await.unwrap(); - let edge = MemoryEdge { - source_id: id_a, - target_id: id_b, - edge_type: "semantic".to_string(), - weight: 0.9, - created_at: chrono::Utc::now(), - }; - s.add_edge(&edge).await.unwrap(); - s.add_edge(&edge).await.unwrap(); // idempotent - let edges = s.get_edges(id_a, None).await.unwrap(); - let filtered: Vec<_> = edges - .iter() - .filter(|e| e.source_id == id_a && e.target_id == id_b) - .collect(); - assert_eq!(filtered.len(), 1, "edge must not be duplicated"); - }); - } - - #[test] - fn trait_get_edges_filters_by_type() { - let s = create_test_storage(); - let rt = rt(); - rt.block_on(async { - let rec_a = make_record("filter a"); - let rec_b = make_record("filter b"); - let id_a = rec_a.id; - let id_b = rec_b.id; - s.insert(&rec_a).await.unwrap(); - s.insert(&rec_b).await.unwrap(); - let edge = MemoryEdge { - source_id: id_a, - target_id: id_b, - edge_type: "causal".to_string(), - weight: 0.5, - created_at: chrono::Utc::now(), - }; - s.add_edge(&edge).await.unwrap(); - let causal = s.get_edges(id_a, Some("causal")).await.unwrap(); - assert!(!causal.is_empty()); - let semantic = s.get_edges(id_a, Some("semantic")).await.unwrap(); - assert!(semantic.is_empty()); - }); - } - - #[test] - fn trait_remove_edge_deletes_single() { - let s = create_test_storage(); - let rt = rt(); - rt.block_on(async { - let rec_a = make_record("rm edge a"); - let rec_b = make_record("rm edge b"); - let id_a = rec_a.id; - let id_b = rec_b.id; - s.insert(&rec_a).await.unwrap(); - s.insert(&rec_b).await.unwrap(); - let edge = MemoryEdge { - source_id: id_a, - target_id: id_b, - edge_type: "semantic".to_string(), - weight: 0.7, - created_at: chrono::Utc::now(), - }; - s.add_edge(&edge).await.unwrap(); - s.remove_edge(id_a, id_b).await.unwrap(); - let edges = s.get_edges(id_a, None).await.unwrap(); - assert!(edges.is_empty()); - }); - } - - #[test] - fn trait_get_neighbors_bfs_depth_zero_returns_self_only() { - let s = create_test_storage(); - let rt = rt(); - rt.block_on(async { - let rec = make_record("depth zero"); - let id = rec.id; - s.insert(&rec).await.unwrap(); - let neighbors = s.get_neighbors(id, 0).await.unwrap(); - assert_eq!(neighbors.len(), 1); - assert_eq!(neighbors[0].0.id, id); - }); - } - - #[test] - fn trait_get_neighbors_bfs_depth_two_expands() { - let s = create_test_storage(); - let rt = rt(); - rt.block_on(async { - let rec_a = make_record("bfs node a"); - let rec_b = make_record("bfs node b"); - let rec_c = make_record("bfs node c"); - let id_a = rec_a.id; - let id_b = rec_b.id; - let id_c = rec_c.id; - s.insert(&rec_a).await.unwrap(); - s.insert(&rec_b).await.unwrap(); - s.insert(&rec_c).await.unwrap(); - s.add_edge(&MemoryEdge { - source_id: id_a, - target_id: id_b, - edge_type: "semantic".to_string(), - weight: 1.0, - created_at: chrono::Utc::now(), - }) - .await - .unwrap(); - s.add_edge(&MemoryEdge { - source_id: id_b, - target_id: id_c, - edge_type: "semantic".to_string(), - weight: 1.0, - created_at: chrono::Utc::now(), - }) - .await - .unwrap(); - let neighbors = s.get_neighbors(id_a, 2).await.unwrap(); - let ids: Vec = neighbors.iter().map(|(r, _)| r.id).collect(); - assert!(ids.contains(&id_a)); - assert!(ids.contains(&id_b)); - assert!(ids.contains(&id_c)); - }); - } - - #[test] - fn trait_list_domains_empty_in_phase_1() { - let s = create_test_storage(); - let rt = rt(); - rt.block_on(async { - let domains = s.list_domains().await.unwrap(); - assert!(domains.is_empty()); - }); - } - - #[test] - fn trait_upsert_then_get_domain_round_trip() { - let s = create_test_storage(); - let rt = rt(); - rt.block_on(async { - let domain = crate::storage::memory_store::Domain { - id: "dev".to_string(), - label: "Development".to_string(), - centroid: vec![0.1, 0.2, 0.3], - top_terms: vec!["rust".to_string(), "code".to_string()], - memory_count: 42, - created_at: chrono::Utc::now(), - }; - s.upsert_domain(&domain).await.unwrap(); - let got = s.get_domain("dev").await.unwrap().unwrap(); - assert_eq!(got.id, "dev"); - assert_eq!(got.memory_count, 42); - }); - } - - #[test] - fn trait_delete_domain_idempotent() { - let s = create_test_storage(); - let rt = rt(); - rt.block_on(async { - s.delete_domain("nonexistent").await.unwrap(); - s.delete_domain("nonexistent").await.unwrap(); - }); - } - - #[test] - fn trait_classify_with_no_domains_returns_empty() { - let s = create_test_storage(); - let rt = rt(); - rt.block_on(async { - let result = s.classify(&[0.1, 0.2, 0.3]).await.unwrap(); - assert!(result.is_empty()); - }); - } - - #[test] - fn trait_count_matches_insert_count() { - let s = create_test_storage(); - let rt = rt(); - rt.block_on(async { - for i in 0..5usize { - let rec = make_record(&format!("count test {i}")); - s.insert(&rec).await.unwrap(); - } - assert_eq!(s.count().await.unwrap(), 5); - }); - } - - #[test] - fn trait_get_stats_reports_registered_model() { - let s = create_test_storage(); - let sig = ModelSignature { - name: "test-model".to_string(), - dimension: 256, - hash: "c".repeat(64), - }; - let rt = rt(); - rt.block_on(async { - use crate::storage::memory_store::MemoryStore; - // Cast to &dyn MemoryStore so the async trait method is called - // instead of the inherent sync get_stats() on SqliteMemoryStore. - let dyn_s: &dyn MemoryStore = &s; - dyn_s.register_model(&sig).await.unwrap(); - let stats = dyn_s.get_stats().await.unwrap(); - assert_eq!(stats.registered_model_name, Some("test-model".to_string())); - assert_eq!(stats.registered_model_dim, Some(256)); - }); - } - - #[test] - fn trait_vacuum_succeeds() { - let s = create_test_storage(); - let rt = rt(); - rt.block_on(async { - s.vacuum().await.unwrap(); - }); - } - - #[test] - fn trait_insert_refuses_dimension_mismatch() { - let s = create_test_storage(); - let sig = ModelSignature { - name: "test-model".to_string(), - dimension: 256, - hash: "d".repeat(64), - }; - let rt = rt(); - rt.block_on(async { - s.register_model(&sig).await.unwrap(); - // Build a record with wrong dimension (512 instead of 256) and - // declare the model signature in metadata - let mut rec = make_record("dimension mismatch"); - rec.embedding = Some(vec![0.0f32; 512]); - rec.metadata = serde_json::json!({ - "model_name": "test-model", - "model_dim": 256_u64, - "model_hash": "d".repeat(64), - }); - let err = s.insert(&rec).await.unwrap_err(); - assert!( - matches!(err, MemoryStoreError::InvalidInput(_)), - "expected InvalidInput, got {:?}", - err - ); - }); - } - - // Seed a node's stability directly via the scheduling seam so the +365 cap - // in promote_memory_backfill is actually exercised (a freshly ingested node - // has low stability where the *1.5 multiply, not the additive ceiling, wins). - fn seed_stability(s: &Storage, id: &str, stability: f64) { - use crate::storage::memory_store::{MemoryStoreSend, SchedulingState}; - rt().block_on(async { - let state = SchedulingState { - memory_id: uuid::Uuid::parse_str(id).unwrap(), - stability, - difficulty: 0.4, - retrievability: 0.8, - last_review: Some(chrono::Utc::now()), - next_review: Some(chrono::Utc::now() + chrono::Duration::days(7)), - reps: 3, - lapses: 0, - }; - MemoryStoreSend::update_scheduling(s, &state) - .await - .unwrap(); - }); - } - - #[test] - fn promote_memory_backfill_caps_stability_at_plus_365() { - // Above the crossover (stability=730) the additive +365 ceiling must win - // over the *1.5 multiply, so repeated backfill promotions cannot inflate - // stability without bound. This is the bound issue #103 asked us to apply. - let s = create_test_storage(); - let node = s - .ingest(IngestInput { - content: "high-stability cause memory".to_string(), - node_type: "fact".to_string(), - ..Default::default() - }) - .unwrap(); - seed_stability(&s, &node.id, 1000.0); - - let promoted = s.promote_memory_backfill(&node.id).unwrap(); - // 1000 * 1.5 = 1500 (uncapped) vs 1000 + 365 = 1365 (capped). Cap wins. - assert!( - (promoted.stability - 1365.0).abs() < 1e-6, - "expected additive +365 cap (1365.0), got {} (uncapped would be 1500.0)", - promoted.stability - ); - } - - #[test] - fn promote_memory_backfill_uses_multiply_below_crossover() { - // Below the crossover the *1.5 multiply wins (the cap never binds), so - // backfill promotion strength is unchanged from the old promote_memory. - let s = create_test_storage(); - let node = s - .ingest(IngestInput { - content: "low-stability cause memory".to_string(), - node_type: "fact".to_string(), - ..Default::default() - }) - .unwrap(); - seed_stability(&s, &node.id, 10.0); - - let promoted = s.promote_memory_backfill(&node.id).unwrap(); - // 10 * 1.5 = 15 (multiply) vs 10 + 365 = 375 (cap). Multiply wins. - assert!( - (promoted.stability - 15.0).abs() < 1e-6, - "expected *1.5 multiply (15.0) below crossover, got {}", - promoted.stability - ); - } - - #[test] - fn suppress_then_reverse_restores_fsrs_state() { - // reverse_suppression must be a TRUE inverse of suppress_memory. Suppress - // applies stability*0.4, retrieval-0.35, retention-0.20; reverse now undoes - // exactly that (stability/0.4, retrieval+0.35, retention+0.20). Previously - // reverse used non-inverse deltas and left stability permanently halved. - let s = create_test_storage(); - let node = s - .ingest(IngestInput { - content: "a memory to suppress then un-suppress".to_string(), - node_type: "fact".to_string(), - ..Default::default() - }) - .unwrap(); - // Seed above the 0.05 floor so the forward pass never clips (making the - // round-trip exactly recoverable). - seed_stability(&s, &node.id, 20.0); - let before = s.get_node(&node.id).unwrap().unwrap(); - - s.suppress_memory(&node.id).unwrap(); - let suppressed = s.get_node(&node.id).unwrap().unwrap(); - assert!( - (suppressed.stability - before.stability * 0.4).abs() < 1e-6, - "suppress must multiply stability by 0.4" - ); - - let reversed = s.reverse_suppression(&node.id, 24).unwrap(); - // stability: 20 * 0.4 / 0.4 = 20 (fully restored, not 0.5x) - assert!( - (reversed.stability - before.stability).abs() < 1e-6, - "reverse must restore stability to {} (got {})", - before.stability, - reversed.stability - ); - assert!( - (reversed.retrieval_strength - before.retrieval_strength).abs() < 1e-6, - "reverse must restore retrieval_strength" - ); - assert!( - (reversed.retention_strength - before.retention_strength).abs() < 1e-6, - "reverse must restore retention_strength" - ); - } - - #[test] - fn backfill_autofire_gate_defaults_on_and_reads_opt_out() { - // v2.2.1 opt-out semantics: unset => ON (preserves shipped v2.2.0 - // behavior); explicit 0/false/off/no => OFF; anything else => ON. - fn parse(v: Option<&str>) -> bool { - v.map(|v| { - let v = v.trim(); - !(v.eq_ignore_ascii_case("false") - || v.eq_ignore_ascii_case("off") - || v.eq_ignore_ascii_case("no") - || v == "0") - }) - .unwrap_or(true) - } - assert!(parse(None), "unset must default ON"); - assert!(parse(Some("1")), "1 is ON"); - assert!(parse(Some("true")), "true is ON"); - assert!(parse(Some("anything")), "unrecognized is ON"); - assert!(!parse(Some("0")), "0 is OFF"); - assert!(!parse(Some("false")), "false is OFF"); - assert!(!parse(Some("OFF")), "OFF (case-insensitive) is OFF"); - assert!(!parse(Some(" no ")), "whitespace-padded no is OFF (trim)"); - } } diff --git a/crates/vestige-core/src/storage/trace_store.rs b/crates/vestige-core/src/storage/trace_store.rs deleted file mode 100644 index fcac379..0000000 --- a/crates/vestige-core/src/storage/trace_store.rs +++ /dev/null @@ -1,747 +0,0 @@ -//! # Black Box / Receipts / Memory PRs — persistence -//! -//! CRUD for the three V18 tables (`agent_traces` + `agent_runs`, -//! `memory_receipts`, `memory_prs`) on [`SqliteMemoryStore`]. The pure data -//! model lives in [`crate::trace`]; this file is the storage half of the -//! Black Box, immune system, and cinematic debugger for agent memory. -//! -//! Every method follows the established store idiom: lock the writer/reader -//! `Mutex`, `params![]`-bind, store timestamps as RFC3339 (and -//! event millis as INTEGER), serialize structured fields with `serde_json`, and -//! map rows back through a small closure. - -use chrono::Utc; -use rusqlite::{params, OptionalExtension}; -use uuid::Uuid; - -use super::sqlite::SqliteMemoryStore; -use super::{Result, StorageError}; -use crate::trace::{MemoryPr, MemoryPrAction, MemoryPrStatus, MemoryTraceEvent, Receipt}; - -/// A roll-up summary of one agent run, for the Black Box run list. -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] -pub struct AgentRunSummary { - /// The run id. - pub run_id: String, - /// The first tool invoked in the run (the run's "entry point"). - pub first_tool: Option, - /// Total events recorded. - pub event_count: i64, - /// Memories retrieved across the run. - pub retrieved_count: i64, - /// Memories suppressed across the run. - pub suppressed_count: i64, - /// Memory writes across the run. - pub write_count: i64, - /// Sanhedrin vetoes across the run. - pub veto_count: i64, - /// Millis of the first event. - pub started_at: i64, - /// Millis of the most recent event. - pub last_at: i64, -} - -impl SqliteMemoryStore { - // ======================================================================== - // BLACK BOX — trace events + run roll-up - // ======================================================================== - - /// Append one trace event to a run (append-only) and update the run - /// roll-up. Returns the assigned sequence number within the run. - /// - /// `seq` is `MAX(seq)+1` for the run, computed under the writer lock so a - /// run's events stay totally ordered even under concurrent tool calls. - pub fn append_trace_event(&self, event: &MemoryTraceEvent) -> Result { - let now = Utc::now(); - let run_id = event.run_id().to_string(); - let event_type = event.kind(); - let at = event.at(); - let payload = serde_json::to_string(event) - .map_err(|e| StorageError::Init(format!("trace event serialize: {e}")))?; - let tool = match event { - MemoryTraceEvent::McpCall { tool, .. } => Some(tool.clone()), - _ => None, - }; - - // Roll-up deltas this event contributes. - let (d_retrieved, d_suppressed, d_write, d_veto) = match event { - MemoryTraceEvent::MemoryRetrieve { ids, .. } => (ids.len() as i64, 0, 0, 0), - MemoryTraceEvent::MemorySuppress { .. } => (0, 1, 0, 0), - MemoryTraceEvent::MemoryWrite { .. } => (0, 0, 1, 0), - MemoryTraceEvent::SanhedrinVeto { .. } => (0, 0, 0, 1), - _ => (0, 0, 0, 0), - }; - - let writer = self - .writer - .lock() - .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; - - // Propagate a seq-query failure instead of defaulting to 0: swallowing - // the error with unwrap_or(0) could write a duplicate seq=0 for a run - // that already has events, corrupting Black Box replay ordering. On an - // empty run COALESCE(...,-1)+1 already yields 0 correctly. - let seq: i64 = writer.query_row( - "SELECT COALESCE(MAX(seq), -1) + 1 FROM agent_traces WHERE run_id = ?1", - params![run_id], - |r| r.get(0), - )?; - - writer.execute( - "INSERT INTO agent_traces (id, run_id, seq, event_type, tool, payload, at, created_at) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", - params![ - Uuid::new_v4().to_string(), - run_id, - seq, - event_type, - tool, - payload, - at, - now.to_rfc3339(), - ], - )?; - - // Upsert the run roll-up. On first event the row is created with the - // event's tool as the entry point; subsequent events accumulate counts - // and advance `last_at`. - writer.execute( - "INSERT INTO agent_runs (run_id, first_tool, event_count, retrieved_count, - suppressed_count, write_count, veto_count, started_at, last_at, created_at) - VALUES (?1, ?2, 1, ?3, ?4, ?5, ?6, ?7, ?7, ?8) - ON CONFLICT(run_id) DO UPDATE SET - first_tool = COALESCE(agent_runs.first_tool, excluded.first_tool), - event_count = agent_runs.event_count + 1, - retrieved_count = agent_runs.retrieved_count + ?3, - suppressed_count = agent_runs.suppressed_count + ?4, - write_count = agent_runs.write_count + ?5, - veto_count = agent_runs.veto_count + ?6, - last_at = MAX(agent_runs.last_at, ?7)", - params![ - run_id, - tool, - d_retrieved, - d_suppressed, - d_write, - d_veto, - at, - now.to_rfc3339(), - ], - )?; - - Ok(seq) - } - - /// Fetch every event of a run, in sequence order. The black-box replay. - pub fn get_trace(&self, run_id: &str) -> Result> { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let mut stmt = reader.prepare( - "SELECT payload FROM agent_traces WHERE run_id = ?1 ORDER BY seq ASC", - )?; - let rows = stmt.query_map(params![run_id], |row| { - let payload: String = row.get(0)?; - Ok(payload) - })?; - let mut out = Vec::new(); - for r in rows { - let payload = r?; - if let Ok(ev) = serde_json::from_str::(&payload) { - out.push(ev); - } - } - Ok(out) - } - - /// List recent runs, newest activity first. - pub fn list_agent_runs(&self, limit: usize) -> Result> { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let mut stmt = reader.prepare( - "SELECT run_id, first_tool, event_count, retrieved_count, suppressed_count, - write_count, veto_count, started_at, last_at - FROM agent_runs ORDER BY last_at DESC LIMIT ?1", - )?; - let rows = stmt.query_map(params![limit as i64], Self::row_to_run_summary)?; - let mut out = Vec::new(); - for r in rows { - out.push(r?); - } - Ok(out) - } - - /// Fetch one run summary. - pub fn get_agent_run(&self, run_id: &str) -> Result> { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - reader - .query_row( - "SELECT run_id, first_tool, event_count, retrieved_count, suppressed_count, - write_count, veto_count, started_at, last_at - FROM agent_runs WHERE run_id = ?1", - params![run_id], - Self::row_to_run_summary, - ) - .optional() - .map_err(StorageError::from) - } - - fn row_to_run_summary(row: &rusqlite::Row) -> rusqlite::Result { - Ok(AgentRunSummary { - run_id: row.get("run_id")?, - first_tool: row.get("first_tool").ok().flatten(), - event_count: row.get("event_count")?, - retrieved_count: row.get("retrieved_count")?, - suppressed_count: row.get("suppressed_count")?, - write_count: row.get("write_count")?, - veto_count: row.get("veto_count")?, - started_at: row.get("started_at")?, - last_at: row.get("last_at")?, - }) - } - - // ======================================================================== - // MEMORY RECEIPTS - // ======================================================================== - - /// Persist a retrieval receipt. `run_id`/`tool`/`query` are denormalized - /// context for the dashboard; the full [`Receipt`] is stored as JSON. - pub fn save_receipt( - &self, - receipt: &Receipt, - run_id: Option<&str>, - tool: Option<&str>, - query: Option<&str>, - ) -> Result<()> { - let payload = serde_json::to_string(receipt) - .map_err(|e| StorageError::Init(format!("receipt serialize: {e}")))?; - let writer = self - .writer - .lock() - .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; - writer.execute( - "INSERT OR REPLACE INTO memory_receipts - (receipt_id, run_id, tool, query, retrieved_count, suppressed_count, - trust_floor, decay_risk, payload, created_at) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)", - params![ - receipt.receipt_id, - run_id, - tool, - query, - receipt.retrieved.len() as i64, - receipt.suppressed.len() as i64, - receipt.trust_floor, - receipt.decay_risk.as_str(), - payload, - Utc::now().to_rfc3339(), - ], - )?; - Ok(()) - } - - /// Fetch one receipt by id. - pub fn get_receipt(&self, receipt_id: &str) -> Result> { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let payload: Option = reader - .query_row( - "SELECT payload FROM memory_receipts WHERE receipt_id = ?1", - params![receipt_id], - |row| row.get(0), - ) - .optional()?; - Ok(payload.and_then(|p| serde_json::from_str(&p).ok())) - } - - /// List recent receipts, newest first. - pub fn list_receipts(&self, limit: usize) -> Result> { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let mut stmt = reader.prepare( - "SELECT payload FROM memory_receipts ORDER BY created_at DESC LIMIT ?1", - )?; - let rows = stmt.query_map(params![limit as i64], |row| { - let p: String = row.get(0)?; - Ok(p) - })?; - let mut out = Vec::new(); - for r in rows { - if let Ok(rc) = serde_json::from_str::(&r?) { - out.push(rc); - } - } - Ok(out) - } - - /// List the receipts belonging to one run, newest first (B5). The Black Box - /// receipts panel uses this so the receipts it shows actually belong to the - /// selected run, not the global latest. - pub fn list_receipts_for_run(&self, run_id: &str, limit: usize) -> Result> { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let mut stmt = reader.prepare( - "SELECT payload FROM memory_receipts WHERE run_id = ?1 - ORDER BY created_at DESC LIMIT ?2", - )?; - let rows = stmt.query_map(params![run_id, limit as i64], |row| { - let p: String = row.get(0)?; - Ok(p) - })?; - let mut out = Vec::new(); - for r in rows { - if let Ok(rc) = serde_json::from_str::(&r?) { - out.push(rc); - } - } - Ok(out) - } - - // ======================================================================== - // MEMORY PRs — the risk-gated review queue - // ======================================================================== - - /// Open (insert) a Memory PR. - pub fn save_memory_pr(&self, pr: &MemoryPr) -> Result<()> { - let diff = serde_json::to_string(&pr.diff).unwrap_or_else(|_| "{}".to_string()); - let signals = serde_json::to_string(&pr.signals).unwrap_or_else(|_| "[]".to_string()); - let writer = self - .writer - .lock() - .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; - writer.execute( - "INSERT OR REPLACE INTO memory_prs - (id, kind, status, title, subject_id, run_id, diff, signals, - decision, created_at, decided_at) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", - params![ - pr.id, - pr.kind.as_str(), - pr.status.as_str(), - pr.title, - pr.subject_id, - pr.run_id, - diff, - signals, - pr.decision - .and_then(|d| serde_json::to_value(d).ok()) - .and_then(|v| v.as_str().map(|s| s.to_string())), - pr.created_at, - pr.decided_at, - ], - )?; - Ok(()) - } - - /// Fetch one Memory PR by id. - pub fn get_memory_pr(&self, id: &str) -> Result> { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - reader - .query_row( - "SELECT id, kind, status, title, subject_id, run_id, diff, signals, - decision, created_at, decided_at - FROM memory_prs WHERE id = ?1", - params![id], - Self::row_to_memory_pr, - ) - .optional() - .map_err(StorageError::from) - } - - /// List Memory PRs, optionally filtered by status, newest first. - pub fn list_memory_prs( - &self, - status: Option, - limit: usize, - ) -> Result> { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let (sql, with_filter) = match status { - Some(_) => ( - "SELECT id, kind, status, title, subject_id, run_id, diff, signals, - decision, created_at, decided_at - FROM memory_prs WHERE status = ?1 ORDER BY created_at DESC LIMIT ?2", - true, - ), - None => ( - "SELECT id, kind, status, title, subject_id, run_id, diff, signals, - decision, created_at, decided_at - FROM memory_prs ORDER BY created_at DESC LIMIT ?1", - false, - ), - }; - let mut stmt = reader.prepare(sql)?; - let mut out = Vec::new(); - if with_filter { - let st = status.unwrap(); - let rows = - stmt.query_map(params![st.as_str(), limit as i64], Self::row_to_memory_pr)?; - for r in rows { - out.push(r?); - } - } else { - let rows = stmt.query_map(params![limit as i64], Self::row_to_memory_pr)?; - for r in rows { - out.push(r?); - } - } - Ok(out) - } - - /// Count pending Memory PRs (for the nav badge). - pub fn count_pending_memory_prs(&self) -> Result { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let n: i64 = reader - .query_row( - "SELECT COUNT(*) FROM memory_prs WHERE status = 'pending'", - [], - |r| r.get(0), - ) - .unwrap_or(0); - Ok(n) - } - - /// Record a decision on a Memory PR, moving it out of `pending`. Returns the - /// updated PR. `AskAgentWhy` is read-only and never reaches here. - pub fn decide_memory_pr(&self, id: &str, action: MemoryPrAction) -> Result { - let new_status = action.resulting_status().ok_or_else(|| { - StorageError::Init("ask_agent_why is read-only and decides nothing".into()) - })?; - let decision = serde_json::to_value(action) - .ok() - .and_then(|v| v.as_str().map(|s| s.to_string())) - .unwrap_or_default(); - let now = Utc::now().to_rfc3339(); - { - let writer = self - .writer - .lock() - .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; - // Only a still-pending PR may be decided. The `AND status = 'pending'` - // guard makes decisions final: re-POSTing an action on an already - // promoted/forgotten/merged PR cannot flip its status, re-run its - // side effects (e.g. release_quarantine resurrecting a rejected - // memory), or overwrite the audit ledger (decision/decided_at). - let changed = writer.execute( - "UPDATE memory_prs SET status = ?1, decision = ?2, decided_at = ?3 - WHERE id = ?4 AND status = 'pending'", - params![new_status.as_str(), decision, now, id], - )?; - if changed == 0 { - // Distinguish "no such PR" from "already decided" so callers get - // a truthful error instead of a misleading NotFound. - return Err(match self.get_memory_pr(id)? { - Some(_) => StorageError::Init(format!( - "memory PR {id} is already decided and cannot be re-decided" - )), - None => StorageError::NotFound(id.to_string()), - }); - } - } - self.get_memory_pr(id)? - .ok_or_else(|| StorageError::NotFound(id.to_string())) - } - - fn row_to_memory_pr(row: &rusqlite::Row) -> rusqlite::Result { - let kind_s: String = row.get("kind")?; - let status_s: String = row.get("status")?; - let diff_s: String = row.get("diff")?; - let signals_s: String = row.get("signals")?; - let decision_s: Option = row.get("decision").ok().flatten(); - - let kind = crate::trace::MemoryPrKind::from_label(&kind_s) - .unwrap_or(crate::trace::MemoryPrKind::NewFact); - let status = serde_json::from_value(serde_json::Value::String(status_s)) - .unwrap_or(MemoryPrStatus::Pending); - let diff: serde_json::Value = serde_json::from_str(&diff_s).unwrap_or(serde_json::json!({})); - let signals = serde_json::from_str(&signals_s).unwrap_or_default(); - let decision = decision_s - .and_then(|s| serde_json::from_value(serde_json::Value::String(s)).ok()); - - Ok(MemoryPr { - id: row.get("id")?, - kind, - status, - title: row.get("title")?, - diff, - signals, - subject_id: row.get("subject_id").ok().flatten(), - run_id: row.get("run_id").ok().flatten(), - created_at: row.get("created_at")?, - decided_at: row.get("decided_at").ok().flatten(), - decision, - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::trace::{ - DecayRisk, MemoryPrKind, MemoryTraceEvent, Receipt, RiskSignal, SuppressReason, - SuppressedReceiptEntry, - }; - - fn store() -> SqliteMemoryStore { - // Temp-file store for isolated, fast tests (mirrors the existing - // sqlite.rs test helpers; there is no in-memory constructor). - let dir = tempfile::tempdir().unwrap(); - SqliteMemoryStore::new(Some(dir.path().join("trace_test.db"))).expect("test store") - } - - #[test] - fn trace_append_orders_and_rolls_up() { - let s = store(); - let run = "run_abc"; - s.append_trace_event(&MemoryTraceEvent::McpCall { - run_id: run.into(), - tool: "deep_reference".into(), - args_hash: "h".into(), - at: 100, - }) - .unwrap(); - let mut activation = std::collections::BTreeMap::new(); - activation.insert("m1".to_string(), 0.9); - s.append_trace_event(&MemoryTraceEvent::MemoryRetrieve { - run_id: run.into(), - ids: vec!["m1".into(), "m2".into()], - activation, - at: 110, - }) - .unwrap(); - s.append_trace_event(&MemoryTraceEvent::MemorySuppress { - run_id: run.into(), - id: "m3".into(), - reason: SuppressReason::Contradicted, - at: 120, - }) - .unwrap(); - - let events = s.get_trace(run).unwrap(); - assert_eq!(events.len(), 3); - assert_eq!(events[0].kind(), "mcp.call"); - assert_eq!(events[2].kind(), "memory.suppress"); - - let summary = s.get_agent_run(run).unwrap().unwrap(); - assert_eq!(summary.first_tool.as_deref(), Some("deep_reference")); - assert_eq!(summary.event_count, 3); - assert_eq!(summary.retrieved_count, 2); - assert_eq!(summary.suppressed_count, 1); - assert_eq!(summary.started_at, 100); - assert_eq!(summary.last_at, 120); - - let runs = s.list_agent_runs(10).unwrap(); - assert_eq!(runs.len(), 1); - assert_eq!(runs[0].run_id, run); - } - - #[test] - fn receipt_roundtrips() { - let s = store(); - let receipt = Receipt { - receipt_id: "r_2026_06_22_abc".into(), - retrieved: vec!["m1".into(), "m2".into()], - suppressed: vec![SuppressedReceiptEntry::new("m3", SuppressReason::LowTrust)], - activation_path: vec!["a -> b".into()], - trust_floor: 0.62, - decay_risk: DecayRisk::Medium, - mutations: vec![], - }; - s.save_receipt(&receipt, Some("run_abc"), Some("search"), Some("q")) - .unwrap(); - let got = s.get_receipt("r_2026_06_22_abc").unwrap().unwrap(); - assert_eq!(got, receipt); - assert_eq!(s.list_receipts(10).unwrap().len(), 1); - } - - #[test] - fn receipts_are_listable_per_run_b5() { - let s = store(); - let mk = |id: &str| Receipt { - receipt_id: id.into(), - retrieved: vec!["m1".into()], - suppressed: vec![], - activation_path: vec![], - trust_floor: 0.9, - decay_risk: DecayRisk::Low, - mutations: vec![], - }; - s.save_receipt(&mk("r_a1"), Some("run_a"), Some("search"), None) - .unwrap(); - s.save_receipt(&mk("r_a2"), Some("run_a"), Some("search"), None) - .unwrap(); - s.save_receipt(&mk("r_b1"), Some("run_b"), Some("search"), None) - .unwrap(); - - let run_a = s.list_receipts_for_run("run_a", 10).unwrap(); - assert_eq!(run_a.len(), 2, "run_a has exactly its 2 receipts"); - assert!(run_a.iter().all(|r| r.receipt_id.starts_with("r_a"))); - - let run_b = s.list_receipts_for_run("run_b", 10).unwrap(); - assert_eq!(run_b.len(), 1, "run_b has only its own receipt"); - assert_eq!(run_b[0].receipt_id, "r_b1"); - - // Global list still sees all three. - assert_eq!(s.list_receipts(10).unwrap().len(), 3); - } - - #[test] - fn memory_pr_lifecycle() { - let s = store(); - let pr = MemoryPr { - id: "pr_1".into(), - kind: MemoryPrKind::ContradictionDetected, - status: MemoryPrStatus::Pending, - title: "Agent wants to overwrite a high-trust fact".into(), - diff: serde_json::json!({"before": "x", "after": "y"}), - signals: vec![RiskSignal { - code: "contradicts_high_trust".into(), - detail: "Contradicts trust 0.9.".into(), - }], - subject_id: Some("m_old".into()), - run_id: Some("run_abc".into()), - created_at: Utc::now().to_rfc3339(), - decided_at: None, - decision: None, - }; - s.save_memory_pr(&pr).unwrap(); - - assert_eq!(s.count_pending_memory_prs().unwrap(), 1); - let pending = s - .list_memory_prs(Some(MemoryPrStatus::Pending), 10) - .unwrap(); - assert_eq!(pending.len(), 1); - assert_eq!(pending[0].signals[0].code, "contradicts_high_trust"); - - let decided = s.decide_memory_pr("pr_1", MemoryPrAction::Promote).unwrap(); - assert_eq!(decided.status, MemoryPrStatus::Promoted); - assert_eq!(decided.decision, Some(MemoryPrAction::Promote)); - assert!(decided.decided_at.is_some()); - assert_eq!(s.count_pending_memory_prs().unwrap(), 0); - } - - #[test] - fn promote_releases_a_quarantined_memory_end_to_end() { - // B1 regression: the full quarantine→release cycle at the storage layer. - // gate_writes suppresses a risky write; an accept action must reverse it. - let s = store(); - let node = s - .ingest(crate::IngestInput { - content: "Risky write that got quarantined.".to_string(), - node_type: "fact".to_string(), - ..Default::default() - }) - .expect("ingest"); - assert_eq!(node.suppression_count, 0, "fresh node not suppressed"); - - // Quarantine it (what gate_writes does for a risky write). - let suppressed = s.suppress_memory(&node.id).expect("suppress"); - assert_eq!( - suppressed.suppression_count, 1, - "quarantined write is suppressed (held out of retrieval)" - ); - - // Promote = release. (The action releases_memory() == true; the handler - // calls release_quarantine on the subject.) - assert!(crate::MemoryPrAction::Promote.releases_memory()); - let released = s - .release_quarantine(&node.id) - .expect("release quarantine"); - assert_eq!( - released.suppression_count, 0, - "promoting the PR must release the memory — not leave it suppressed" - ); - assert!( - released.suppressed_at.is_none(), - "release must clear suppressed_at" - ); - } - - #[test] - fn release_quarantine_works_past_the_labile_window_c1() { - // C1: a PR reviewed LATE (past the 24h active-forgetting labile window) - // must still release the memory. reverse_suppression refuses after the - // window; release_quarantine must not. - let s = store(); - let node = s - .ingest(crate::IngestInput { - content: "Risky write quarantined and reviewed days later.".to_string(), - node_type: "fact".to_string(), - ..Default::default() - }) - .expect("ingest"); - s.suppress_memory(&node.id).expect("suppress"); - - // Backdate suppressed_at to 100h ago — well past any labile window. - s.set_suppressed_at_for_test(&node.id, chrono::Utc::now() - chrono::Duration::hours(100)); - - // reverse_suppression refuses (window expired)... - assert!( - s.reverse_suppression(&node.id, 24).is_err(), - "reverse_suppression must refuse past the labile window" - ); - // ...but release_quarantine still releases it. - let released = s.release_quarantine(&node.id).expect("release past window"); - assert_eq!(released.suppression_count, 0); - assert!(released.suppressed_at.is_none()); - } - - #[test] - fn release_quarantine_is_idempotent_on_unsuppressed() { - let s = store(); - let node = s - .ingest(crate::IngestInput { - content: "Never suppressed.".to_string(), - node_type: "fact".to_string(), - ..Default::default() - }) - .expect("ingest"); - // No-op when not suppressed — must not error. - let same = s.release_quarantine(&node.id).expect("idempotent release"); - assert_eq!(same.suppression_count, 0); - } - - #[test] - fn ask_agent_why_is_not_a_decision() { - let s = store(); - let pr = MemoryPr { - id: "pr_2".into(), - kind: MemoryPrKind::NewFact, - status: MemoryPrStatus::Pending, - title: "t".into(), - diff: serde_json::json!({}), - signals: vec![], - subject_id: None, - run_id: None, - created_at: Utc::now().to_rfc3339(), - decided_at: None, - decision: None, - }; - s.save_memory_pr(&pr).unwrap(); - assert!(s - .decide_memory_pr("pr_2", MemoryPrAction::AskAgentWhy) - .is_err()); - // Still pending. - assert_eq!(s.count_pending_memory_prs().unwrap(), 1); - } -} diff --git a/crates/vestige-core/src/trace/mod.rs b/crates/vestige-core/src/trace/mod.rs deleted file mode 100644 index 09778cb..0000000 --- a/crates/vestige-core/src/trace/mod.rs +++ /dev/null @@ -1,356 +0,0 @@ -//! # Agent Black Box, Receipts & Memory PRs — the cognitive flight recorder -//! -//! This module holds the **pure** data model and classification logic for three -//! tightly-related capabilities that together make Vestige *the black box, -//! immune system, and cinematic debugger for agent memory*: -//! -//! 1. **Agent Black Box** — a replayable trace of everything an agent run did to -//! memory: prompt → retrieved → suppressed → activated edges → tool calls → -//! writes → contradictions → vetoes → dream consolidation → final answer. -//! The event model is [`MemoryTraceEvent`]. -//! -//! 2. **Memory Receipts** — every important retrieval returns a structured -//! [`Receipt`]: what was retrieved, what was suppressed and why, the -//! activation path that surfaced it, the trust floor, the decay risk, and any -//! mutations. A receipt is the "nutrition label" for a piece of agent memory. -//! -//! 3. **Memory PRs** — changes to an agent's *brain* are reviewed like changes -//! to code. Ordinary context auto-commits (and always leaves a receipt), but -//! risky writes — contradictions against high-trust memory, supersede / forget -//! / merge / protect, identity / preference / workflow / positioning facts, -//! permission / auth / security / money / legal facts, dream consolidation -//! proposals, decay-below-threshold resurrection, low-confidence batch -//! imports, and weak-provenance connector writes — open a reviewable -//! [`MemoryPr`]. The gating decision is [`classify_write`]. -//! -//! ## Design north star (shared with [`crate::advanced::merge_supersede`]) -//! -//! - **append-only** — trace events are never mutated, only appended, so a run -//! replays exactly as the agent experienced it. -//! - **self-explaining** — every gated write carries the [`RiskSignal`]s that -//! explain *why* it needs review, in plain language. -//! - **opt-in friction** — the default [`ReviewMode::RiskGated`] keeps ordinary -//! memory frictionless and only opens a PR when the agent tries to rewrite its -//! own brain. [`ReviewMode::Fast`] never gates; [`ReviewMode::Paranoid`] gates -//! every write. -//! - **DB-free** — this module is pure logic so it is unit-testable without a -//! database. Persistence (the `agent_traces`, `memory_receipts`, and -//! `memory_prs` tables) lives in [`crate::storage`]. -//! -//! The killer line, made literal by [`classify_write`]: -//! -//! > Vestige auto-remembers ordinary context, but opens a Memory PR when the -//! > agent tries to rewrite its own brain. - -use serde::{Deserialize, Serialize}; - -mod receipt; -mod review; - -pub use receipt::{DecayRisk, Receipt, ReceiptMutation, SuppressedReceiptEntry}; -pub use review::{ - classify_write, MemoryPr, MemoryPrAction, MemoryPrKind, MemoryPrStatus, ReviewMode, RiskClass, - RiskSignal, WriteContext, HIGH_TRUST_FLOOR, LOW_CONFIDENCE_FLOOR, -}; - -// ============================================================================ -// TRACE EVENTS — the black-box flight recorder -// ============================================================================ - -/// One append-only event in an agent run's black-box trace. -/// -/// Mirrors the TypeScript `MemoryTraceEvent` union exactly (tagged on `type`, -/// camelCase fields) so the dashboard, the `vestige://trace/{runId}` MCP -/// resource, and the exported `.vestige-trace.json` all speak one schema. -/// -/// ```ts -/// type MemoryTraceEvent = -/// | { type: "mcp.call"; runId; tool; argsHash; at } -/// | { type: "memory.retrieve"; runId; ids; activation; at } -/// | { type: "memory.suppress"; runId; id; reason } -/// | { type: "memory.write"; runId; id; diff; source } -/// | { type: "sanhedrin.veto"; runId; claim; evidenceIds; confidence } -/// | { type: "dream.patch"; runId; proposalIds; at }; -/// ``` -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(tag = "type")] -pub enum MemoryTraceEvent { - /// An MCP tool was invoked. The args are stored as a hash (not the raw - /// payload) so traces never leak prompt contents or secrets. - #[serde(rename = "mcp.call")] - McpCall { - #[serde(rename = "runId")] - run_id: String, - tool: String, - #[serde(rename = "argsHash")] - args_hash: String, - at: i64, - }, - - /// Memories were retrieved, with per-id spreading-activation strength so the - /// graph replay can pulse exactly the nodes the agent saw, at their weight. - #[serde(rename = "memory.retrieve")] - MemoryRetrieve { - #[serde(rename = "runId")] - run_id: String, - ids: Vec, - activation: std::collections::BTreeMap, - at: i64, - }, - - /// A memory that *would* have surfaced was suppressed, with the reason — - /// this is the "what the agent chose NOT to use" channel. - #[serde(rename = "memory.suppress")] - MemorySuppress { - #[serde(rename = "runId")] - run_id: String, - id: String, - reason: SuppressReason, - #[serde(default)] - at: i64, - }, - - /// A memory was written / strengthened. `diff` is an opaque JSON description - /// of the change; `source` records who caused it. - #[serde(rename = "memory.write")] - MemoryWrite { - #[serde(rename = "runId")] - run_id: String, - id: String, - diff: serde_json::Value, - source: WriteSource, - #[serde(default)] - at: i64, - }, - - /// A contradiction was detected between memories during a run — its own - /// first-class event (not folded into `memory.suppress`), so the Black Box - /// can show the exact contradiction decision the agent faced. - #[serde(rename = "contradiction.detected")] - ContradictionDetected { - #[serde(rename = "runId")] - run_id: String, - /// The two (or more) memory ids in tension. - ids: Vec, - /// The id the agent trusted (kept), if it resolved the tension. - #[serde(rename = "winnerId", skip_serializing_if = "Option::is_none")] - winner_id: Option, - /// Plain-language description of the contradiction. - detail: String, - #[serde(default)] - at: i64, - }, - - /// The Sanhedrin verifier vetoed a claim the agent was about to assert, - /// citing the evidence it weighed and its confidence. - #[serde(rename = "sanhedrin.veto")] - SanhedrinVeto { - #[serde(rename = "runId")] - run_id: String, - claim: String, - #[serde(rename = "evidenceIds")] - evidence_ids: Vec, - confidence: f64, - #[serde(default)] - at: i64, - }, - - /// Dream consolidation proposed a patch to memory (merge / insight / prune). - #[serde(rename = "dream.patch")] - DreamPatch { - #[serde(rename = "runId")] - run_id: String, - #[serde(rename = "proposalIds")] - proposal_ids: Vec, - at: i64, - }, -} - -impl MemoryTraceEvent { - /// The run this event belongs to. - pub fn run_id(&self) -> &str { - match self { - MemoryTraceEvent::McpCall { run_id, .. } - | MemoryTraceEvent::MemoryRetrieve { run_id, .. } - | MemoryTraceEvent::MemorySuppress { run_id, .. } - | MemoryTraceEvent::MemoryWrite { run_id, .. } - | MemoryTraceEvent::ContradictionDetected { run_id, .. } - | MemoryTraceEvent::SanhedrinVeto { run_id, .. } - | MemoryTraceEvent::DreamPatch { run_id, .. } => run_id, - } - } - - /// The wall-clock millisecond timestamp the event was recorded at. - pub fn at(&self) -> i64 { - match self { - MemoryTraceEvent::McpCall { at, .. } - | MemoryTraceEvent::MemoryRetrieve { at, .. } - | MemoryTraceEvent::MemorySuppress { at, .. } - | MemoryTraceEvent::MemoryWrite { at, .. } - | MemoryTraceEvent::ContradictionDetected { at, .. } - | MemoryTraceEvent::SanhedrinVeto { at, .. } - | MemoryTraceEvent::DreamPatch { at, .. } => *at, - } - } - - /// Short stable kind label used for filtering / the `event_type` column. - pub fn kind(&self) -> &'static str { - match self { - MemoryTraceEvent::McpCall { .. } => "mcp.call", - MemoryTraceEvent::MemoryRetrieve { .. } => "memory.retrieve", - MemoryTraceEvent::MemorySuppress { .. } => "memory.suppress", - MemoryTraceEvent::MemoryWrite { .. } => "memory.write", - MemoryTraceEvent::ContradictionDetected { .. } => "contradiction.detected", - MemoryTraceEvent::SanhedrinVeto { .. } => "sanhedrin.veto", - MemoryTraceEvent::DreamPatch { .. } => "dream.patch", - } - } - - /// Stamp `at` on events that left it defaulted (the recorder fills this so - /// callers don't have to thread a clock through every emit site). - pub fn with_at(mut self, now_ms: i64) -> Self { - match &mut self { - MemoryTraceEvent::McpCall { at, .. } - | MemoryTraceEvent::MemoryRetrieve { at, .. } - | MemoryTraceEvent::MemorySuppress { at, .. } - | MemoryTraceEvent::MemoryWrite { at, .. } - | MemoryTraceEvent::ContradictionDetected { at, .. } - | MemoryTraceEvent::SanhedrinVeto { at, .. } - | MemoryTraceEvent::DreamPatch { at, .. } => { - if *at == 0 { - *at = now_ms; - } - } - } - self - } -} - -/// Why a memory was suppressed during a run. Mirrors the TS union member -/// `"low_trust" | "decayed" | "contradicted" | "privacy"`, plus `competition` -/// for the existing spreading-activation competition suppression. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum SuppressReason { - /// Below the trust floor for this retrieval. - LowTrust, - /// FSRS retrievability decayed below the usable threshold. - Decayed, - /// Contradicted by a higher-trust memory. - Contradicted, - /// Withheld for privacy / sensitivity reasons. - Privacy, - /// Lost spreading-activation competition to a stronger memory. - Competition, -} - -impl SuppressReason { - /// Stable string label. - pub fn as_str(&self) -> &'static str { - match self { - SuppressReason::LowTrust => "low_trust", - SuppressReason::Decayed => "decayed", - SuppressReason::Contradicted => "contradicted", - SuppressReason::Privacy => "privacy", - SuppressReason::Competition => "competition", - } - } -} - -/// Who caused a `memory.write`. Mirrors the TS `"agent" | "user" | "dream"`, -/// plus `connector` for external-source sync writes. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum WriteSource { - /// The agent wrote it autonomously. - Agent, - /// The user explicitly asked for it. - User, - /// Produced by dream consolidation. - Dream, - /// Ingested by an external connector (GitHub, Redmine, …). - Connector, -} - -impl WriteSource { - /// Stable string label. - pub fn as_str(&self) -> &'static str { - match self { - WriteSource::Agent => "agent", - WriteSource::User => "user", - WriteSource::Dream => "dream", - WriteSource::Connector => "connector", - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn trace_event_roundtrips_with_ts_shape() { - let ev = MemoryTraceEvent::McpCall { - run_id: "run_123".into(), - tool: "deep_reference".into(), - args_hash: "abc".into(), - at: 42, - }; - let json = serde_json::to_value(&ev).unwrap(); - // Tagged on `type`, camelCase runId/argsHash — exactly the TS contract. - assert_eq!(json["type"], "mcp.call"); - assert_eq!(json["runId"], "run_123"); - assert_eq!(json["argsHash"], "abc"); - assert_eq!(json["at"], 42); - - let back: MemoryTraceEvent = serde_json::from_value(json).unwrap(); - assert_eq!(back, ev); - } - - #[test] - fn retrieve_event_carries_activation_map() { - let mut activation = std::collections::BTreeMap::new(); - activation.insert("mem_1".to_string(), 0.91); - activation.insert("mem_7".to_string(), 0.42); - let ev = MemoryTraceEvent::MemoryRetrieve { - run_id: "r".into(), - ids: vec!["mem_1".into(), "mem_7".into()], - activation, - at: 1, - }; - let json = serde_json::to_value(&ev).unwrap(); - assert_eq!(json["type"], "memory.retrieve"); - assert_eq!(json["activation"]["mem_1"], 0.91); - } - - #[test] - fn with_at_fills_only_when_unset() { - let ev = MemoryTraceEvent::MemorySuppress { - run_id: "r".into(), - id: "m".into(), - reason: SuppressReason::Contradicted, - at: 0, - } - .with_at(999); - assert_eq!(ev.at(), 999); - - let ev2 = MemoryTraceEvent::DreamPatch { - run_id: "r".into(), - proposal_ids: vec!["p".into()], - at: 7, - } - .with_at(999); - assert_eq!(ev2.at(), 7, "explicit timestamp must not be overwritten"); - } - - #[test] - fn suppress_reason_labels_match_ts() { - assert_eq!(SuppressReason::LowTrust.as_str(), "low_trust"); - assert_eq!(SuppressReason::Contradicted.as_str(), "contradicted"); - // Serde uses the same snake_case form on the wire. - assert_eq!( - serde_json::to_value(SuppressReason::Privacy).unwrap(), - serde_json::json!("privacy") - ); - } -} diff --git a/crates/vestige-core/src/trace/receipt.rs b/crates/vestige-core/src/trace/receipt.rs deleted file mode 100644 index 1ac3fbf..0000000 --- a/crates/vestige-core/src/trace/receipt.rs +++ /dev/null @@ -1,302 +0,0 @@ -//! # Memory Receipts -//! -//! Every important retrieval returns a [`Receipt`] — a structured record of what -//! the agent's memory actually did to answer a query. It is built entirely from -//! data the retrieval pipeline *already computes* (scored memories, suppression -//! decisions, spreading-activation path, FSRS trust), so attaching one is nearly -//! free and never changes the answer. -//! -//! The canonical shape (matching the product spec): -//! -//! ```json -//! { -//! "receipt_id": "r_2026_06_22_abc", -//! "retrieved": ["mem_1", "mem_7", "mem_9"], -//! "suppressed": [{"id": "mem_4", "reason": "contradicted"}], -//! "activation_path": ["project_goal -> design_decision -> current_file"], -//! "trust_floor": 0.62, -//! "decay_risk": "medium", -//! "mutations": [] -//! } -//! ``` - -use serde::{Deserialize, Serialize}; - -use super::SuppressReason; - -/// A structured receipt attached to a retrieval's output. -/// -/// Field names are snake_case to match the published product spec and the -/// dashboard receipt card exactly. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct Receipt { - /// Stable, human-legible id: `r___
                    _`. - pub receipt_id: String, - - /// Ids of the memories that actually informed the answer, best-first. - pub retrieved: Vec, - - /// Memories that were withheld, each with the reason — the "what the agent - /// chose NOT to use" channel that makes retrieval auditable. - pub suppressed: Vec, - - /// Human-readable spreading-activation path(s) that surfaced the result, - /// e.g. `"project_goal -> design_decision -> current_file"`. - pub activation_path: Vec, - - /// The minimum trust score among the retrieved memories — the weakest link - /// the answer rests on. - pub trust_floor: f64, - - /// Coarse decay risk for the retrieved set (how stale the evidence is). - pub decay_risk: DecayRisk, - - /// Any memory mutations this retrieval triggered (testing-effect - /// strengthening, reconsolidation, supersession). Empty for a pure read. - pub mutations: Vec, -} - -impl Receipt { - /// Build a receipt from already-computed retrieval signals. - /// - /// `receipt_id` is `r___` — human-legible - /// and dated, with a short random suffix so that **multiple retrievals in - /// the same run never collide** (B3). The discriminator (usually the runId) - /// keeps receipts from one run visually grouped; the suffix guarantees - /// uniqueness so `INSERT OR REPLACE` can't overwrite an earlier receipt. - /// `trust_scores` is the per-id FSRS retrievability/trust the pipeline - /// already produced. - pub fn build( - now: chrono::DateTime, - discriminator: &str, - retrieved: Vec, - suppressed: Vec, - activation_path: Vec, - trust_scores: &[f64], - mutations: Vec, - ) -> Self { - Self::build_with_unique( - now, - discriminator, - &uuid::Uuid::new_v4().simple().to_string()[..6], - retrieved, - suppressed, - activation_path, - trust_scores, - mutations, - ) - } - - /// Like [`Receipt::build`] but with a caller-supplied uniqueness token, - /// so the id is fully deterministic for tests. Production uses - /// [`Receipt::build`] which mints a random token. - #[allow(clippy::too_many_arguments)] - pub fn build_with_unique( - now: chrono::DateTime, - discriminator: &str, - unique: &str, - retrieved: Vec, - suppressed: Vec, - activation_path: Vec, - trust_scores: &[f64], - mutations: Vec, - ) -> Self { - let trust_floor = trust_scores - .iter() - .copied() - .fold(f64::INFINITY, f64::min); - let trust_floor = if trust_floor.is_finite() { - (trust_floor * 100.0).round() / 100.0 - } else { - 0.0 - }; - let decay_risk = DecayRisk::from_trust_floor(trust_floor); - - let short: String = discriminator - .chars() - .filter(|c| c.is_ascii_alphanumeric()) - .take(8) - .collect(); - let unique_clean: String = unique - .chars() - .filter(|c| c.is_ascii_alphanumeric()) - .take(6) - .collect(); - let receipt_id = format!( - "r_{}_{}_{}", - now.format("%Y_%m_%d"), - short, - unique_clean - ); - - Self { - receipt_id, - retrieved, - suppressed, - activation_path, - trust_floor, - decay_risk, - mutations, - } - } -} - -/// One suppressed-memory entry in a [`Receipt`]. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct SuppressedReceiptEntry { - /// The id of the suppressed memory. - pub id: String, - /// Why it was withheld. - pub reason: SuppressReason, -} - -impl SuppressedReceiptEntry { - /// Convenience constructor. - pub fn new(id: impl Into, reason: SuppressReason) -> Self { - Self { - id: id.into(), - reason, - } - } -} - -/// Coarse staleness signal for a retrieved set, derived from the trust floor. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "lowercase")] -pub enum DecayRisk { - /// Trust floor is healthy; the evidence is fresh. - Low, - /// Some of the evidence is weakening. - Medium, - /// The answer rests on memory that is decaying out. - High, -} - -impl DecayRisk { - /// Map the weakest retrieved-trust score to a decay-risk band. - /// - /// Thresholds align with the FSRS "due for review" intuition: above 0.7 the - /// memory is comfortably retrievable, 0.4–0.7 is getting weak, below 0.4 is - /// at risk of being forgotten. - pub fn from_trust_floor(trust_floor: f64) -> Self { - if trust_floor >= 0.7 { - DecayRisk::Low - } else if trust_floor >= 0.4 { - DecayRisk::Medium - } else { - DecayRisk::High - } - } - - /// Stable string label. - pub fn as_str(&self) -> &'static str { - match self { - DecayRisk::Low => "low", - DecayRisk::Medium => "medium", - DecayRisk::High => "high", - } - } -} - -/// A memory mutation that a retrieval triggered, recorded on the receipt so the -/// side effects of "just reading" are never invisible. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct ReceiptMutation { - /// The mutated memory id. - pub id: String, - /// What changed: `"strengthened"`, `"reconsolidated"`, `"superseded"`, … - pub kind: String, - /// Optional human note about the change. - #[serde(skip_serializing_if = "Option::is_none")] - pub note: Option, -} - -#[cfg(test)] -mod tests { - use super::*; - use chrono::TimeZone; - - fn fixed_now() -> chrono::DateTime { - chrono::Utc.with_ymd_and_hms(2026, 6, 22, 15, 0, 0).unwrap() - } - - #[test] - fn receipt_id_is_human_legible_and_dated() { - let r = Receipt::build_with_unique( - fixed_now(), - "abc123!!", - "u1u2u3", - vec!["mem_1".into()], - vec![], - vec![], - &[0.9], - vec![], - ); - assert_eq!(r.receipt_id, "r_2026_06_22_abc123_u1u2u3"); - } - - #[test] - fn receipt_ids_unique_within_a_run_b3() { - // B3: two retrievals in the SAME run (same date + discriminator) must - // get DISTINCT ids so INSERT OR REPLACE can't overwrite the first. - let a = Receipt::build(fixed_now(), "run_x", vec![], vec![], vec![], &[], vec![]); - let b = Receipt::build(fixed_now(), "run_x", vec![], vec![], vec![], &[], vec![]); - assert_ne!( - a.receipt_id, b.receipt_id, - "same-run receipts must not collide" - ); - assert!(a.receipt_id.starts_with("r_2026_06_22_runx_")); - assert!(b.receipt_id.starts_with("r_2026_06_22_runx_")); - } - - #[test] - fn trust_floor_is_the_weakest_link() { - let r = Receipt::build( - fixed_now(), - "x", - vec!["a".into(), "b".into(), "c".into()], - vec![], - vec![], - &[0.91, 0.62, 0.78], - vec![], - ); - assert_eq!(r.trust_floor, 0.62); - assert_eq!(r.decay_risk, DecayRisk::Medium); - } - - #[test] - fn empty_trust_scores_floor_to_zero_high_risk() { - let r = Receipt::build(fixed_now(), "x", vec![], vec![], vec![], &[], vec![]); - assert_eq!(r.trust_floor, 0.0); - assert_eq!(r.decay_risk, DecayRisk::High); - } - - #[test] - fn decay_bands() { - assert_eq!(DecayRisk::from_trust_floor(0.95), DecayRisk::Low); - assert_eq!(DecayRisk::from_trust_floor(0.55), DecayRisk::Medium); - assert_eq!(DecayRisk::from_trust_floor(0.20), DecayRisk::High); - } - - #[test] - fn matches_published_spec_shape() { - let r = Receipt { - receipt_id: "r_2026_06_22_abc".into(), - retrieved: vec!["mem_1".into(), "mem_7".into(), "mem_9".into()], - suppressed: vec![SuppressedReceiptEntry::new( - "mem_4", - SuppressReason::Contradicted, - )], - activation_path: vec!["project_goal -> design_decision -> current_file".into()], - trust_floor: 0.62, - decay_risk: DecayRisk::Medium, - mutations: vec![], - }; - let json = serde_json::to_value(&r).unwrap(); - assert_eq!(json["receipt_id"], "r_2026_06_22_abc"); - assert_eq!(json["suppressed"][0]["reason"], "contradicted"); - assert_eq!(json["decay_risk"], "medium"); - assert_eq!(json["trust_floor"], 0.62); - assert!(json["mutations"].as_array().unwrap().is_empty()); - } -} diff --git a/crates/vestige-core/src/trace/review.rs b/crates/vestige-core/src/trace/review.rs deleted file mode 100644 index 7c63f55..0000000 --- a/crates/vestige-core/src/trace/review.rs +++ /dev/null @@ -1,802 +0,0 @@ -//! # Memory PRs — review changes to an agent's brain like code -//! -//! Ordinary context auto-commits and always leaves a receipt. But a *risky* -//! write — one where the agent is rewriting its own brain — opens a reviewable -//! [`MemoryPr`] instead. [`classify_write`] is the immune system: given a -//! [`WriteContext`] and a [`ReviewMode`], it returns the [`RiskClass`] and the -//! [`RiskSignal`]s that explain, in plain language, *why* a write needs review. -//! -//! ## The three modes (one-click in the dashboard) -//! -//! | Mode | Behaviour | -//! |------|-----------| -//! | [`ReviewMode::Fast`] | Never gate. Every write auto-commits. (Demos, trusted solo flows.) | -//! | [`ReviewMode::RiskGated`] | **Default.** Auto-commit ordinary writes; open a PR for risky ones. | -//! | [`ReviewMode::Paranoid`] | Gate *every* write. Nothing enters the brain without approval. | -//! -//! ## What counts as "risky" (the taxonomy) -//! -//! A write is risky when any of these hold: -//! - it **contradicts a high-trust memory**, -//! - it **supersedes / forgets / merges / protects** existing memory, -//! - it touches **identity, user preference, workflow, or project positioning**, -//! - it asserts a **permission / auth / security / money / bounty / legal-ish** fact, -//! - it is a **dream consolidation** proposal, -//! - it **resurrects a decayed** memory (below the retention threshold), -//! - it is part of a **low-confidence batch import**, -//! - it is an **external connector write without strong provenance**. -//! -//! Each rule maps to a [`RiskSignal`] so the resulting Memory PR is fully -//! self-explaining. - -use serde::{Deserialize, Serialize}; - -use super::WriteSource; - -/// A memory is "high trust" at or above this FSRS retrievability/trust score. -/// Contradicting something this trusted is always worth a review. -pub const HIGH_TRUST_FLOOR: f64 = 0.7; - -/// Writes below this confidence are treated as low-confidence (e.g. a bulk -/// import where the model wasn't sure). -pub const LOW_CONFIDENCE_FLOOR: f64 = 0.5; - -// ============================================================================ -// REVIEW MODE -// ============================================================================ - -/// How aggressively the agent's brain gates incoming writes. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] -#[serde(rename_all = "snake_case")] -pub enum ReviewMode { - /// Never gate — every write auto-commits. - Fast, - /// Default: auto-commit ordinary writes, open a PR for risky ones. - #[default] - RiskGated, - /// Gate every write — nothing enters the brain without approval. - Paranoid, -} - -impl ReviewMode { - /// Stable string label, also the wire form. - pub fn as_str(&self) -> &'static str { - match self { - ReviewMode::Fast => "fast", - ReviewMode::RiskGated => "risk_gated", - ReviewMode::Paranoid => "paranoid", - } - } - - /// Parse from a label (case-insensitive, tolerant of `-`/`_`). Falls back to - /// the default [`ReviewMode::RiskGated`] on anything unrecognised. - pub fn from_label(s: &str) -> Self { - match s.trim().to_ascii_lowercase().replace('-', "_").as_str() { - "fast" => ReviewMode::Fast, - "paranoid" => ReviewMode::Paranoid, - _ => ReviewMode::RiskGated, - } - } -} - -// ============================================================================ -// RISK CLASSIFICATION -// ============================================================================ - -/// The outcome of [`classify_write`]: does this write auto-commit or open a PR? -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum RiskClass { - /// Ordinary context — auto-commit (a receipt is still generated). - AutoCommit, - /// Risky — open a [`MemoryPr`] for review. - Review, -} - -impl RiskClass { - /// Stable string label. - pub fn as_str(&self) -> &'static str { - match self { - RiskClass::AutoCommit => "auto_commit", - RiskClass::Review => "review", - } - } - - /// Whether this write should be held for review. - pub fn needs_review(&self) -> bool { - matches!(self, RiskClass::Review) - } -} - -/// A single, self-explaining reason a write was flagged for review. The -/// `code` is stable for filtering/telemetry; the `detail` is human prose for -/// the PR card. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct RiskSignal { - /// Stable machine code, e.g. `"contradicts_high_trust"`. - pub code: String, - /// Plain-language explanation shown on the Memory PR. - pub detail: String, -} - -impl RiskSignal { - fn new(code: &str, detail: impl Into) -> Self { - Self { - code: code.into(), - detail: detail.into(), - } - } -} - -/// Everything [`classify_write`] needs to decide whether a write is risky. -/// -/// All fields default to the "ordinary, safe" interpretation so callers only -/// set the signals that actually apply to their write. -#[derive(Debug, Clone, Default)] -pub struct WriteContext { - /// Who is performing the write. - pub source: Option, - /// The node type being written, e.g. `"fact"`, `"preference"`, `"identity"`. - pub node_type: String, - /// The content (or a representative slice) — scanned for sensitive topics. - pub content: String, - /// Tags attached to the write — also scanned for sensitive topics. - pub tags: Vec, - /// The write contradicts an existing memory whose trust is this high. - /// `None` if there is no contradiction. - pub contradicts_trust: Option, - /// This write supersedes / replaces an existing memory. - pub supersedes: bool, - /// This write forgets / suppresses an existing memory. - pub forgets: bool, - /// This write merges existing memories. - pub merges: bool, - /// This write protects / pins a memory. - pub protects: bool, - /// This write resurrects a memory that had decayed below retention. - pub resurrects_decayed: bool, - /// Confidence of the write (0..1). `None` means "not a batch / unknown". - pub confidence: Option, - /// This write is one of many in a bulk import. - pub batch_import: bool, - /// For connector writes: whether the source envelope carries strong - /// provenance (a verified `source_system` + `source_id` + URL). - pub strong_provenance: bool, -} - -/// Sensitive topic substrings. A write whose content/tags/type mention any of -/// these is treated as touching identity / preference / security / money / -/// legal / workflow / positioning and is routed to review. -const SENSITIVE_TOPICS: &[(&str, &str)] = &[ - // identity & preference - ("identity", "identity fact"), - ("preference", "user preference"), - ("workflow", "workflow rule"), - ("positioning", "project positioning"), - ("persona", "agent persona"), - // permission / auth / security - ("permission", "tool permission"), - ("auth", "authentication / authorization"), - ("token", "credential / token"), - ("secret", "secret material"), - ("password", "credential"), - ("api key", "credential / API key"), - ("security", "security-relevant fact"), - ("vuln", "security vulnerability"), - ("vulnerability", "security vulnerability"), - ("credential", "credential material"), - ("credentials", "credential material"), - ("api key", "credential / API key"), - ("apikey", "credential / API key"), - // money / bounty / legal - ("money", "financial fact"), - ("payment", "financial fact"), - ("invoice", "financial fact"), - ("bounty", "bounty / payout"), - ("salary", "financial fact"), - ("license", "legal / license fact"), - ("legal", "legal-relevant fact"), - ("contract", "legal / contract fact"), -]; - -/// Node types that are intrinsically sensitive regardless of content. -const SENSITIVE_NODE_TYPES: &[&str] = &[ - "identity", - "preference", - "user_preference", - "credential", - "permission", - "security", - "constitution", -]; - -/// Classify a write into auto-commit vs. review, with the signals explaining the -/// decision. -/// -/// This is the immune system. It is pure and deterministic, so the dashboard's -/// "explain this PR" view and the agent's `Ask Agent Why` action see exactly the -/// same reasoning the gate used. -pub fn classify_write(ctx: &WriteContext, mode: ReviewMode) -> (RiskClass, Vec) { - // Mode shortcuts. - match mode { - // Fast never gates — but we still collect signals so the receipt/PR - // record can note what *would* have been flagged. - ReviewMode::Fast => return (RiskClass::AutoCommit, Vec::new()), - ReviewMode::Paranoid => { - let mut signals = collect_signals(ctx); - if signals.is_empty() { - signals.push(RiskSignal::new( - "paranoid_mode", - "Paranoid mode: every write is reviewed before entering memory.", - )); - } - return (RiskClass::Review, signals); - } - ReviewMode::RiskGated => {} - } - - let signals = collect_signals(ctx); - if signals.is_empty() { - (RiskClass::AutoCommit, signals) - } else { - (RiskClass::Review, signals) - } -} - -/// Gather every risk signal that applies to a write, independent of mode. -fn collect_signals(ctx: &WriteContext) -> Vec { - let mut signals = Vec::new(); - - // 1. Contradiction against a high-trust memory. - if let Some(trust) = ctx.contradicts_trust - && trust >= HIGH_TRUST_FLOOR - { - signals.push(RiskSignal::new( - "contradicts_high_trust", - format!( - "Contradicts an existing high-trust memory (trust {:.2} ≥ {:.2}).", - trust, HIGH_TRUST_FLOOR - ), - )); - } - - // 2. Structural rewrites of existing memory. - if ctx.supersedes { - signals.push(RiskSignal::new( - "supersedes_memory", - "Supersedes / replaces an existing memory.", - )); - } - if ctx.forgets { - signals.push(RiskSignal::new( - "forgets_memory", - "Forgets / suppresses an existing memory.", - )); - } - if ctx.merges { - signals.push(RiskSignal::new( - "merges_memory", - "Merges existing memories into one.", - )); - } - if ctx.protects { - signals.push(RiskSignal::new( - "protects_memory", - "Protects / pins a memory against decay and forgetting.", - )); - } - - // 3. Sensitive node types & topics (identity / preference / workflow / - // positioning / permission / auth / security / money / legal). - let node_type_lc = ctx.node_type.to_ascii_lowercase(); - if SENSITIVE_NODE_TYPES.contains(&node_type_lc.as_str()) { - signals.push(RiskSignal::new( - "sensitive_node_type", - format!("Writes a sensitive node type: `{}`.", node_type_lc), - )); - } - if let Some(topic) = first_sensitive_topic(&ctx.content, &ctx.tags) { - signals.push(RiskSignal::new( - "sensitive_topic", - format!("Touches a sensitive topic: {topic}."), - )); - } - - // 4. Dream consolidation proposals. - if matches!(ctx.source, Some(WriteSource::Dream)) { - signals.push(RiskSignal::new( - "dream_consolidation", - "Proposed by dream consolidation — a machine-generated change to memory.", - )); - } - - // 5. Decay-below-threshold resurrection. - if ctx.resurrects_decayed { - signals.push(RiskSignal::new( - "resurrects_decayed", - "Resurrects a memory that had decayed below the retention threshold.", - )); - } - - // 6. Low-confidence batch imports. - if ctx.batch_import { - if let Some(conf) = ctx.confidence { - if conf < LOW_CONFIDENCE_FLOOR { - signals.push(RiskSignal::new( - "low_confidence_batch", - format!( - "Low-confidence batch import (confidence {:.2} < {:.2}).", - conf, LOW_CONFIDENCE_FLOOR - ), - )); - } - } else { - signals.push(RiskSignal::new( - "unscored_batch", - "Batch import with no confidence score.", - )); - } - } - - // 7. External connector writes without strong provenance. - if matches!(ctx.source, Some(WriteSource::Connector)) && !ctx.strong_provenance { - signals.push(RiskSignal::new( - "weak_provenance_connector", - "External connector write without strong provenance (unverified source envelope).", - )); - } - - signals -} - -/// Return the human label of the first sensitive topic found in content/tags. -/// -/// B6: matches on WORD BOUNDARIES, not substrings — so "tokenizer" no longer -/// trips "token", "author" no longer trips "auth", "secretary" no longer trips -/// "secret". Multi-word needles (e.g. "api key") match a consecutive run of -/// words. The text is lowercased and split on any non-alphanumeric char. -fn first_sensitive_topic(content: &str, tags: &[String]) -> Option<&'static str> { - // Tokenize content + tags into lowercased alphanumeric words. - let mut words: Vec = Vec::new(); - let mut push_words = |s: &str| { - for w in s - .to_ascii_lowercase() - .split(|c: char| !c.is_ascii_alphanumeric()) - { - if !w.is_empty() { - words.push(w.to_string()); - } - } - }; - push_words(content); - for t in tags { - push_words(t); - } - - SENSITIVE_TOPICS - .iter() - .find(|(needle, _)| matches_word_sequence(&words, needle)) - .map(|(_, label)| *label) -} - -/// Whether `needle` (one or more space-separated words) appears as a consecutive -/// whole-word run in `words`. -fn matches_word_sequence(words: &[String], needle: &str) -> bool { - let needle_words: Vec<&str> = needle.split_whitespace().collect(); - if needle_words.is_empty() { - return false; - } - if needle_words.len() == 1 { - return words.iter().any(|w| w == needle_words[0]); - } - words - .windows(needle_words.len()) - .any(|win| win.iter().zip(&needle_words).all(|(w, n)| w == n)) -} - -// ============================================================================ -// MEMORY PR DATA MODEL -// ============================================================================ - -/// What kind of change a Memory PR represents. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum MemoryPrKind { - /// A brand-new fact entering the brain. - NewFact, - /// An existing fact being strengthened / reinforced. - StrengthenedFact, - /// A contradiction was detected against existing memory. - ContradictionDetected, - /// A memory being superseded by a newer one. - MemorySuperseded, - /// A new edge added to the knowledge graph. - EdgeAdded, - /// A node decayed below the retention threshold. - NodeDecayed, - /// Dream consolidation proposed a merge / insight. - DreamConsolidation, -} - -impl MemoryPrKind { - /// Stable string label. - pub fn as_str(&self) -> &'static str { - match self { - MemoryPrKind::NewFact => "new_fact", - MemoryPrKind::StrengthenedFact => "strengthened_fact", - MemoryPrKind::ContradictionDetected => "contradiction_detected", - MemoryPrKind::MemorySuperseded => "memory_superseded", - MemoryPrKind::EdgeAdded => "edge_added", - MemoryPrKind::NodeDecayed => "node_decayed", - MemoryPrKind::DreamConsolidation => "dream_consolidation", - } - } - - /// Parse from a label; `None` if unrecognised. - pub fn from_label(s: &str) -> Option { - Some(match s { - "new_fact" => MemoryPrKind::NewFact, - "strengthened_fact" => MemoryPrKind::StrengthenedFact, - "contradiction_detected" => MemoryPrKind::ContradictionDetected, - "memory_superseded" => MemoryPrKind::MemorySuperseded, - "edge_added" => MemoryPrKind::EdgeAdded, - "node_decayed" => MemoryPrKind::NodeDecayed, - "dream_consolidation" => MemoryPrKind::DreamConsolidation, - _ => return None, - }) - } -} - -/// The review status of a Memory PR. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] -#[serde(rename_all = "snake_case")] -pub enum MemoryPrStatus { - /// Awaiting a decision. - #[default] - Pending, - /// Promoted into long-term memory as-is. - Promoted, - /// Merged into an existing memory. - Merged, - /// Superseded an existing memory. - Superseded, - /// Quarantined — held in the firewall, not used for retrieval. - Quarantined, - /// Forgotten — rejected and suppressed. - Forgotten, -} - -impl MemoryPrStatus { - /// Stable string label. - pub fn as_str(&self) -> &'static str { - match self { - MemoryPrStatus::Pending => "pending", - MemoryPrStatus::Promoted => "promoted", - MemoryPrStatus::Merged => "merged", - MemoryPrStatus::Superseded => "superseded", - MemoryPrStatus::Quarantined => "quarantined", - MemoryPrStatus::Forgotten => "forgotten", - } - } -} - -/// The actions a reviewer can take on a Memory PR (the buttons in the diff UI). -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum MemoryPrAction { - /// Accept the change as-is. - Promote, - /// Fold it into an existing memory. - Merge, - /// Use it to supersede an existing memory. - Supersede, - /// Hold it in the firewall. - Quarantine, - /// Reject and suppress it. - Forget, - /// Ask the agent to explain the change (returns the risk signals). - AskAgentWhy, -} - -impl MemoryPrAction { - /// Parse from a URL/path label; `None` if unrecognised. - pub fn from_label(s: &str) -> Option { - Some(match s { - "promote" => MemoryPrAction::Promote, - "merge" => MemoryPrAction::Merge, - "supersede" => MemoryPrAction::Supersede, - "quarantine" => MemoryPrAction::Quarantine, - "forget" => MemoryPrAction::Forget, - "ask_agent_why" | "ask-agent-why" | "why" => MemoryPrAction::AskAgentWhy, - _ => return None, - }) - } - - /// The status this action moves the PR into (`None` for `AskAgentWhy`, which - /// is read-only). - pub fn resulting_status(&self) -> Option { - Some(match self { - MemoryPrAction::Promote => MemoryPrStatus::Promoted, - MemoryPrAction::Merge => MemoryPrStatus::Merged, - MemoryPrAction::Supersede => MemoryPrStatus::Superseded, - MemoryPrAction::Quarantine => MemoryPrStatus::Quarantined, - MemoryPrAction::Forget => MemoryPrStatus::Forgotten, - MemoryPrAction::AskAgentWhy => return None, - }) - } - - /// Whether deciding the PR with this action should **release** the subject - /// memory from quarantine (reverse the suppression that gate_writes applied). - /// - /// A risky write is committed-then-suppressed; approving it must restore its - /// retrieval influence, otherwise the UI says "promoted" while the memory - /// stays held out — the bug this guards against. Accept actions release; - /// `Quarantine` keeps it held; `Forget` rejects it (stays suppressed); - /// `AskAgentWhy` is read-only. - pub fn releases_memory(&self) -> bool { - matches!( - self, - MemoryPrAction::Promote | MemoryPrAction::Merge | MemoryPrAction::Supersede - ) - } -} - -/// A reviewable change to the agent's brain — the persisted Memory PR record. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct MemoryPr { - /// UUID. - pub id: String, - /// What kind of change this is. - pub kind: MemoryPrKind, - /// Current review status. - pub status: MemoryPrStatus, - /// Short human title for the PR list. - pub title: String, - /// The proposed change as a structured diff (before/after, ids, payload). - pub diff: serde_json::Value, - /// The self-explaining risk signals that opened this PR. - pub signals: Vec, - /// The memory id this PR concerns, if any. - #[serde(skip_serializing_if = "Option::is_none")] - pub subject_id: Option, - /// The run that produced this change, linking the PR back to the black box. - #[serde(skip_serializing_if = "Option::is_none")] - pub run_id: Option, - /// RFC3339 creation time. - pub created_at: String, - /// RFC3339 decision time, once decided. - #[serde(skip_serializing_if = "Option::is_none")] - pub decided_at: Option, - /// The action that resolved this PR, once decided. - #[serde(skip_serializing_if = "Option::is_none")] - pub decision: Option, -} - -#[cfg(test)] -mod tests { - use super::*; - - fn ordinary() -> WriteContext { - WriteContext { - source: Some(WriteSource::Agent), - node_type: "fact".into(), - content: "The build uses cargo and pnpm.".into(), - tags: vec!["build".into()], - ..Default::default() - } - } - - #[test] - fn ordinary_write_auto_commits_in_risk_gated() { - let (class, signals) = classify_write(&ordinary(), ReviewMode::RiskGated); - assert_eq!(class, RiskClass::AutoCommit); - assert!(signals.is_empty()); - } - - #[test] - fn fast_mode_never_gates_even_risky_writes() { - let mut ctx = ordinary(); - ctx.supersedes = true; - ctx.contradicts_trust = Some(0.95); - let (class, _) = classify_write(&ctx, ReviewMode::Fast); - assert_eq!(class, RiskClass::AutoCommit); - } - - #[test] - fn paranoid_mode_gates_even_ordinary_writes() { - let (class, signals) = classify_write(&ordinary(), ReviewMode::Paranoid); - assert_eq!(class, RiskClass::Review); - assert_eq!(signals[0].code, "paranoid_mode"); - } - - #[test] - fn contradiction_against_high_trust_is_risky() { - let mut ctx = ordinary(); - ctx.contradicts_trust = Some(0.82); - let (class, signals) = classify_write(&ctx, ReviewMode::RiskGated); - assert_eq!(class, RiskClass::Review); - assert!(signals.iter().any(|s| s.code == "contradicts_high_trust")); - } - - #[test] - fn contradiction_against_low_trust_is_fine() { - let mut ctx = ordinary(); - ctx.contradicts_trust = Some(0.3); - let (class, _) = classify_write(&ctx, ReviewMode::RiskGated); - assert_eq!(class, RiskClass::AutoCommit); - } - - #[test] - fn supersede_forget_merge_protect_all_gate() { - for set in [ - |c: &mut WriteContext| c.supersedes = true, - |c: &mut WriteContext| c.forgets = true, - |c: &mut WriteContext| c.merges = true, - |c: &mut WriteContext| c.protects = true, - ] { - let mut ctx = ordinary(); - set(&mut ctx); - let (class, _) = classify_write(&ctx, ReviewMode::RiskGated); - assert_eq!(class, RiskClass::Review); - } - } - - #[test] - fn sensitive_topics_gate() { - for topic in [ - "remember my auth token is xyz", - "Sam's salary is confidential", - "the bounty payout terms", - "user preference: dark mode", - "this is a security vulnerability", - ] { - let mut ctx = ordinary(); - ctx.content = topic.into(); - let (class, signals) = classify_write(&ctx, ReviewMode::RiskGated); - assert_eq!(class, RiskClass::Review, "should gate: {topic}"); - assert!(signals.iter().any(|s| s.code == "sensitive_topic")); - } - } - - #[test] - fn sensitive_topic_word_boundary_no_false_positives_b6() { - // B6: these ordinary technical writes must NOT gate — they only CONTAIN - // a sensitive substring, they don't USE the sensitive word. - // These each only CONTAIN a sensitive substring; the word-boundary fix - // means they no longer gate. (Note: bare "license"/"contract"/"legal" - // ARE kept as gating words — a license/contract fact is legitimately - // legal-relevant — so they're intentionally not in this benign set.) - for benign in [ - "The tokenizer converts input strings to embeddings.", - "The author of this module is documented in the header.", - "The secretary pattern coordinates the worker pool.", - "Contraction of the array happens during compaction.", - "The authority record links to the canonical node.", - "The authentication-free endpoint is for health checks.", // "authentication" != "auth" - ] { - let mut ctx = ordinary(); - ctx.content = benign.into(); - ctx.node_type = "fact".into(); - ctx.tags = vec![]; - let (class, _) = classify_write(&ctx, ReviewMode::RiskGated); - assert_eq!( - class, - RiskClass::AutoCommit, - "must NOT gate ordinary write: {benign}" - ); - } - } - - #[test] - fn sensitive_topic_word_boundary_still_catches_real_b6() { - // The real sensitive phrasings must still gate. - for risky in [ - "store the auth token for the deploy", - "this is a security vulnerability in the parser", - "the api key for the service", - "remember the user preference for dark mode", - "the bounty payout is configured", - ] { - let mut ctx = ordinary(); - ctx.content = risky.into(); - ctx.node_type = "fact".into(); - ctx.tags = vec![]; - let (class, signals) = classify_write(&ctx, ReviewMode::RiskGated); - assert_eq!(class, RiskClass::Review, "must gate: {risky}"); - assert!(signals.iter().any(|s| s.code == "sensitive_topic")); - } - } - - #[test] - fn sensitive_node_type_gates() { - let mut ctx = ordinary(); - ctx.node_type = "identity".into(); - let (class, signals) = classify_write(&ctx, ReviewMode::RiskGated); - assert_eq!(class, RiskClass::Review); - assert!(signals.iter().any(|s| s.code == "sensitive_node_type")); - } - - #[test] - fn dream_consolidation_gates() { - let mut ctx = ordinary(); - ctx.source = Some(WriteSource::Dream); - let (class, signals) = classify_write(&ctx, ReviewMode::RiskGated); - assert_eq!(class, RiskClass::Review); - assert!(signals.iter().any(|s| s.code == "dream_consolidation")); - } - - #[test] - fn decayed_resurrection_gates() { - let mut ctx = ordinary(); - ctx.resurrects_decayed = true; - let (class, _) = classify_write(&ctx, ReviewMode::RiskGated); - assert_eq!(class, RiskClass::Review); - } - - #[test] - fn low_confidence_batch_gates_but_confident_batch_does_not() { - let mut low = ordinary(); - low.batch_import = true; - low.confidence = Some(0.3); - assert_eq!( - classify_write(&low, ReviewMode::RiskGated).0, - RiskClass::Review - ); - - let mut high = ordinary(); - high.batch_import = true; - high.confidence = Some(0.9); - assert_eq!( - classify_write(&high, ReviewMode::RiskGated).0, - RiskClass::AutoCommit - ); - } - - #[test] - fn weak_provenance_connector_gates_strong_does_not() { - let mut weak = ordinary(); - weak.source = Some(WriteSource::Connector); - weak.strong_provenance = false; - assert_eq!( - classify_write(&weak, ReviewMode::RiskGated).0, - RiskClass::Review - ); - - let mut strong = ordinary(); - strong.source = Some(WriteSource::Connector); - strong.strong_provenance = true; - assert_eq!( - classify_write(&strong, ReviewMode::RiskGated).0, - RiskClass::AutoCommit - ); - } - - #[test] - fn mode_label_roundtrip() { - assert_eq!(ReviewMode::from_label("FAST"), ReviewMode::Fast); - assert_eq!(ReviewMode::from_label("risk-gated"), ReviewMode::RiskGated); - assert_eq!(ReviewMode::from_label("paranoid"), ReviewMode::Paranoid); - assert_eq!(ReviewMode::from_label("garbage"), ReviewMode::RiskGated); - } - - #[test] - fn action_resulting_status() { - assert_eq!( - MemoryPrAction::Promote.resulting_status(), - Some(MemoryPrStatus::Promoted) - ); - assert_eq!(MemoryPrAction::AskAgentWhy.resulting_status(), None); - } - - #[test] - fn only_accept_actions_release_the_memory() { - // B1: accepting a risky write must release it from quarantine. - assert!(MemoryPrAction::Promote.releases_memory()); - assert!(MemoryPrAction::Merge.releases_memory()); - assert!(MemoryPrAction::Supersede.releases_memory()); - // Rejecting / holding keeps it suppressed. - assert!(!MemoryPrAction::Forget.releases_memory()); - assert!(!MemoryPrAction::Quarantine.releases_memory()); - assert!(!MemoryPrAction::AskAgentWhy.releases_memory()); - } -} diff --git a/crates/vestige-mcp/Cargo.toml b/crates/vestige-mcp/Cargo.toml index e55b586..3916203 100644 --- a/crates/vestige-mcp/Cargo.toml +++ b/crates/vestige-mcp/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "vestige-mcp" -version = "2.2.1" +version = "2.1.0" edition = "2024" -description = "Cognitive memory MCP server for AI agents - FSRS-6, spreading activation, synaptic tagging, 3D dashboard, and 130 years of memory research" +description = "Cognitive memory MCP server for Claude - FSRS-6, spreading activation, synaptic tagging, 3D dashboard, and 130 years of memory research" authors = ["samvallad33"] license = "AGPL-3.0-only" keywords = ["mcp", "ai", "memory", "fsrs", "neuroscience", "cognitive-science", "spaced-repetition"] @@ -10,18 +10,9 @@ categories = ["command-line-utilities", "database"] repository = "https://github.com/samvallad33/vestige" [features] -default = ["embeddings", "ort-download", "vector-search", "connectors", "cloud-sync"] +default = ["embeddings", "ort-download", "vector-search"] embeddings = ["vestige-core/embeddings"] vector-search = ["vestige-core/vector-search"] -# External-source connectors (#57): GitHub Issues / Redmine indexing via the -# `source_sync` MCP tool. On by default so the tool works out of the box; turn -# off for a build with no HTTP client. -connectors = ["vestige-core/connectors"] -# Hosted managed-sync (Vestige Cloud): `vestige sync --cloud` pushes/pulls the -# portable archive to the hosted blob service. On by default so the command -# works out of the box; the binary already links an HTTP client via -# `connectors`, so this adds no new dependency cost. -cloud-sync = ["vestige-core/cloud-sync"] # 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"] @@ -30,13 +21,6 @@ ort-download = ["embeddings", "vestige-core/ort-download"] # Usage: cargo build --no-default-features --features ort-dynamic,vector-search # Runtime: export ORT_DYLIB_PATH=$(brew --prefix onnxruntime)/lib/libonnxruntime.dylib ort-dynamic = ["embeddings", "vestige-core/ort-dynamic"] -qwen3-embeddings = ["embeddings", "vestige-core/qwen3-embeddings"] -qwen3-reranker = ["qwen3-embeddings"] -metal = ["embeddings", "vestige-core/metal"] -# CUDA GPU acceleration on NVIDIA hardware (Windows / Linux, x86_64 + aarch64). -# Pairs with `qwen3-embeddings`; see vestige-core Cargo.toml. -cuda = ["qwen3-embeddings", "vestige-core/cuda"] -cudnn = ["cuda", "vestige-core/cudnn"] [[bin]] name = "vestige-mcp" @@ -60,7 +44,7 @@ path = "src/bin/cli.rs" # Only `bundled-sqlite` is always on. `embeddings` and `vector-search` are # toggled via vestige-mcp's own feature flags below so `--no-default-features` # actually works (previously hardcoded here, which silently defeated the flag). -vestige-core = { version = "2.2.1", path = "../vestige-core", default-features = false, features = ["bundled-sqlite"] } +vestige-core = { version = "2.1.0", path = "../vestige-core", default-features = false, features = ["bundled-sqlite"] } # ============================================================================ # MCP Server Dependencies diff --git a/crates/vestige-mcp/README.md b/crates/vestige-mcp/README.md index 2547e42..10bdfa3 100644 --- a/crates/vestige-mcp/README.md +++ b/crates/vestige-mcp/README.md @@ -1,75 +1,115 @@ # Vestige MCP Server -Local cognitive memory for MCP-compatible AI agents. +A bleeding-edge Rust MCP (Model Context Protocol) server for Vestige - providing Claude and other AI assistants with long-term memory capabilities. -This crate provides the `vestige-mcp` stdio MCP server plus the `vestige` CLI. -The cognitive engine lives in `vestige-core`; this crate owns protocol handling, -tool dispatch, optional dashboard serving, backups, restore, update, and -portable import/export commands. +## Features -## Install +- **FSRS-6 Algorithm**: State-of-the-art spaced repetition (21 parameters, personalized decay) +- **Dual-Strength Memory Model**: Based on Bjork & Bjork 1992 cognitive science research +- **Local Semantic Embeddings**: nomic-embed-text-v1.5 (768d) via fastembed v5 (no external API) +- **HNSW Vector Search**: USearch-based, 20x faster than FAISS +- **Hybrid Search**: BM25 + semantic with RRF fusion +- **Codebase Memory**: Remember patterns, decisions, and context -For normal users, prefer the release package: +## Installation ```bash -npm install -g vestige-mcp-server +cd /path/to/vestige/crates/vestige-mcp +cargo build --release ``` -For local development: +Binary will be at `target/release/vestige-mcp` -```bash -cargo build --release -p vestige-mcp -``` +## Claude Desktop Configuration -## Register With An MCP Client - -Use the command `vestige-mcp` in any stdio MCP client: +Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS): ```json { "mcpServers": { "vestige": { - "command": "vestige-mcp" + "command": "/path/to/vestige-mcp" } } } ``` -Examples: +## Available Tools -```bash -claude mcp add vestige vestige-mcp -s user -codex mcp add vestige -- vestige-mcp +### Core Memory + +| Tool | Description | +|------|-------------| +| `ingest` | Add new knowledge to memory | +| `recall` | Search and retrieve memories | +| `semantic_search` | Find conceptually similar content | +| `hybrid_search` | Combined keyword + semantic search | +| `get_knowledge` | Retrieve a specific memory by ID | +| `delete_knowledge` | Delete a memory | +| `mark_reviewed` | Review with FSRS rating (1-4) | + +### Statistics & Maintenance + +| Tool | Description | +|------|-------------| +| `get_stats` | Memory system statistics | +| `health_check` | System health status | +| `run_consolidation` | Apply decay, generate embeddings | + +### Codebase Tools + +| Tool | Description | +|------|-------------| +| `remember_pattern` | Remember code patterns | +| `remember_decision` | Remember architectural decisions | +| `get_codebase_context` | Get patterns and decisions | + +## Available Resources + +### Memory Resources + +| URI | Description | +|-----|-------------| +| `memory://stats` | Current statistics | +| `memory://recent?n=10` | Recent memories | +| `memory://decaying` | Low retention memories | +| `memory://due` | Memories due for review | + +### Codebase Resources + +| URI | Description | +|-----|-------------| +| `codebase://structure` | Known codebases | +| `codebase://patterns` | Remembered patterns | +| `codebase://decisions` | Architectural decisions | + +## Example Usage (with Claude) + +``` +User: Remember that we decided to use FSRS-6 instead of SM-2 because it's 20-30% more efficient. + +Claude: [calls remember_decision] +I've recorded that architectural decision. + +User: What decisions have we made about algorithms? + +Claude: [calls get_codebase_context] +I found 1 decision: +- We decided to use FSRS-6 instead of SM-2 because it's 20-30% more efficient. ``` -## Transports +## Data Storage -- Default: JSON-RPC 2.0 over stdio. -- Optional: MCP-over-HTTP on `/mcp`, enabled only with `--http`, - `--http-port`, or `VESTIGE_HTTP_ENABLED=1`. -- Dashboard: `vestige dashboard` or `VESTIGE_DASHBOARD_ENABLED=1`. +- Database: `~/Library/Application Support/com.vestige.mcp/vestige-mcp.db` (macOS) +- Uses SQLite with FTS5 for full-text search +- Vector embeddings stored in separate table -HTTP and dashboard bearer tokens are generated locally; see -[`docs/CONFIGURATION.md`](../../docs/CONFIGURATION.md). +## Protocol -## Current Tool Surface - -The server exposes the current unified MCP tools from -[`src/server.rs`](src/server.rs), including: - -- `session_context` -- `search`, `smart_ingest`, `memory`, `codebase`, `intention` -- `deep_reference`, `cross_reference`, `contradictions` -- `dream`, `explore_connections`, `predict` -- `memory_health`, `memory_graph`, `composed_graph`, `system_status` -- `importance_score`, `find_duplicates` -- `consolidate`, `memory_timeline`, `memory_changelog` -- `backup`, `export`, `restore`, `gc`, `suppress` - -See the root [`README.md`](../../README.md) and -[`docs/AGENT-MEMORY-PROTOCOL.md`](../../docs/AGENT-MEMORY-PROTOCOL.md) for -agent instructions. +- JSON-RPC 2.0 over stdio +- MCP Protocol Version: 2024-11-05 +- Logging to stderr (stdout reserved for JSON-RPC) ## License -AGPL-3.0-only +MIT diff --git a/crates/vestige-mcp/src/autopilot.rs b/crates/vestige-mcp/src/autopilot.rs index 3e355fb..4b4c260 100644 --- a/crates/vestige-mcp/src/autopilot.rs +++ b/crates/vestige-mcp/src/autopilot.rs @@ -435,7 +435,6 @@ async fn handle_event( | VestigeEvent::MemoryUnsuppressed { .. } | VestigeEvent::Rac1CascadeSwept { .. } | VestigeEvent::DeepReferenceCompleted { .. } - | VestigeEvent::HookVerdictRecorded { .. } | VestigeEvent::DreamStarted { .. } | VestigeEvent::DreamProgress { .. } | VestigeEvent::DreamCompleted { .. } @@ -443,10 +442,7 @@ async fn handle_event( | VestigeEvent::ConsolidationCompleted { .. } | VestigeEvent::RetentionDecayed { .. } | VestigeEvent::ConnectionDiscovered { .. } - | VestigeEvent::ActivationSpread { .. } - | VestigeEvent::TraceEvent { .. } - | VestigeEvent::MemoryPrOpened { .. } - | VestigeEvent::MemoryPrDecided { .. } => {} + | VestigeEvent::ActivationSpread { .. } => {} } } diff --git a/crates/vestige-mcp/src/bin/cli.rs b/crates/vestige-mcp/src/bin/cli.rs index 15df46e..bc1f8ae 100644 --- a/crates/vestige-mcp/src/bin/cli.rs +++ b/crates/vestige-mcp/src/bin/cli.rs @@ -2,20 +2,20 @@ //! //! Command-line interface for managing cognitive memory system. -use std::collections::HashSet; use std::env; use std::fs; use std::io::{BufWriter, Write}; use std::path::Path; use std::path::PathBuf; use std::process::Command; -use std::sync::{Arc, OnceLock}; +use std::sync::Arc; use anyhow::Context; use chrono::{NaiveDate, Utc}; -use clap::{Args, Parser, Subcommand}; +use clap::{Parser, Subcommand}; use colored::Colorize; -use vestige_core::{IngestInput, PortableImportMode, Storage}; +use directories::ProjectDirs; +use vestige_core::{IngestInput, Storage}; /// Vestige - Cognitive Memory System CLI #[derive(Parser)] @@ -27,68 +27,10 @@ use vestige_core::{IngestInput, PortableImportMode, Storage}; long_about = "Vestige is a cognitive memory system based on 130 years of memory research.\n\nIt implements FSRS-6, spreading activation, synaptic tagging, and more." )] struct Cli { - /// Use a specific Vestige data directory for this command. - #[arg(long, global = true, value_name = "DIR")] - data_dir: Option, - #[command(subcommand)] command: Commands, } -static CLI_DB_PATH: OnceLock = OnceLock::new(); - -#[derive(Debug, Clone, Default, Args)] -struct SandwichInstallOptions { - /// Overwrite existing staged Vestige hook and agent files. - #[arg(long)] - force: bool, - - /// Wire optional UserPromptSubmit preflight hooks. - #[arg(long)] - enable_preflight: bool, - - /// Wire both optional preflight hooks and the optional Sanhedrin verifier. - #[arg(long)] - enable_sandwich: bool, - - /// Wire optional Sanhedrin Stop hook. - #[arg(long)] - enable_sanhedrin: bool, - - /// On Apple Silicon, auto-start the local MLX Sanhedrin backend. - #[arg(long)] - with_launchd: bool, - - /// Also stage the large memory-loader hook file. - #[arg(long)] - include_memory_loader: bool, - - /// OpenAI-compatible chat completions endpoint for optional Sanhedrin. - #[arg(long, value_name = "URL")] - sanhedrin_endpoint: Option, - - /// Model name passed to the optional Sanhedrin endpoint. - #[arg(long, value_name = "MODEL")] - sanhedrin_model: Option, - - /// Use a local checkout/release root containing hooks/ and agents/. - #[arg(long, value_name = "DIR", hide = true)] - src: Option, -} - -#[derive(Subcommand)] -enum SandwichCommands { - /// Install/update Cognitive Sandwich companion files without enabling hooks by default. - Install { - /// Install files from a specific release tag instead of latest. - #[arg(long)] - version: Option, - - #[command(flatten)] - options: SandwichInstallOptions, - }, -} - #[derive(Subcommand)] enum Commands { /// Show memory statistics @@ -110,7 +52,7 @@ enum Commands { /// Update Vestige binaries from the latest GitHub release Update { - /// Install a specific release tag instead of latest (example: v2.1.27) + /// Install a specific release tag instead of latest (example: v2.1.0) #[arg(long)] version: Option, @@ -121,23 +63,6 @@ enum Commands { /// Print what would be updated without changing files #[arg(long)] dry_run: bool, - - /// Deprecated: companion updates are skipped by default. - #[arg(long)] - no_sandwich: bool, - - /// Also refresh optional Claude Code Cognitive Sandwich companion files. - #[arg(long)] - sandwich_companion: bool, - - #[command(flatten)] - sandwich: SandwichInstallOptions, - }, - - /// Manage optional Claude Code Cognitive Sandwich companion files. - Sandwich { - #[command(subcommand)] - command: SandwichCommands, }, /// Restore memories from backup file @@ -167,37 +92,6 @@ enum Commands { since: Option, }, - /// Export an exact portable archive for Vestige-to-Vestige transfer - PortableExport { - /// Output archive path - output: PathBuf, - }, - - /// Import an exact portable archive - PortableImport { - /// Input archive path - input: PathBuf, - /// Merge into the current database instead of requiring an empty target - #[arg(long)] - merge: bool, - }, - - /// Two-way sync with a file-backed portable archive, or Vestige Cloud - Sync { - /// Sync archive path, often in Dropbox/iCloud/Syncthing/Git. - /// Omit when using --cloud. - archive: Option, - /// Sync with the hosted Vestige Cloud managed-sync service instead of a - /// file. Requires a sync key (VESTIGE_CLOUD_SYNC_KEY) and endpoint - /// (--endpoint or VESTIGE_CLOUD_ENDPOINT). - #[arg(long)] - cloud: bool, - /// Vestige Cloud base endpoint (e.g. https://sync.vestige.dev). - /// Defaults to the VESTIGE_CLOUD_ENDPOINT env var. - #[arg(long)] - endpoint: Option, - }, - /// Garbage collect stale memories below retention threshold Gc { /// Minimum retention strength to keep (delete below this) @@ -237,64 +131,6 @@ enum Commands { /// Source reference #[arg(long)] source: Option, - /// Backdate this memory N days in the past (for demos / seeding history) - #[arg(long)] - ago_days: Option, - }, - - /// Retroactive Salience Backfill — reach BACKWARD from a failure and surface - /// the quiet earlier memory that caused it (the root cause a vector search - /// can't find). Cai 2024 Nature. "Memory with hindsight." - Backfill { - /// ID of the failure memory; if omitted, the latest failure-like memory is used - #[arg(long)] - failure_id: Option, - /// Force the backfill even if the event isn't auto-detected as salient - #[arg(long)] - manual: bool, - /// How many days back to reach - #[arg(long, default_value = "30")] - lookback_days: i64, - /// Dry run: don't actually promote the surfaced cause - #[arg(long)] - no_promote: bool, - /// Demo mode: first show what a plain SEMANTIC SEARCH returns for the - /// failure (the lookalike, NOT the cause), then what Postdict surfaces. - #[arg(long)] - contrast: bool, - /// Machine-readable: print the raw backfill result as JSON (for tooling / - /// benchmarks). Suppresses the human-formatted output. - #[arg(long)] - json: bool, - }, - - /// Recall + reason across memories (deep_reference): hybrid search, FSRS-6 trust, - /// spreading activation, supersession + contradiction analysis. Returns the - /// synthesized answer, evidence, and confidence. - Recall { - /// The query / claim to reason about - query: String, - /// How many memories to analyze (candidate depth) - #[arg(long, default_value = "20")] - depth: i64, - /// Output raw JSON instead of the human-readable summary - #[arg(long)] - json: bool, - }, - - /// Compose: surface NEVER-COMPOSED memory pairs — two memories you wrote that nobody - /// (including you) ever connected — and the testable question they imply. The insight - /// generator: semantic-band + structural-bridge ranking over your cross-domain memory. - Compose { - /// How many candidate insight pairs to surface - #[arg(long, default_value = "5")] - limit: i32, - /// Optional tag filter (comma-separated) to focus a domain - #[arg(long)] - tags: Option, - /// Output raw JSON instead of the human-readable summary - #[arg(long)] - json: bool, }, /// Start standalone HTTP MCP server (no stdio, for remote access) @@ -314,13 +150,6 @@ enum Commands { fn main() -> anyhow::Result<()> { let cli = Cli::parse(); - if let Some(data_dir) = cli.data_dir { - let db_path = Storage::db_path_for_data_dir(data_dir)?; - CLI_DB_PATH - .set(db_path) - .map_err(|_| anyhow::anyhow!("data directory was initialized more than once"))?; - } - match cli.command { Commands::Stats { tagging, states } => run_stats(tagging, states), Commands::Health => run_health(), @@ -329,22 +158,7 @@ fn main() -> anyhow::Result<()> { version, install_dir, dry_run, - no_sandwich, - sandwich_companion, - sandwich, - } => run_update( - version, - install_dir, - dry_run, - no_sandwich, - sandwich_companion, - sandwich, - ), - Commands::Sandwich { command } => match command { - SandwichCommands::Install { version, options } => { - run_sandwich_install(version.as_deref(), &options) - } - }, + } => run_update(version, install_dir, dry_run), Commands::Restore { file } => run_restore(file), Commands::Backup { output } => run_backup(output), Commands::Export { @@ -353,13 +167,6 @@ fn main() -> anyhow::Result<()> { tags, since, } => run_export(output, format, tags, since), - Commands::PortableExport { output } => run_portable_export(output), - Commands::PortableImport { input, merge } => run_portable_import(input, merge), - Commands::Sync { - archive, - cloud, - endpoint, - } => run_sync(archive, cloud, endpoint), Commands::Gc { min_retention, max_age_days, @@ -372,18 +179,7 @@ fn main() -> anyhow::Result<()> { tags, node_type, source, - ago_days, - } => run_ingest(content, tags, node_type, source, ago_days), - Commands::Backfill { - failure_id, - manual, - lookback_days, - no_promote, - contrast, - json, - } => run_backfill(failure_id, manual, lookback_days, !no_promote, contrast, json), - Commands::Recall { query, depth, json } => run_recall(query, depth, json), - Commands::Compose { limit, tags, json } => run_compose(limit, tags, json), + } => run_ingest(content, tags, node_type, source), Commands::Serve { port, dashboard, @@ -460,7 +256,11 @@ fn release_download_url(asset: ReleaseAsset, version: Option<&str>) -> String { let archive_name = format!("vestige-mcp-{}.{}", asset.target, asset.archive_ext); match version { Some(version) => { - let tag = normalize_release_tag(version); + let tag = if version.starts_with('v') { + version.to_string() + } else { + format!("v{}", version) + }; format!( "https://github.com/samvallad33/vestige/releases/download/{}/{}", tag, archive_name @@ -473,615 +273,6 @@ fn release_download_url(asset: ReleaseAsset, version: Option<&str>) -> String { } } -fn normalize_release_tag(version: &str) -> String { - if version.starts_with('v') { - version.to_string() - } else { - format!("v{}", version) - } -} - -fn source_archive_url(tag: &str) -> String { - format!( - "https://github.com/samvallad33/vestige/archive/refs/tags/{}.tar.gz", - tag - ) -} - -fn download_file(url: &str, output: &Path, action: &str) -> anyhow::Result<()> { - run_command( - Command::new("curl") - .arg("-fsSL") - .arg("-A") - .arg("vestige-cli") - .arg(url) - .arg("-o") - .arg(output), - action, - ) -} - -fn parse_sha256(text: &str) -> anyhow::Result { - let hash = text - .split_whitespace() - .next() - .ok_or_else(|| anyhow::anyhow!("checksum file is empty"))? - .to_ascii_lowercase(); - if hash.len() != 64 || !hash.chars().all(|ch| ch.is_ascii_hexdigit()) { - anyhow::bail!("checksum file does not contain a valid SHA-256 hash"); - } - Ok(hash) -} - -fn sha256_from_command(command: &mut Command) -> anyhow::Result> { - match command.output() { - Ok(output) if output.status.success() => { - let text = String::from_utf8_lossy(&output.stdout); - Ok(Some(parse_sha256(&text)?)) - } - Ok(_) => Ok(None), - Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), - Err(err) => Err(err).context("failed to run checksum command"), - } -} - -fn compute_sha256(path: &Path) -> anyhow::Result { - #[cfg(windows)] - { - if let Some(hash) = sha256_from_command( - Command::new("powershell") - .arg("-NoProfile") - .arg("-Command") - .arg("(Get-FileHash -Algorithm SHA256 -LiteralPath $args[0]).Hash.ToLowerInvariant()") - .arg(path), - )? { - return Ok(hash); - } - } - - #[cfg(not(windows))] - { - if let Some(hash) = - sha256_from_command(Command::new("shasum").arg("-a").arg("256").arg(path))? - { - return Ok(hash); - } - if let Some(hash) = sha256_from_command(Command::new("sha256sum").arg(path))? { - return Ok(hash); - } - } - - anyhow::bail!("no SHA-256 command available to verify release archive"); -} - -fn verify_release_checksum(archive_path: &Path, checksum_path: &Path) -> anyhow::Result<()> { - let expected = parse_sha256(&fs::read_to_string(checksum_path).with_context(|| { - format!( - "failed to read release checksum file {}", - checksum_path.display() - ) - })?)?; - let actual = compute_sha256(archive_path)?; - if actual != expected { - anyhow::bail!( - "release archive checksum mismatch for {}", - archive_path.display() - ); - } - Ok(()) -} - -fn latest_release_tag() -> anyhow::Result { - let temp_dir = UpdateTempDir::create()?; - let metadata_path = temp_dir.path.join("latest-release.json"); - download_file( - "https://api.github.com/repos/samvallad33/vestige/releases/latest", - &metadata_path, - "checking latest Vestige release", - )?; - let file = fs::File::open(&metadata_path)?; - let metadata: serde_json::Value = - serde_json::from_reader(file).context("failed to parse latest Vestige release metadata")?; - metadata - .get("tag_name") - .and_then(|tag| tag.as_str()) - .map(|tag| tag.to_string()) - .ok_or_else(|| anyhow::anyhow!("latest Vestige release metadata did not include tag_name")) -} - -fn release_tag_for_source(version: Option<&str>) -> anyhow::Result { - match version { - Some(version) => Ok(normalize_release_tag(version)), - None => latest_release_tag(), - } -} - -fn find_sandwich_source_root(root: &Path) -> Option { - if root.join("hooks").is_dir() && root.join("agents").is_dir() { - return Some(root.to_path_buf()); - } - - let entries = fs::read_dir(root).ok()?; - for entry in entries.flatten() { - let path = entry.path(); - if path.is_dir() && path.join("hooks").is_dir() && path.join("agents").is_dir() { - return Some(path); - } - } - - None -} - -fn download_sandwich_source(version: Option<&str>, output_dir: &Path) -> anyhow::Result { - let tag = release_tag_for_source(version)?; - let archive_path = output_dir.join(format!("vestige-source-{}.tar.gz", tag)); - let url = source_archive_url(&tag); - - println!("{}: {}", "Sandwich source".white().bold(), tag); - download_file(&url, &archive_path, "downloading Vestige source archive")?; - extract_source_archive(&archive_path, output_dir)?; - find_sandwich_source_root(output_dir).ok_or_else(|| { - anyhow::anyhow!("Vestige source archive did not contain hooks/ and agents/ directories") - }) -} - -fn home_dir() -> anyhow::Result { - directories::BaseDirs::new() - .map(|dirs| dirs.home_dir().to_path_buf()) - .ok_or_else(|| anyhow::anyhow!("failed to locate home directory")) -} - -fn is_vestige_hook_command(command: &str) -> bool { - const NEEDLES: &[&str] = &[ - "synthesis-preflight.sh", - "cwd-state-injector.sh", - "vestige-pulse-daemon.sh", - "preflight-swarm.sh", - "load-all-memory.sh", - "veto-detector.sh", - "sanhedrin.sh", - "synthesis-stop-validator.sh", - "synthesis-gate.sh", - ]; - NEEDLES.iter().any(|needle| command.contains(needle)) -} - -fn scrub_vestige_hooks(settings: &mut serde_json::Value) { - let Some(hooks) = settings - .get_mut("hooks") - .and_then(|hooks| hooks.as_object_mut()) - else { - return; - }; - - for event_name in ["UserPromptSubmit", "Stop"] { - let Some(groups) = hooks - .get_mut(event_name) - .and_then(|groups| groups.as_array_mut()) - else { - continue; - }; - - for group in groups.iter_mut() { - if let Some(commands) = group - .get_mut("hooks") - .and_then(|hooks| hooks.as_array_mut()) - { - commands.retain(|hook| { - !hook - .get("command") - .and_then(|command| command.as_str()) - .is_some_and(is_vestige_hook_command) - }); - } - } - - groups.retain(|group| { - group - .get("hooks") - .and_then(|hooks| hooks.as_array()) - .is_some_and(|hooks| !hooks.is_empty()) - }); - } - - hooks.retain(|_, value| match value { - serde_json::Value::Array(items) => !items.is_empty(), - serde_json::Value::Object(items) => !items.is_empty(), - serde_json::Value::Null => false, - _ => true, - }); - - if hooks.is_empty() - && let Some(root) = settings.as_object_mut() - { - root.remove("hooks"); - } -} - -fn merge_json(base: &mut serde_json::Value, overlay: serde_json::Value) { - match (base, overlay) { - (serde_json::Value::Object(base), serde_json::Value::Object(overlay)) => { - for (key, value) in overlay { - match base.get_mut(&key) { - Some(existing) => merge_json(existing, value), - None => { - base.insert(key, value); - } - } - } - } - (base, overlay) => *base = overlay, - } -} - -fn merge_settings_fragment( - settings: &mut serde_json::Value, - fragment_path: &Path, -) -> anyhow::Result<()> { - let file = fs::File::open(fragment_path) - .with_context(|| format!("failed to open {}", fragment_path.display()))?; - let fragment: serde_json::Value = serde_json::from_reader(file) - .with_context(|| format!("failed to parse {}", fragment_path.display()))?; - merge_json(settings, fragment); - Ok(()) -} - -fn copy_companion_files( - source_dir: &Path, - destination_dir: &Path, - allowed_extensions: &[&str], - _mode: u32, - options: &SandwichInstallOptions, -) -> anyhow::Result<(usize, usize)> { - fs::create_dir_all(destination_dir)?; - let mut copied = 0; - let mut skipped = 0; - - for entry in fs::read_dir(source_dir) - .with_context(|| format!("failed to read {}", source_dir.display()))? - { - let entry = entry?; - let source = entry.path(); - if !source.is_file() { - continue; - } - - let extension = source - .extension() - .and_then(|ext| ext.to_str()) - .unwrap_or(""); - if !allowed_extensions.contains(&extension) { - continue; - } - - let Some(file_name) = source.file_name() else { - continue; - }; - if file_name.to_string_lossy() == "load-all-memory.sh" && !options.include_memory_loader { - continue; - } - - let destination = destination_dir.join(file_name); - if destination.exists() && !options.force { - skipped += 1; - continue; - } - - fs::copy(&source, &destination).with_context(|| { - format!( - "failed to copy {} to {}", - source.display(), - destination.display() - ) - })?; - - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let mut perms = fs::metadata(&destination)?.permissions(); - perms.set_mode(_mode); - fs::set_permissions(&destination, perms)?; - } - - copied += 1; - } - - Ok((copied, skipped)) -} - -fn quote_shell_env(value: &str) -> String { - format!("'{}'", value.replace('\'', "'\\''")) -} - -fn write_sanhedrin_env( - hooks_dir: &Path, - endpoint: &str, - model: &str, - dashboard_port: &str, -) -> anyhow::Result<()> { - let env_path = hooks_dir.join("vestige-sanhedrin.env"); - let contents = format!( - "VESTIGE_SANHEDRIN_ENABLED=1\nVESTIGE_SANHEDRIN_ENDPOINT={}\nVESTIGE_SANHEDRIN_MODEL={}\nVESTIGE_DASHBOARD_PORT={}\nVESTIGE_SANHEDRIN_CLAIM_MODE=1\nVESTIGE_SANHEDRIN_OUTPUT=json\n", - quote_shell_env(endpoint), - quote_shell_env(model), - quote_shell_env(dashboard_port) - ); - fs::write(&env_path, contents)?; - - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let mut perms = fs::metadata(&env_path)?.permissions(); - perms.set_mode(0o600); - fs::set_permissions(&env_path, perms)?; - } - - println!("{}: {}", "Sanhedrin env".white().bold(), env_path.display()); - Ok(()) -} - -fn install_launchd_job(source_root: &Path, home: &Path, model: &str) -> anyhow::Result<()> { - let launchd_dir = home.join("Library").join("LaunchAgents"); - fs::create_dir_all(&launchd_dir)?; - - let template_path = source_root - .join("launchd") - .join("com.vestige.mlx-server.plist.template"); - let template = fs::read_to_string(&template_path) - .with_context(|| format!("failed to read {}", template_path.display()))?; - // XML-escape interpolated values: this plist is XML, and an unescaped model - // string containing &, <, >, " or ' would corrupt the plist (or inject - // elements). Escape before substitution. - let xml_escape = |s: &str| { - s.replace('&', "&") - .replace('<', "<") - .replace('>', ">") - .replace('"', """) - .replace('\'', "'") - }; - let rendered = template - .replace("__HOME__", &xml_escape(&home.display().to_string())) - .replace("__MODEL__", &xml_escape(model)); - - let plist = launchd_dir.join("com.vestige.mlx-server.plist"); - fs::write(&plist, rendered)?; - let _ = Command::new("launchctl").arg("unload").arg(&plist).status(); - run_command( - Command::new("launchctl").arg("load").arg(&plist), - "loading Vestige MLX launchd job", - )?; - println!("{}: {}", "launchd".white().bold(), plist.display()); - Ok(()) -} - -fn remove_legacy_launchd_job(home: &Path) { - if env::consts::OS != "macos" { - return; - } - - let plist = home - .join("Library") - .join("LaunchAgents") - .join("com.vestige.mlx-server.plist"); - if plist.exists() { - let _ = Command::new("launchctl").arg("unload").arg(&plist).status(); - if fs::remove_file(&plist).is_ok() { - println!( - "{}: removed old Sanhedrin launchd job", - "launchd".white().bold() - ); - } - } -} - -fn install_sandwich_from_source( - source_root: &Path, - options: &SandwichInstallOptions, -) -> anyhow::Result<()> { - let home = home_dir()?; - let claude_dir = home.join(".claude"); - let hooks_dir = claude_dir.join("hooks"); - let agents_dir = claude_dir.join("agents"); - let settings_path = claude_dir.join("settings.json"); - let source_root = - find_sandwich_source_root(source_root).unwrap_or_else(|| source_root.to_path_buf()); - - if !source_root.join("hooks").is_dir() || !source_root.join("agents").is_dir() { - anyhow::bail!( - "Cognitive Sandwich source missing hooks/ or agents/: {}", - source_root.display() - ); - } - - let enable_preflight = options.enable_preflight || options.enable_sandwich; - let mut enable_sanhedrin = - options.enable_sanhedrin || options.enable_sandwich || options.with_launchd; - let mut with_launchd = options.with_launchd; - - if with_launchd && (env::consts::OS != "macos" || env::consts::ARCH != "aarch64") { - println!( - "{}", - "--with-launchd is Apple Silicon only; using endpoint-backed Sanhedrin instead." - .yellow() - ); - with_launchd = false; - enable_sanhedrin = true; - } - - fs::create_dir_all(&claude_dir)?; - let (hooks_copied, hooks_skipped) = copy_companion_files( - &source_root.join("hooks"), - &hooks_dir, - &["sh", "py"], - 0o755, - options, - )?; - let (json_copied, json_skipped) = copy_companion_files( - &source_root.join("hooks"), - &hooks_dir, - &["json"], - 0o644, - options, - )?; - let (agents_copied, agents_skipped) = copy_companion_files( - &source_root.join("agents"), - &agents_dir, - &["md"], - 0o644, - options, - )?; - - println!( - "{}: {} installed, {} skipped", - "Hooks".white().bold(), - hooks_copied + json_copied, - hooks_skipped + json_skipped - ); - println!( - "{}: {} installed, {} skipped", - "Agents".white().bold(), - agents_copied, - agents_skipped - ); - - if !with_launchd { - remove_legacy_launchd_job(&home); - } - - let dashboard_port = env::var("VESTIGE_DASHBOARD_PORT").unwrap_or_else(|_| "3927".to_string()); - let mut endpoint = options - .sanhedrin_endpoint - .clone() - .or_else(|| env::var("VESTIGE_SANHEDRIN_ENDPOINT").ok()) - .or_else(|| env::var("MLX_ENDPOINT").ok()) - .unwrap_or_default() - .trim_end_matches('/') - .to_string(); - let mut model = options - .sanhedrin_model - .clone() - .or_else(|| env::var("VESTIGE_SANHEDRIN_MODEL").ok()) - .or_else(|| env::var("VESTIGE_SANDWICH_MODEL").ok()) - .unwrap_or_default(); - if with_launchd { - if endpoint.is_empty() { - endpoint = "http://127.0.0.1:8080/v1/chat/completions".to_string(); - } - if model.is_empty() { - model = "mlx-community/Qwen3.6-35B-A3B-4bit".to_string(); - } - } - - if enable_sanhedrin { - if endpoint.is_empty() || model.is_empty() { - println!( - "{}", - "Sanhedrin enabled without a verifier model; it will fail open until VESTIGE_SANHEDRIN_ENDPOINT and VESTIGE_SANHEDRIN_MODEL are set." - .yellow() - ); - } - write_sanhedrin_env(&hooks_dir, &endpoint, &model, &dashboard_port)?; - } - if with_launchd { - install_launchd_job(&source_root, &home, &model)?; - } - - if !settings_path.exists() { - fs::write(&settings_path, "{}\n")?; - } - let backup_path = claude_dir.join("settings.json.bak.pre-sandwich"); - if !backup_path.exists() { - fs::copy(&settings_path, &backup_path)?; - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let mut perms = fs::metadata(&backup_path)?.permissions(); - perms.set_mode(0o600); - fs::set_permissions(&backup_path, perms)?; - } - } - - let settings_file = fs::File::open(&settings_path)?; - let mut settings: serde_json::Value = - serde_json::from_reader(settings_file).unwrap_or_else(|_| serde_json::json!({})); - scrub_vestige_hooks(&mut settings); - - if enable_preflight { - merge_settings_fragment( - &mut settings, - &source_root - .join("hooks") - .join("settings.preflight.fragment.json"), - )?; - } - if enable_sanhedrin { - merge_settings_fragment( - &mut settings, - &source_root - .join("hooks") - .join("settings.sanhedrin.fragment.json"), - )?; - } - - let mut settings_file = fs::File::create(&settings_path)?; - serde_json::to_writer_pretty(&mut settings_file, &settings)?; - writeln!(settings_file)?; - - if enable_preflight || enable_sanhedrin { - let mut layers = Vec::new(); - if enable_preflight { - layers.push("preflight"); - } - if enable_sanhedrin { - layers.push("sanhedrin"); - } - println!( - "{}: enabled optional layer(s): {}", - "Settings".white().bold(), - layers.join(", ") - ); - } else { - println!( - "{}: no Vestige Claude Code hooks enabled by default", - "Settings".white().bold() - ); - } - - Ok(()) -} - -fn run_sandwich_install( - version: Option<&str>, - options: &SandwichInstallOptions, -) -> anyhow::Result<()> { - println!( - "{}", - "=== Vestige Cognitive Sandwich Install ===".cyan().bold() - ); - println!(); - - if let Some(source_root) = &options.src { - install_sandwich_from_source(source_root, options)?; - } else { - let temp_dir = UpdateTempDir::create()?; - let source_root = download_sandwich_source(version, &temp_dir.path)?; - install_sandwich_from_source(&source_root, options)?; - } - - println!(); - let optional_layers_enabled = options.enable_preflight - || options.enable_sandwich - || options.enable_sanhedrin - || options.with_launchd; - let message = if optional_layers_enabled { - "Cognitive Sandwich files updated. Restart Claude Code to use enabled optional hooks." - } else { - "Cognitive Sandwich files updated. No hooks enabled; no automatic model calls." - }; - println!("{}", message.green().bold()); - Ok(()) -} - fn run_command(command: &mut Command, action: &str) -> anyhow::Result<()> { let status = command .status() @@ -1092,123 +283,11 @@ fn run_command(command: &mut Command, action: &str) -> anyhow::Result<()> { Ok(()) } -fn create_private_file(path: &Path) -> std::io::Result { - #[cfg(unix)] - { - use std::os::unix::fs::OpenOptionsExt; - fs::OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .mode(0o600) - .open(path) - } - - #[cfg(not(unix))] - { - fs::File::create(path) - } -} - -fn command_output(command: &mut Command, action: &str) -> anyhow::Result { - let output = command - .output() - .with_context(|| format!("failed to start {}", action))?; - if !output.status.success() { - anyhow::bail!("{} failed with status {}", action, output.status); - } - Ok(String::from_utf8_lossy(&output.stdout).to_string()) -} - -fn powershell_quote(value: &Path) -> String { - format!("'{}'", value.display().to_string().replace('\'', "''")) -} - -fn normalize_archive_entry(entry: &str) -> anyhow::Result { - let normalized = entry.trim().replace('\\', "/"); - let normalized = normalized.strip_prefix("./").unwrap_or(&normalized); - if normalized.is_empty() - || normalized.starts_with('/') - || normalized.get(1..2) == Some(":") - || normalized - .split('/') - .any(|part| part.is_empty() || part == "..") - { - anyhow::bail!("archive contains unsafe entry: {}", entry); - } - Ok(normalized.to_string()) -} - -fn archive_listing(archive_path: &Path, archive_ext: &str) -> anyhow::Result { - let listing = match archive_ext { - "tar.gz" => command_output( - Command::new("tar").arg("-tzf").arg(archive_path), - "listing Vestige archive with tar", - )?, - "zip" => { - let script = format!( - "Add-Type -AssemblyName System.IO.Compression.FileSystem; \ - $zip = [System.IO.Compression.ZipFile]::OpenRead({}); \ - try {{ $zip.Entries | ForEach-Object {{ $_.FullName }} }} finally {{ $zip.Dispose() }}", - powershell_quote(archive_path) - ); - command_output( - Command::new("powershell") - .arg("-NoProfile") - .arg("-Command") - .arg(script), - "listing Vestige archive with PowerShell", - )? - } - other => anyhow::bail!("unsupported release archive extension: {}", other), - }; - Ok(listing) -} - -fn validate_archive_safety(archive_path: &Path, archive_ext: &str) -> anyhow::Result<()> { - let listing = archive_listing(archive_path, archive_ext)?; - for entry in listing.lines().filter(|line| !line.trim().is_empty()) { - normalize_archive_entry(entry)?; - } - Ok(()) -} - -fn validate_archive_entries( - archive_path: &Path, - archive_ext: &str, - expected_members: &[String], -) -> anyhow::Result<()> { - let listing = archive_listing(archive_path, archive_ext)?; - - let expected: HashSet<&str> = expected_members.iter().map(String::as_str).collect(); - for entry in listing.lines().filter(|line| !line.trim().is_empty()) { - let normalized = normalize_archive_entry(entry)?; - if !expected.contains(normalized.as_str()) { - anyhow::bail!("release archive contains unexpected entry: {}", entry); - } - } - Ok(()) -} - -fn extract_source_archive(archive_path: &Path, output_dir: &Path) -> anyhow::Result<()> { - validate_archive_safety(archive_path, "tar.gz")?; - run_command( - Command::new("tar") - .arg("-xzf") - .arg(archive_path) - .arg("-C") - .arg(output_dir), - "extracting Vestige source archive with tar", - ) -} - fn extract_archive( archive_path: &Path, output_dir: &Path, archive_ext: &str, - expected_members: &[String], ) -> anyhow::Result<()> { - validate_archive_entries(archive_path, archive_ext, expected_members)?; match archive_ext { "tar.gz" => run_command( Command::new("tar") @@ -1223,9 +302,9 @@ fn extract_archive( .arg("-NoProfile") .arg("-Command") .arg(format!( - "Expand-Archive -LiteralPath {} -DestinationPath {} -Force", - powershell_quote(archive_path), - powershell_quote(output_dir) + "Expand-Archive -Path '{}' -DestinationPath '{}' -Force", + archive_path.display(), + output_dir.display() )), "extracting Vestige release archive with PowerShell", ), @@ -1285,9 +364,6 @@ fn run_update( version: Option, install_dir: Option, dry_run: bool, - no_sandwich: bool, - sandwich_companion: bool, - sandwich: SandwichInstallOptions, ) -> anyhow::Result<()> { println!("{}", "=== Vestige Update ===".cyan().bold()); println!(); @@ -1338,35 +414,22 @@ fn run_update( let temp_dir = UpdateTempDir::create()?; let archive_path = temp_dir.path.join(&archive_name); - let checksum_path = temp_dir.path.join(format!("{}.sha256", archive_name)); println!(); println!("{}", "Downloading release archive...".cyan()); - download_file(&url, &archive_path, "downloading Vestige release archive")?; - download_file( - &format!("{}.sha256", url), - &checksum_path, - "downloading Vestige release checksum", + run_command( + Command::new("curl") + .arg("-fL") + .arg(&url) + .arg("-o") + .arg(&archive_path), + "downloading Vestige release archive with curl", )?; - verify_release_checksum(&archive_path, &checksum_path)?; - - let binaries = ["vestige", "vestige-mcp", "vestige-restore"]; - let mut expected_members = binaries - .iter() - .map(|binary| format!("{}{}", binary, asset.binary_suffix)) - .collect::>(); - if asset.target == "x86_64-apple-darwin" { - expected_members.push("INSTALL-INTEL-MAC.md".to_string()); - } println!("{}", "Extracting release archive...".cyan()); - extract_archive( - &archive_path, - &temp_dir.path, - asset.archive_ext, - &expected_members, - )?; + extract_archive(&archive_path, &temp_dir.path, asset.archive_ext)?; + let binaries = ["vestige", "vestige-mcp", "vestige-restore"]; for binary in binaries { let filename = format!("{}{}", binary, asset.binary_suffix); let source = temp_dir.path.join(&filename); @@ -1392,37 +455,17 @@ fn run_update( println!( "{}", - "Binary update complete. Restart your MCP client to pick up the new binary." + "Update complete. Restart your MCP client to pick up the new binary." .green() .bold() ); - if sandwich_companion && !no_sandwich { - println!(); - println!( - "{}", - "Updating Cognitive Sandwich companion files...".cyan() - ); - run_sandwich_install(version.as_deref(), &sandwich)?; - } else if no_sandwich { - println!( - "{}", - "Skipped Cognitive Sandwich companion update (--no-sandwich).".yellow() - ); - } else { - println!( - "{}", - "Skipped Cognitive Sandwich companion update (default). Pass --sandwich-companion to refresh Claude Code companion files." - .yellow() - ); - } - Ok(()) } /// Run stats command fn run_stats(show_tagging: bool, show_states: bool) -> anyhow::Result<()> { - let storage = open_storage()?; + let storage = Storage::new(None)?; let stats = storage.get_stats()?; println!("{}", "=== Vestige Memory Statistics ===".cyan().bold()); @@ -1456,20 +499,8 @@ fn run_stats(show_tagging: bool, show_states: bool) -> anyhow::Result<()> { stats.nodes_with_embeddings ); - if let Some(model) = &stats.active_embedding_model { - println!("{}: {}", "Active Embedding Model".white().bold(), model); - } - if let Some(model) = &stats.embedding_model { - println!("{}: {}", "Stored Embedding Model".white().bold(), model); - } - - if stats.nodes_with_mismatched_embeddings > 0 { - println!( - "{}: {}", - "Mismatched Embeddings".white().bold(), - stats.nodes_with_mismatched_embeddings - ); + println!("{}: {}", "Embedding Model".white().bold(), model); } if let Some(oldest) = stats.oldest_memory { @@ -1489,13 +520,13 @@ fn run_stats(show_tagging: bool, show_states: bool) -> anyhow::Result<()> { // Embedding coverage let embedding_coverage = if stats.total_nodes > 0 { - (stats.nodes_with_active_embeddings as f64 / stats.total_nodes as f64) * 100.0 + (stats.nodes_with_embeddings as f64 / stats.total_nodes as f64) * 100.0 } else { 0.0 }; println!( "{}: {:.1}%", - "Active Embedding Coverage".white().bold(), + "Embedding Coverage".white().bold(), embedding_coverage ); @@ -1620,7 +651,7 @@ fn print_distribution_bar(label: &str, count: usize, total: usize, color: &str) /// Run health check fn run_health() -> anyhow::Result<()> { - let storage = open_storage()?; + let storage = Storage::new(None)?; let stats = storage.get_stats()?; println!("{}", "=== Vestige Health Check ===".cyan().bold()); @@ -1659,13 +690,13 @@ fn run_health() -> anyhow::Result<()> { // Embedding coverage let embedding_coverage = if stats.total_nodes > 0 { - (stats.nodes_with_active_embeddings as f64 / stats.total_nodes as f64) * 100.0 + (stats.nodes_with_embeddings as f64 / stats.total_nodes as f64) * 100.0 } else { 0.0 }; println!( "{}: {:.1}%", - "Active Embedding Coverage".white(), + "Embedding Coverage".white(), embedding_coverage ); println!( @@ -1690,18 +721,14 @@ fn run_health() -> anyhow::Result<()> { warnings.push("Many memories are due for review"); } - if stats.total_nodes > 0 && stats.nodes_with_active_embeddings == 0 { - warnings.push("No active-model embeddings generated - semantic search unavailable"); + 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 consolidation to improve semantic search"); } - if stats.nodes_with_mismatched_embeddings > 0 { - warnings.push("Stored embeddings from another model are present - run consolidation after changing embedding models"); - } - if !warnings.is_empty() { println!(); println!("{}", "Warnings:".yellow().bold()); @@ -1722,9 +749,9 @@ fn run_health() -> anyhow::Result<()> { recommendations.push("Review due memories to strengthen retention."); } - if stats.nodes_with_active_embeddings < stats.total_nodes { + if stats.nodes_with_embeddings < stats.total_nodes { recommendations - .push("Run 'vestige consolidate' to generate active-model embeddings for better semantic search."); + .push("Run 'vestige consolidate' to generate embeddings for better semantic search."); } if stats.total_nodes > 100 && stats.average_retention < 0.7 { @@ -1761,7 +788,7 @@ fn run_consolidate() -> anyhow::Result<()> { println!("Running memory consolidation cycle..."); println!(); - let storage = open_storage()?; + let storage = Storage::new(None)?; let result = storage.run_consolidation()?; println!( @@ -1807,49 +834,7 @@ fn run_restore(backup_path: PathBuf) -> anyhow::Result<()> { println!("Loading backup from: {}", backup_path.display()); // Read and parse backup - let backup_bytes = std::fs::read(&backup_path)?; - if backup_bytes.starts_with(b"SQLite format 3\0") { - anyhow::bail!( - "{} is a raw SQLite database backup, not a JSON restore file. Use portable-export/portable-import for cross-device transfer, or replace the database file manually while Vestige is stopped.", - backup_path.display() - ); - } - let backup_content = String::from_utf8(backup_bytes) - .with_context(|| format!("{} is not UTF-8 JSON", backup_path.display()))?; - - if let Ok(archive) = serde_json::from_str::(&backup_content) - && archive.archive_format == vestige_core::PORTABLE_ARCHIVE_FORMAT - { - println!("Detected portable archive."); - println!("{}: {}", "Format".white().bold(), archive.archive_format); - println!("{}: {}", "Schema".white().bold(), archive.schema_version); - println!("{}: {}", "Tables".white().bold(), archive.tables.len()); - println!("{}: {}", "Rows".white().bold(), archive.total_rows()); - println!(); - - let storage = open_storage()?; - let report = storage.import_portable_archive(&archive, PortableImportMode::EmptyOnly)?; - - println!( - "{}: {}", - "Tables imported".white().bold(), - report.tables_imported - ); - println!( - "{}: {}", - "Rows imported".white().bold(), - report.rows_imported - ); - println!( - "{}: {}", - "Tables skipped".white().bold(), - report.tables_skipped - ); - println!("{}: {}", "FTS rebuilt".white().bold(), report.fts_rebuilt); - println!(); - println!("{}", "Portable restore complete.".green().bold()); - return Ok(()); - } + let backup_content = std::fs::read_to_string(&backup_path)?; #[derive(serde::Deserialize)] struct BackupWrapper { @@ -1872,27 +857,16 @@ fn run_restore(backup_path: PathBuf) -> anyhow::Result<()> { source: Option, } - let memories = if let Ok(wrapper) = serde_json::from_str::>(&backup_content) - { - let first = wrapper.first().context("backup wrapper is empty")?; - let recall_result: RecallResult = serde_json::from_str(&first.text)?; - recall_result.results - } else if let Ok(recall_result) = serde_json::from_str::(&backup_content) { - recall_result.results - } else if let Ok(memories) = serde_json::from_str::>(&backup_content) { - memories - } else { - anyhow::bail!( - "Unrecognized backup format. Expected portable archive, MCP wrapper, RecallResult, or array of memories." - ); - }; + let wrapper: Vec = serde_json::from_str(&backup_content)?; + let recall_result: RecallResult = serde_json::from_str(&wrapper[0].text)?; + let memories = recall_result.results; println!("Found {} memories to restore", memories.len()); println!(); // Initialize storage println!("Initializing storage..."); - let storage = open_storage()?; + let storage = Storage::new(None)?; println!("Generating embeddings and ingesting memories..."); println!(); @@ -1910,7 +884,6 @@ fn run_restore(backup_path: PathBuf) -> anyhow::Result<()> { tags: memory.tags.unwrap_or_default(), valid_from: None, valid_until: None, - source_envelope: None, }; match storage.ingest(input) { @@ -1943,8 +916,8 @@ fn run_restore(backup_path: PathBuf) -> anyhow::Result<()> { println!("{}: {}", "Total Nodes".white(), stats.total_nodes); println!( "{}: {}", - "Active Embeddings".white(), - stats.nodes_with_active_embeddings + "With Embeddings".white(), + stats.nodes_with_embeddings ); Ok(()) @@ -1952,20 +925,9 @@ fn run_restore(backup_path: PathBuf) -> anyhow::Result<()> { /// Get the default database path fn get_default_db_path() -> anyhow::Result { - if let Some(path) = CLI_DB_PATH.get() { - Ok(path.clone()) - } else { - Ok(Storage::default_db_path()?) - } -} - -/// Open storage using the CLI-selected data directory, if one was provided. -fn open_storage() -> anyhow::Result { - if let Some(path) = CLI_DB_PATH.get() { - Ok(Storage::new(Some(path.clone()))?) - } else { - Ok(Storage::new(None)?) - } + let proj_dirs = ProjectDirs::from("com", "vestige", "core") + .ok_or_else(|| anyhow::anyhow!("Could not determine project directories"))?; + Ok(proj_dirs.data_dir().join("vestige.db")) } /// Fetch all nodes from storage using pagination @@ -2001,21 +963,15 @@ fn run_backup(output: PathBuf) -> anyhow::Result<()> { // Open storage to flush WAL before copying println!("Flushing WAL checkpoint..."); { - let storage = open_storage()?; + let storage = Storage::new(None)?; // get_stats triggers a read so the connection is active, then drop flushes let _ = storage.get_stats()?; } - // Also flush WAL directly via a separate connection for extra safety. This - // is a raw, UN-keyed connection, so on a SQLCipher-encrypted DB it cannot - // read the header and the checkpoint fails with "file is not a database". - // The keyed `storage` opened above already flushed the WAL on drop, so this - // is redundant belt-and-suspenders — make it best-effort instead of letting - // it abort the whole backup on encrypted databases. + // Also flush WAL directly via a separate connection for safety { - if let Ok(conn) = rusqlite::Connection::open(&db_path) { - let _ = conn.execute_batch("PRAGMA wal_checkpoint(TRUNCATE);"); - } + let conn = rusqlite::Connection::open(&db_path)?; + conn.execute_batch("PRAGMA wal_checkpoint(TRUNCATE);")?; } // Create parent directories if needed @@ -2031,13 +987,6 @@ fn run_backup(output: PathBuf) -> anyhow::Result<()> { println!(" {} {}", "To:".dimmed(), output.display()); std::fs::copy(&db_path, &output)?; - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let mut perms = std::fs::metadata(&output)?.permissions(); - perms.set_mode(0o600); - std::fs::set_permissions(&output, perms)?; - } let file_size = std::fs::metadata(&output)?.len(); let size_display = if file_size >= 1024 * 1024 { @@ -2101,7 +1050,7 @@ fn run_export( }) .unwrap_or_default(); - let storage = open_storage()?; + let storage = Storage::new(None)?; let all_nodes = fetch_all_nodes(&storage)?; // Apply filters @@ -2148,7 +1097,7 @@ fn run_export( std::fs::create_dir_all(parent)?; } - let file = create_private_file(&output)?; + let file = std::fs::File::create(&output)?; let mut writer = BufWriter::new(file); match format.as_str() { @@ -2192,217 +1141,6 @@ fn run_export( Ok(()) } -/// Run exact portable archive export. -fn run_portable_export(output: PathBuf) -> anyhow::Result<()> { - println!("{}", "=== Vestige Portable Export ===".cyan().bold()); - println!(); - - if let Some(parent) = output.parent() - && !parent.exists() - { - std::fs::create_dir_all(parent)?; - } - - let storage = open_storage()?; - let archive = storage.export_portable_archive_to_path(&output)?; - - let file_size = std::fs::metadata(&output)?.len(); - let size_display = if file_size >= 1024 * 1024 { - format!("{:.2} MB", file_size as f64 / (1024.0 * 1024.0)) - } else if file_size >= 1024 { - format!("{:.1} KB", file_size as f64 / 1024.0) - } else { - format!("{} bytes", file_size) - }; - - println!("{}: {}", "Archive".white().bold(), output.display()); - println!("{}: {}", "Format".white().bold(), archive.archive_format); - println!("{}: {}", "Schema".white().bold(), archive.schema_version); - println!("{}: {}", "Tables".white().bold(), archive.tables.len()); - println!("{}: {}", "Rows".white().bold(), archive.total_rows()); - println!(); - println!( - "{}", - format!( - "Portable export complete: {} ({})", - output.display(), - size_display - ) - .green() - .bold() - ); - - Ok(()) -} - -/// Run exact portable archive import. -fn run_portable_import(input: PathBuf, merge: bool) -> anyhow::Result<()> { - println!("{}", "=== Vestige Portable Import ===".cyan().bold()); - println!(); - println!("{}: {}", "Archive".white().bold(), input.display()); - let mode = if merge { - PortableImportMode::Merge - } else { - PortableImportMode::EmptyOnly - }; - println!( - "{}", - if merge { - "Mode: merge into existing database".yellow() - } else { - "Mode: empty database only".yellow() - } - ); - println!(); - - let storage = open_storage()?; - let report = storage.import_portable_archive_from_path(&input, mode)?; - - println!( - "{}: {}", - "Tables imported".white().bold(), - report.tables_imported - ); - println!( - "{}: {}", - "Rows imported".white().bold(), - report.rows_imported - ); - println!( - "{}: {}", - "Tables skipped".white().bold(), - report.tables_skipped - ); - println!("{}: {}", "FTS rebuilt".white().bold(), report.fts_rebuilt); - if merge { - println!( - "{}: {} inserted, {} updated, {} deleted, {} skipped, {} kept local", - "Merge".white().bold(), - report.rows_inserted, - report.rows_updated, - report.rows_deleted, - report.rows_skipped, - report.conflicts_kept_local - ); - } - println!(); - println!("{}", "Portable import complete.".green().bold()); - - Ok(()) -} - -/// Run file-backed two-way sync. -fn run_sync(archive: Option, cloud: bool, endpoint: Option) -> anyhow::Result<()> { - if cloud { - run_sync_cloud(endpoint) - } else { - let archive = archive.ok_or_else(|| { - anyhow::anyhow!( - "no sync target: pass an archive path for file sync, or --cloud for Vestige Cloud" - ) - })?; - run_sync_file(archive) - } -} - -fn run_sync_file(archive: PathBuf) -> anyhow::Result<()> { - println!("{}", "=== Vestige File Sync ===".cyan().bold()); - println!(); - println!("{}: {}", "Archive".white().bold(), archive.display()); - - let storage = open_storage()?; - let report = storage.sync_portable_archive_file(&archive)?; - print_sync_report(&report); - Ok(()) -} - -#[cfg(feature = "cloud-sync")] -fn run_sync_cloud(endpoint: Option) -> anyhow::Result<()> { - let endpoint = endpoint - .or_else(|| std::env::var("VESTIGE_CLOUD_ENDPOINT").ok()) - .filter(|s| !s.trim().is_empty()) - .ok_or_else(|| { - anyhow::anyhow!( - "no cloud endpoint: pass --endpoint or set VESTIGE_CLOUD_ENDPOINT \ - (e.g. https://sync.vestige.dev)" - ) - })?; - let sync_key = std::env::var("VESTIGE_CLOUD_SYNC_KEY") - .ok() - .filter(|s| !s.trim().is_empty()) - .ok_or_else(|| { - anyhow::anyhow!( - "no sync key: set VESTIGE_CLOUD_SYNC_KEY (issued when you subscribe to \ - Vestige Cloud)" - ) - })?; - - // Optional zero-knowledge encryption passphrase. Never sent to the server. - let encryption_key = std::env::var("VESTIGE_CLOUD_ENCRYPTION_KEY") - .ok() - .filter(|s| !s.trim().is_empty()); - - println!("{}", "=== Vestige Cloud Sync ===".cyan().bold()); - println!(); - println!("{}: {}", "Endpoint".white().bold(), endpoint); - if encryption_key.is_some() { - println!( - "{}: {}", - "Encryption".white().bold(), - "zero-knowledge (XChaCha20-Poly1305) — your data is encrypted before upload".green() - ); - } else { - println!( - "{}: {}", - "Encryption".white().bold(), - "OFF — set VESTIGE_CLOUD_ENCRYPTION_KEY for zero-knowledge sync".yellow() - ); - } - - let storage = open_storage()?; - let report = storage.sync_portable_archive_cloud(&endpoint, &sync_key, encryption_key)?; - print_sync_report(&report); - Ok(()) -} - -#[cfg(not(feature = "cloud-sync"))] -fn run_sync_cloud(_endpoint: Option) -> anyhow::Result<()> { - anyhow::bail!( - "this build was compiled without the `cloud-sync` feature; rebuild with \ - --features cloud-sync to use Vestige Cloud" - ) -} - -fn print_sync_report(report: &vestige_core::PortableSyncReport) { - if let Some(pull) = &report.pull { - println!("{}", "Pull: merged remote archive".yellow()); - println!( - " {} inserted, {} updated, {} deleted, {} skipped, {} kept local", - pull.rows_inserted, - pull.rows_updated, - pull.rows_deleted, - pull.rows_skipped, - pull.conflicts_kept_local - ); - } else { - println!( - "{}", - "Pull: archive does not exist yet; creating it".yellow() - ); - } - - println!("{}", "Push: wrote merged local state".yellow()); - println!( - "{}", - format!( - "Sync complete: {} tables, {} rows", - report.pushed_tables, report.pushed_rows - ) - .green() - .bold() - ); -} - /// Run garbage collection command fn run_gc( min_retention: f64, @@ -2413,7 +1151,7 @@ fn run_gc( println!("{}", "=== Vestige Garbage Collection ===".cyan().bold()); println!(); - let storage = open_storage()?; + let storage = Storage::new(None)?; let all_nodes = fetch_all_nodes(&storage)?; let now = Utc::now(); @@ -2468,7 +1206,7 @@ fn run_gc( let age_days = (now - node.created_at).num_days(); println!( " {} [ret={:.3}, age={}d] {}", - node.id.get(..8).unwrap_or(&node.id).dimmed(), + node.id[..8].dimmed(), node.retention_strength, age_days, truncate(&node.content, 60).dimmed() @@ -2529,7 +1267,7 @@ fn run_gc( eprintln!( " {} Failed to delete {}: {}", "ERR".red(), - node.id.get(..8).unwrap_or(&node.id), + &node.id[..8], e ); errors += 1; @@ -2563,7 +1301,6 @@ fn run_ingest( tags: Option, node_type: String, source: Option, - ago_days: Option, ) -> anyhow::Result<()> { if content.trim().is_empty() { anyhow::bail!("Content cannot be empty"); @@ -2588,28 +1325,14 @@ fn run_ingest( tags: tag_list, valid_from: None, valid_until: None, - source_envelope: None, }; - let storage = open_storage()?; + let storage = Storage::new(None)?; // Try smart_ingest (PE Gating) if available, otherwise regular ingest #[cfg(all(feature = "embeddings", feature = "vector-search"))] { let result = storage.smart_ingest(input)?; - if let Some(days) = ago_days { - // Duration::days panics on overflow for extreme inputs; try_days - // returns None instead. The subtraction itself can ALSO overflow the - // DateTime range, so use checked_sub_signed rather than `-` (which - // panics). Both the construction and the subtraction are guarded. - let delta = chrono::Duration::try_days(days).ok_or_else(|| { - anyhow::anyhow!("--ago-days value {days} is out of the supported range") - })?; - let when = chrono::Utc::now().checked_sub_signed(delta).ok_or_else(|| { - anyhow::anyhow!("--ago-days value {days} is out of the supported range") - })?; - storage.set_created_at(&result.node.id, when)?; - } println!("{}", "=== Vestige Ingest ===".cyan().bold()); println!(); println!("{}: {}", "Decision".white().bold(), result.decision.green()); @@ -2620,9 +1343,6 @@ fn run_ingest( if let Some(pe) = result.prediction_error { println!("{}: {:.3}", "Prediction Error".white().bold(), pe); } - if let Some(days) = ago_days { - println!("{}: {} days ago", "Backdated".white().bold(), days); - } println!("{}: {}", "Reason".white().bold(), result.reason); println!(); println!( @@ -2636,19 +1356,6 @@ fn run_ingest( #[cfg(not(all(feature = "embeddings", feature = "vector-search")))] { let node = storage.ingest(input)?; - if let Some(days) = ago_days { - // Duration::days panics on overflow for extreme inputs; try_days - // returns None instead. The subtraction itself can ALSO overflow the - // DateTime range, so use checked_sub_signed rather than `-` (which - // panics). Both the construction and the subtraction are guarded. - let delta = chrono::Duration::try_days(days).ok_or_else(|| { - anyhow::anyhow!("--ago-days value {days} is out of the supported range") - })?; - let when = chrono::Utc::now().checked_sub_signed(delta).ok_or_else(|| { - anyhow::anyhow!("--ago-days value {days} is out of the supported range") - })?; - storage.set_created_at(&node.id, when)?; - } println!("{}", "=== Vestige Ingest ===".cyan().bold()); println!(); println!("{}: create", "Decision".white().bold()); @@ -2665,354 +1372,8 @@ fn run_ingest( Ok(()) } -/// Run Retroactive Salience Backfill from the CLI (the demo's payoff command). -fn run_backfill( - failure_id: Option, - manual: bool, - lookback_days: i64, - promote: bool, - contrast: bool, - json: bool, -) -> anyhow::Result<()> { - let storage = std::sync::Arc::new(open_storage()?); - #[cfg(feature = "embeddings")] - { - let _ = storage.init_embeddings(); - } - - // Resolve the failure text up front (used by the contrast baseline). - // Use the SAME failure detector the backfill tool uses (content + tags, full - // marker list) so the CLI's pick and the tool's pick never diverge. - let failure_text: Option = match &failure_id { - Some(id) => storage.get_node(id).ok().flatten().map(|n| n.content), - None => storage - .get_all_nodes(500, 0) - .ok() - .and_then(|nodes| { - nodes - .into_iter() - .find(vestige_mcp::tools::backfill::looks_like_failure) - }) - .map(|n| n.content), - }; - - // CONTRAST: show what a SIMILARITY SEARCH returns for the failure first — the - // lookalike it ranks at the top, which is NOT the cause. Same store, same - // query. Uses semantic (hybrid) search when embeddings exist, else keyword - // search — either way it ranks by RESEMBLANCE, which is exactly the blind spot. - if contrast - && let Some(ftext) = &failure_text { - // Generic salient-words query: keep alphanumerics, drop a leading - // ":" label if present (e.g. "Service crashed:"). No hardcoding. - let query = match ftext.split_once(": ") { - Some((lead, rest)) if lead.split_whitespace().count() <= 2 => rest, - _ => ftext.as_str(), - }; - - // Track which engine ACTUALLY ran so the label is honest (the audit's - // top finding: never present keyword search as "semantic"). - let mut engine = "keyword (BM25)"; - let mut shown = false; - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - { - if storage.is_embedding_ready() - && let Ok(hits) = storage.hybrid_search(query, 6, 0.3, 0.7) { - let others: Vec<_> = - hits.iter().filter(|h| h.node.content != *ftext).take(3).collect(); - if !others.is_empty() { - engine = "semantic (vector + BM25 hybrid)"; - } - } - } - println!( - "{}", - format!("── 1. SIMILARITY SEARCH · {engine} ──").dimmed().bold() - ); - println!(" query: {}", truncate(query, 60).dimmed()); - - // best OTHER match (exclude the failure itself, which trivially matches). - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - { - if storage.is_embedding_ready() - && let Ok(hits) = storage.hybrid_search(query, 6, 0.3, 0.7) { - let others: Vec<_> = - hits.iter().filter(|h| h.node.content != *ftext).take(3).collect(); - for (i, h) in others.iter().enumerate() { - let tag = if i == 0 { " ← top match".red().bold().to_string() } else { String::new() }; - println!(" {}. {}{}", i + 1, truncate(&h.node.content, 60).normal(), tag); - shown = true; - } - } - } - if !shown { - // keyword/BM25 (always works) — still ranks by lexical resemblance. - if let Ok(hits) = storage.search(query, 6) { - let others: Vec<_> = - hits.iter().filter(|h| h.content != *ftext).take(3).collect(); - for (i, h) in others.iter().enumerate() { - let tag = if i == 0 { " ← top match".red().bold().to_string() } else { String::new() }; - println!(" {}. {}{}", i + 1, truncate(&h.content, 60).normal(), tag); - shown = true; - } - } - } - if shown { - println!( - " {}", - "→ ranked by RESEMBLANCE. its top hit is a lookalike, not the cause.".red() - ); - } else { - println!(" {}", "(no lookalikes — nothing resembles the crash)".dimmed()); - } - println!(); - println!("{}", "── 2. POSTDICT (reach backward for the CAUSE) ──".magenta().bold()); - } - - let args = serde_json::json!({ - "failure_id": failure_id, - "manual": manual, - "lookback_days": lookback_days, - "promote": promote, - }); - - let rt = tokio::runtime::Runtime::new()?; - let result = rt - .block_on(vestige_mcp::tools::backfill::execute(&storage, Some(args))) - .map_err(|e| anyhow::anyhow!(e))?; - - // Machine-readable path: dump the raw tool result (includes per-cause - // memory_id, shared_entities, similarity_rank) and stop. Used by tooling and - // the CauseBench harness so it can score against real engine output. - if json { - println!("{}", serde_json::to_string(&result)?); - return Ok(()); - } - - println!("{}", "=== Retroactive Salience Backfill ===".magenta().bold()); - println!(); - if result["triggered"] != serde_json::json!(true) { - println!( - "{} {}", - "Not triggered:".yellow().bold(), - result["reason"].as_str().unwrap_or("event not salient") - ); - return Ok(()); - } - if let Some(f) = result["failure"].as_object() { - println!( - "{} {}", - "Failure:".red().bold(), - f.get("content_preview").and_then(|v| v.as_str()).unwrap_or("") - ); - } - println!(); - println!("{}", "Reached BACKWARD and surfaced the cause(s) a vector search would miss:".white()); - println!(); - if let Some(causes) = result["causes"].as_array() { - for (i, c) in causes.iter().enumerate() { - let age = c["age_days_before_failure"].as_f64().unwrap_or(0.0); - let rank = c["similarity_rank"].as_u64(); - println!( - " {} {}", - format!("#{}", i + 1).cyan().bold(), - c["content_preview"].as_str().unwrap_or("").green().bold() - ); - println!( - " {} {:.1} days before the failure", - "↩ reached back".magenta(), - age - ); - if let Some(shared) = c["shared_entities"].as_array() { - let ents: Vec<&str> = shared.iter().filter_map(|e| e.as_str()).collect(); - println!(" {} {}", "🔗 causal join:".magenta(), ents.join(", ")); - } - if let Some(r) = rank { - println!( - " {} ranked #{} on similarity {}", - "🔍".magenta(), - r, - "(so semantic search would NOT have surfaced it)".dimmed() - ); - } - if c["promoted"] == serde_json::json!(true) { - println!(" {} promoted — it will resurface next time", "✅".green()); - } - println!(); - } - } - Ok(()) -} - -/// Recall + reason across memories using the real deep_reference engine. -fn run_recall(query: String, depth: i64, json: bool) -> anyhow::Result<()> { - use vestige_mcp::cognitive::CognitiveEngine; - - let storage = open_storage()?; - - #[cfg(feature = "embeddings")] - { - if let Err(e) = storage.init_embeddings() { - eprintln!( - " {} Embeddings unavailable: {} (recall will use keyword-only)", - "!".yellow(), - e - ); - } - } - - let storage = Arc::new(storage); - - let rt = tokio::runtime::Runtime::new()?; - let result = rt.block_on(async move { - let cognitive = Arc::new(tokio::sync::Mutex::new(CognitiveEngine::new())); - { - let mut cog = cognitive.lock().await; - cog.hydrate(&storage); - } - let args = serde_json::json!({ "query": query, "depth": depth }); - vestige_mcp::tools::cross_reference::execute(&storage, &cognitive, Some(args)).await - }); - - let value = result.map_err(|e| anyhow::anyhow!("recall error: {}", e))?; - - if json { - println!("{}", serde_json::to_string_pretty(&value)?); - return Ok(()); - } - - // Human-readable summary of the real engine output. - let conf = value - .get("confidence") - .and_then(|v| v.as_f64()) - .unwrap_or(0.0); - let intent = value - .get("intent") - .and_then(|v| v.as_str()) - .unwrap_or("Synthesis"); - let analyzed = value - .get("memoriesAnalyzed") - .and_then(|v| v.as_i64()) - .unwrap_or(0); - - println!( - "{} intent={} confidence={:.0}% memories_analyzed={}", - "Recall".cyan().bold(), - intent, - conf * 100.0, - analyzed - ); - - if let Some(rec) = value.get("recommended") { - let ans = rec - .get("answer_preview") - .or_else(|| rec.get("preview")) - .and_then(|v| v.as_str()) - .unwrap_or(""); - if !ans.is_empty() { - println!("\n{}", "Recommended:".white().bold()); - for line in ans.lines().take(6) { - println!(" {}", line); - } - } - } - - if let Some(ev) = value.get("evidence").and_then(|v| v.as_array()) { - println!("\n{} ({})", "Evidence".white().bold(), ev.len()); - for (i, e) in ev.iter().take(5).enumerate() { - let pv = e - .get("preview") - .and_then(|v| v.as_str()) - .unwrap_or("") - .replace('\n', " "); - let pv: String = pv.chars().take(78).collect(); - println!(" {}. {}", i + 1, pv); - } - } - - Ok(()) -} - -/// Compose: surface never-composed memory pairs + the testable question they imply. -fn run_compose(limit: i32, tags: Option, json: bool) -> anyhow::Result<()> { - let storage = open_storage()?; - - #[cfg(feature = "embeddings")] - { - let _ = storage.init_embeddings(); - } - - let tag_vec: Option> = tags.map(|t| { - t.split(',') - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()) - .collect() - }); - - let candidates = storage - .get_never_composed_candidates(limit, tag_vec.as_deref()) - .map_err(|e| anyhow::anyhow!("compose error: {}", e))?; - - if json { - let arr: Vec<_> = candidates - .iter() - .map(|c| { - serde_json::json!({ - "score": c.score, - "novelty": c.novelty_score, - "bridge": c.bridge_score, - "trust": c.trust_score, - "a": c.first_preview, - "b": c.second_preview, - "shared_tags": c.shared_tags, - "question": c.composition_question, - "reason": c.reason, - }) - }) - .collect(); - println!("{}", serde_json::to_string_pretty(&arr)?); - return Ok(()); - } - - if candidates.is_empty() { - println!( - "{} no never-composed candidates surfaced (try a wider --limit or remove --tags)", - "Compose".magenta().bold() - ); - return Ok(()); - } - - println!( - "{} {} never-composed insight{} — pairs you wrote that were never connected:\n", - "Compose".magenta().bold(), - candidates.len(), - if candidates.len() == 1 { "" } else { "s" } - ); - - for (i, c) in candidates.iter().enumerate() { - let a: String = c.first_preview.replace('\n', " ").chars().take(70).collect(); - let b: String = c.second_preview.replace('\n', " ").chars().take(70).collect(); - let idx = format!("{}.", i + 1).cyan().bold(); - let metrics = format!( - "{:.2} (novelty {:.2}, bridge {:.2})", - c.score, c.novelty_score, c.bridge_score - ); - println!("{} {} {}", idx, "score".white(), metrics); - println!(" A: {}", a); - println!(" B: {}", b); - let q: String = c.composition_question.replace('\n', " ").chars().take(120).collect(); - if !q.is_empty() { - println!(" {} {}", "?".yellow().bold(), q.yellow()); - } - println!(); - } - - Ok(()) -} - /// Run the dashboard web server fn run_dashboard(port: u16, open_browser: bool) -> anyhow::Result<()> { - use vestige_mcp::cognitive::CognitiveEngine; - println!("{}", "=== Vestige Dashboard ===".cyan().bold()); println!(); println!( @@ -3020,7 +1381,7 @@ fn run_dashboard(port: u16, open_browser: bool) -> anyhow::Result<()> { format!("http://127.0.0.1:{}", port).cyan() ); - let storage = open_storage()?; + let storage = Storage::new(None)?; // Try to initialize embeddings for search support #[cfg(feature = "embeddings")] @@ -3038,14 +1399,7 @@ fn run_dashboard(port: u16, open_browser: bool) -> anyhow::Result<()> { let rt = tokio::runtime::Runtime::new()?; rt.block_on(async move { - // Initialize cognitive engine for dream and other cognitive features - let cognitive = Arc::new(tokio::sync::Mutex::new(CognitiveEngine::new())); - { - let mut cog = cognitive.lock().await; - cog.hydrate(&storage); // Load persisted connections - } - - vestige_mcp::dashboard::start_dashboard(storage, Some(cognitive), port, open_browser) + vestige_mcp::dashboard::start_dashboard(storage, None, port, open_browser) .await .map_err(|e| anyhow::anyhow!("Dashboard error: {}", e)) }) @@ -3058,7 +1412,7 @@ fn run_serve(port: u16, with_dashboard: bool, dashboard_port: u16) -> anyhow::Re println!("{}", "=== Vestige HTTP Server ===".cyan().bold()); println!(); - let storage = open_storage()?; + let storage = Storage::new(None)?; #[cfg(feature = "embeddings")] { @@ -3119,9 +1473,7 @@ fn run_serve(port: u16, with_dashboard: bool, dashboard_port: u16) -> anyhow::Re bind, port ); - if let Ok(path) = vestige_mcp::protocol::auth::token_path() { - println!(" {} Auth token file: {}", ">".cyan(), path.display()); - } + println!(" {} Auth token: {}...", ">".cyan(), &token[..8]); println!(); println!("{}", "Press Ctrl+C to stop.".dimmed()); @@ -3193,48 +1545,4 @@ mod tests { "https://github.com/samvallad33/vestige/releases/download/v2.1.0/vestige-mcp-aarch64-apple-darwin.tar.gz" ); } - - #[test] - fn source_archive_url_uses_normalized_tag() { - assert_eq!(normalize_release_tag("2.1.1"), "v2.1.1"); - assert_eq!(normalize_release_tag("v2.1.1"), "v2.1.1"); - assert_eq!( - source_archive_url("v2.1.1"), - "https://github.com/samvallad33/vestige/archive/refs/tags/v2.1.1.tar.gz" - ); - } - - #[test] - fn scrub_vestige_hooks_removes_only_vestige_commands() { - let mut settings = serde_json::json!({ - "hooks": { - "UserPromptSubmit": [ - { - "hooks": [ - { "type": "command", "command": "/tmp/synthesis-preflight.sh" }, - { "type": "command", "command": "/tmp/custom-user-hook.sh" } - ] - } - ], - "Stop": [ - { - "hooks": [ - { "type": "command", "command": "/tmp/sanhedrin.sh" } - ] - } - ] - }, - "other": true - }); - - scrub_vestige_hooks(&mut settings); - - let user_hooks = settings["hooks"]["UserPromptSubmit"][0]["hooks"] - .as_array() - .unwrap(); - assert_eq!(user_hooks.len(), 1); - assert_eq!(user_hooks[0]["command"], "/tmp/custom-user-hook.sh"); - assert!(settings["hooks"].get("Stop").is_none()); - assert_eq!(settings["other"], true); - } } diff --git a/crates/vestige-mcp/src/bin/restore.rs b/crates/vestige-mcp/src/bin/restore.rs index 68aa990..6e24800 100644 --- a/crates/vestige-mcp/src/bin/restore.rs +++ b/crates/vestige-mcp/src/bin/restore.rs @@ -26,20 +26,7 @@ fn main() -> anyhow::Result<()> { // Parse args let args: Vec = std::env::args().collect(); if args.len() < 2 { - print_usage_stderr(); - std::process::exit(1); - } - if matches!(args[1].as_str(), "-h" | "--help") { - print_usage_stdout(); - return Ok(()); - } - if matches!(args[1].as_str(), "-V" | "--version") { - println!("vestige-restore {}", env!("CARGO_PKG_VERSION")); - return Ok(()); - } - if args.len() > 2 { - eprintln!("Unexpected extra argument: {}", args[2]); - print_usage_stderr(); + eprintln!("Usage: vestige-restore "); std::process::exit(1); } @@ -49,11 +36,7 @@ fn main() -> anyhow::Result<()> { // Read and parse backup let backup_content = std::fs::read_to_string(&backup_path)?; let wrapper: Vec = serde_json::from_str(&backup_content)?; - // Guard the index: an empty backup array would panic on wrapper[0]. - let first = wrapper - .first() - .ok_or_else(|| anyhow::anyhow!("backup wrapper array is empty — nothing to restore"))?; - let recall_result: RecallResult = serde_json::from_str(&first.text)?; + let recall_result: RecallResult = serde_json::from_str(&wrapper[0].text)?; let memories = recall_result.results; println!("Found {} memories to restore", memories.len()); @@ -77,7 +60,6 @@ fn main() -> anyhow::Result<()> { tags: memory.tags.unwrap_or_default(), valid_from: None, valid_until: None, - source_envelope: None, }; match storage.ingest(input) { @@ -109,18 +91,6 @@ fn main() -> anyhow::Result<()> { Ok(()) } -fn print_usage_stdout() { - println!("{}", usage()); -} - -fn print_usage_stderr() { - eprintln!("{}", usage()); -} - -fn usage() -> &'static str { - "Vestige Restore\n\nUSAGE:\n vestige-restore \n\nOPTIONS:\n -h, --help Print help information\n -V, --version Print version information" -} - /// Truncate a string for display (UTF-8 safe) fn truncate(s: &str, max_chars: usize) -> String { let s = s.replace('\n', " "); diff --git a/crates/vestige-mcp/src/cognitive.rs b/crates/vestige-mcp/src/cognitive.rs index fc73f97..86aadcf 100644 --- a/crates/vestige-mcp/src/cognitive.rs +++ b/crates/vestige-mcp/src/cognitive.rs @@ -6,7 +6,6 @@ use vestige_core::neuroscience::predictive_retrieval::PredictiveMemory; use vestige_core::neuroscience::prospective_memory::{IntentionParser, ProspectiveMemory}; -#[cfg(feature = "vector-search")] use vestige_core::search::TemporalSearcher; use vestige_core::{ AccessibilityCalculator, @@ -32,6 +31,9 @@ use vestige_core::{ MemoryDreamer, NoveltySignal, ReconsolidationManager, + // Search modules + Reranker, + RerankerConfig, RewardSignal, SpeculativeRetriever, StateUpdateService, @@ -39,8 +41,6 @@ use vestige_core::{ Storage, SynapticTaggingSystem, }; -#[cfg(feature = "vector-search")] -use vestige_core::{Reranker, RerankerConfig}; /// Stateful cognitive engine holding all neuroscience modules. /// @@ -80,9 +80,7 @@ pub struct CognitiveEngine { pub consolidation_scheduler: ConsolidationScheduler, // -- Search -- - #[cfg(feature = "vector-search")] pub reranker: Reranker, - #[cfg(feature = "vector-search")] pub temporal_searcher: TemporalSearcher, } @@ -163,9 +161,7 @@ impl CognitiveEngine { consolidation_scheduler: ConsolidationScheduler::new(), // Search - #[cfg(feature = "vector-search")] reranker: Reranker::new(RerankerConfig::default()), - #[cfg(feature = "vector-search")] temporal_searcher: TemporalSearcher::new(), } } @@ -195,7 +191,6 @@ mod tests { tags: vec!["test".to_string()], valid_from: None, valid_until: None, - source_envelope: None, }) .unwrap(); result.id diff --git a/crates/vestige-mcp/src/dashboard/events.rs b/crates/vestige-mcp/src/dashboard/events.rs index 4d5c8d4..a6807e2 100644 --- a/crates/vestige-mcp/src/dashboard/events.rs +++ b/crates/vestige-mcp/src/dashboard/events.rs @@ -85,16 +85,6 @@ pub enum VestigeEvent { timestamp: DateTime, }, - // -- Hook verdicts -- - HookVerdictRecorded { - hook: String, - verdict: String, - phase: String, - reason: String, - receipt_id: Option, - timestamp: DateTime, - }, - // -- Dream -- DreamStarted { memory_count: usize, @@ -167,39 +157,6 @@ pub enum VestigeEvent { timestamp: DateTime, }, - // -- Agent Black Box (v2.2) -- - // One replayable trace event from an agent run. The dashboard Black Box tab - // appends these to the live timeline and pulses the graph exactly as the - // agent experienced it. The inner event is the canonical - // `vestige_core::MemoryTraceEvent`, serialized with its own `type` tag, so - // the wire shape is `{ "type": "TraceEvent", "data": { "runId": ..., "event": { "type": "mcp.call", ... } } }`. - TraceEvent { - run_id: String, - seq: i64, - event: vestige_core::MemoryTraceEvent, - timestamp: DateTime, - }, - - // -- Memory PRs (v2.2) — the cognitive immune system -- - // A risky write opened a Memory PR. The dashboard raises the PR-queue badge - // and can surface a toast: "Vestige opened a Memory PR — the agent tried to - // rewrite its own brain." - MemoryPrOpened { - id: String, - kind: String, - title: String, - signal_count: usize, - run_id: Option, - timestamp: DateTime, - }, - // A Memory PR was decided (promote / merge / supersede / quarantine / forget). - MemoryPrDecided { - id: String, - decision: String, - status: String, - timestamp: DateTime, - }, - // -- System -- Heartbeat { uptime_secs: u64, diff --git a/crates/vestige-mcp/src/dashboard/handlers.rs b/crates/vestige-mcp/src/dashboard/handlers.rs index 263df75..99a278f 100644 --- a/crates/vestige-mcp/src/dashboard/handlers.rs +++ b/crates/vestige-mcp/src/dashboard/handlers.rs @@ -3,10 +3,6 @@ //! v2.0: Adds cognitive operation endpoints (dream, explore, predict, importance, consolidation) use std::cmp::Reverse; -use std::collections::{BTreeMap, HashSet}; -use std::fs::{self, OpenOptions}; -use std::io::{BufRead, BufReader, Write}; -use std::path::{Path as FsPath, PathBuf}; use axum::extract::{Path, Query, State}; use axum::http::StatusCode; @@ -58,22 +54,6 @@ pub async fn list_memories( true } }) - // Honor node_type/tag filters on the search path too. Previously - // these were applied only in the no-query branch, so a filtered - // search (?q=foo&node_type=decision&tag=x) silently returned - // non-matching rows. - .filter(|r| { - params - .node_type - .as_ref() - .is_none_or(|nt| &r.node.node_type == nt) - }) - .filter(|r| { - params - .tag - .as_ref() - .is_none_or(|tag| r.node.tags.iter().any(|t| t == tag)) - }) .map(|r| { serde_json::json!({ "id": r.node.id, @@ -362,561 +342,6 @@ pub async fn unsuppress_memory( }))) } -#[derive(Debug, Deserialize)] -pub struct SanhedrinAppealRequest { - pub reason: String, - pub note: Option, - #[serde(rename = "receiptId")] - pub receipt_id: Option, - #[serde(rename = "claimId")] - pub claim_id: Option, -} - -#[derive(Debug, Deserialize)] -pub struct SanhedrinTelemetryParams { - pub days: Option, -} - -/// Return the latest Sanhedrin receipt written by the Stop-hook bridge. -pub async fn get_sanhedrin_latest() -> Result, StatusCode> { - let state_dir = sanhedrin_state_dir(); - let latest_path = state_dir.join("latest.json"); - if !latest_path.exists() { - return Ok(Json(serde_json::json!({ - "receipt": null, - "stateDir": state_dir, - }))); - } - - let raw = fs::read_to_string(&latest_path).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - let receipt: Value = - serde_json::from_str(&raw).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - let schema_warning = sanhedrin_schema_warning(&receipt); - - Ok(Json(serde_json::json!({ - "receipt": receipt, - "stateDir": state_dir, - "receiptPath": latest_path, - "htmlPath": state_dir.join("latest.html"), - "schemaWarning": schema_warning, - }))) -} - -/// Return rolling Sanhedrin receipts, appeals, and fail-open counters. -pub async fn get_sanhedrin_telemetry( - Query(params): Query, -) -> Result, StatusCode> { - let state_dir = sanhedrin_state_dir(); - let days = params.days.unwrap_or(7).clamp(1, 90); - let telemetry = tokio::task::spawn_blocking(move || build_sanhedrin_telemetry(state_dir, days)) - .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)??; - Ok(Json(telemetry)) -} - -fn build_sanhedrin_telemetry(state_dir: PathBuf, days: i64) -> Result { - let cutoff = Utc::now() - Duration::days(days); - let receipts_dir = state_dir.join("receipts"); - let mut by_verdict = serde_json::Map::new(); - for verdict in ["PASS", "NOTE", "CAUTION", "VETO", "APPEALED"] { - by_verdict.insert(verdict.to_string(), Value::from(0)); - } - let mut by_class: BTreeMap = BTreeMap::new(); - let mut daily: BTreeMap> = BTreeMap::new(); - let mut total_runs = 0i64; - let mut last_run_at: Option> = None; - let mut truncated = false; - - if let Ok(entries) = bounded_receipt_entries(&receipts_dir, cutoff) { - for path in entries { - let Ok(raw) = fs::read_to_string(&path) else { - continue; - }; - let Ok(receipt) = serde_json::from_str::(&raw) else { - continue; - }; - let Some(created_at) = parse_sanhedrin_timestamp(&receipt["createdAt"]) else { - continue; - }; - if created_at < cutoff { - continue; - } - total_runs += 1; - if last_run_at.map(|last| created_at > last).unwrap_or(true) { - last_run_at = Some(created_at); - } - let verdict = receipt - .get("verdictBar") - .and_then(Value::as_str) - .unwrap_or("NOTE") - .to_ascii_uppercase(); - increment_json_counter(&mut by_verdict, &verdict); - let day = created_at.date_naive().to_string(); - let bucket = daily.entry(day).or_insert_with(|| { - let mut map = serde_json::Map::new(); - map.insert("date".to_string(), Value::String(String::new())); - map.insert("total".to_string(), Value::from(0)); - map.insert("pass".to_string(), Value::from(0)); - map.insert("note".to_string(), Value::from(0)); - map.insert("caution".to_string(), Value::from(0)); - map.insert("veto".to_string(), Value::from(0)); - map.insert("appealed".to_string(), Value::from(0)); - map.insert("failOpen".to_string(), Value::from(0)); - map - }); - increment_json_counter(bucket, "total"); - match verdict.as_str() { - "PASS" => increment_json_counter(bucket, "pass"), - "NOTE" => increment_json_counter(bucket, "note"), - "CAUTION" => increment_json_counter(bucket, "caution"), - "VETO" => increment_json_counter(bucket, "veto"), - "APPEALED" => increment_json_counter(bucket, "appealed"), - _ => increment_json_counter(bucket, "note"), - } - - if let Some(claims) = receipt.get("claims").and_then(Value::as_array) { - for claim in claims { - let class = known_sanhedrin_class( - claim - .get("class") - .and_then(Value::as_str) - .unwrap_or("UNKNOWN"), - ); - *by_class.entry(class).or_insert(0) += 1; - } - } - } - truncated = total_runs >= 5_000; - } - - let appeals = count_jsonl_since(&state_dir.join("appeals.jsonl"), cutoff, false); - let fail_open = count_jsonl_since(&state_dir.join("fail-open.jsonl"), cutoff, true); - for (date, bucket) in daily.iter_mut() { - bucket.insert("date".to_string(), Value::String(date.clone())); - } - - Ok(serde_json::json!({ - "days": days, - "stateDir": state_dir, - "totalRuns": total_runs, - "byVerdict": by_verdict, - "byClass": by_class, - "appeals": appeals, - "failOpen": fail_open, - "truncated": truncated, - "lastRunAt": last_run_at.map(|dt| dt.to_rfc3339()), - "daily": daily.into_values().map(Value::Object).collect::>(), - })) -} - -/// Record feedback that a Sanhedrin veto was stale, wrong, or too strict. -/// -/// This intentionally does not promote, demote, suppress, edit, or delete any -/// memory. The hook reads this ledger and suppresses future same-fingerprint -/// vetoes, which keeps appeal training scoped to Sanhedrin behavior. -pub async fn appeal_sanhedrin( - State(state): State, - Json(req): Json, -) -> Result, StatusCode> { - let reason = req.reason.trim().to_ascii_lowercase(); - if !matches!(reason.as_str(), "stale" | "wrong" | "too_strict") { - return Err(StatusCode::BAD_REQUEST); - } - - let state_dir = sanhedrin_state_dir(); - let latest_path = state_dir.join("latest.json"); - let raw = match fs::read_to_string(&latest_path) { - Ok(raw) => raw, - Err(err) if err.kind() == std::io::ErrorKind::NotFound => { - return Err(StatusCode::NOT_FOUND); - } - Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR), - }; - let mut receipt: Value = serde_json::from_str(&raw).map_err(|_| StatusCode::BAD_REQUEST)?; - let original_receipt = receipt.clone(); - let note = req.note.unwrap_or_default(); - let receipt_id = receipt - .get("id") - .and_then(Value::as_str) - .map(ToOwned::to_owned); - let receipt_id_ref = receipt_id.as_deref().ok_or(StatusCode::BAD_REQUEST)?; - let _ = sanitize_receipt_id(receipt_id_ref)?; - let expected_receipt_id = req.receipt_id.as_deref().ok_or(StatusCode::BAD_REQUEST)?; - if expected_receipt_id != receipt_id_ref { - return Err(StatusCode::CONFLICT); - } - if receipt - .get("verdictBar") - .and_then(Value::as_str) - .map(|v| v != "VETO") - .unwrap_or(true) - { - return Err(StatusCode::CONFLICT); - } - let claim = mark_sanhedrin_claim(&mut receipt, &reason, ¬e, req.claim_id.as_deref())?; - - let appeal = serde_json::json!({ - "timestamp": Utc::now().to_rfc3339(), - "receiptId": receipt_id.as_deref(), - "claimId": claim.get("id").and_then(Value::as_str), - "claimFingerprint": claim.get("fingerprint").and_then(Value::as_str), - "claim": claim.get("text").and_then(Value::as_str), - "reason": &reason, - "note": ¬e, - "status": "active", - }); - - set_json_field(&mut receipt, "overall", "appealed"); - set_json_field(&mut receipt, "verdictBar", "APPEALED"); - set_json_field(&mut receipt, "summary", &format!("Appealed as {}.", reason)); - save_sanhedrin_receipt(&state_dir, &receipt)?; - if let Err(err) = append_sanhedrin_appeal(&state_dir, &appeal) { - let _ = save_sanhedrin_receipt(&state_dir, &original_receipt); - return Err(err); - } - - state.emit(VestigeEvent::HookVerdictRecorded { - hook: "sanhedrin".to_string(), - verdict: "APPEALED".to_string(), - phase: "appeal".to_string(), - reason: reason.clone(), - receipt_id: receipt_id.clone(), - timestamp: Utc::now(), - }); - - Ok(Json(serde_json::json!({ - "appeal": appeal, - "receipt": receipt, - }))) -} - -fn sanhedrin_state_dir() -> PathBuf { - std::env::var_os("VESTIGE_SANHEDRIN_STATE_DIR") - .map(PathBuf::from) - .or_else(|| { - std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".vestige/sanhedrin")) - }) - .unwrap_or_else(|| PathBuf::from(".vestige/sanhedrin")) -} - -fn sanhedrin_schema_warning(receipt: &Value) -> Option { - let schema = receipt.get("schema").and_then(Value::as_str).unwrap_or(""); - if schema.is_empty() || schema == "vestige.sanhedrin.receipt.v1" { - None - } else { - Some(format!( - "Unsupported Sanhedrin receipt schema '{}'; dashboard expects vestige.sanhedrin.receipt.v1", - schema - )) - } -} - -fn parse_sanhedrin_timestamp(value: &Value) -> Option> { - let raw = value.as_str()?; - DateTime::parse_from_rfc3339(raw) - .map(|dt| dt.with_timezone(&Utc)) - .ok() -} - -fn bounded_receipt_entries( - receipts_dir: &FsPath, - cutoff: DateTime, -) -> Result, StatusCode> { - const MAX_RECEIPTS: usize = 5_000; - const MAX_RECEIPT_BYTES: u64 = 256 * 1024; - - let mut entries: Vec<(Option>, PathBuf)> = Vec::new(); - let Ok(read_dir) = fs::read_dir(receipts_dir) else { - return Ok(Vec::new()); - }; - - for entry in read_dir.flatten() { - let path = entry.path(); - if path.extension().and_then(|ext| ext.to_str()) != Some("json") { - continue; - } - let Ok(metadata) = fs::symlink_metadata(&path) else { - continue; - }; - if metadata.file_type().is_symlink() - || !metadata.is_file() - || metadata.len() > MAX_RECEIPT_BYTES - { - continue; - } - let modified = metadata.modified().ok().map(DateTime::::from); - if modified.map(|mtime| mtime < cutoff).unwrap_or(false) { - continue; - } - entries.push((modified, path)); - } - - entries.sort_by(|(left_time, _), (right_time, _)| right_time.cmp(left_time)); - Ok(entries - .into_iter() - .take(MAX_RECEIPTS) - .map(|(_, path)| path) - .collect()) -} - -fn increment_json_counter(map: &mut serde_json::Map, key: &str) { - let current = map.get(key).and_then(Value::as_i64).unwrap_or(0); - map.insert(key.to_string(), Value::from(current + 1)); -} - -fn count_jsonl_since(path: &FsPath, cutoff: DateTime, distinct_run: bool) -> i64 { - const MAX_LEDGER_BYTES: u64 = 2 * 1024 * 1024; - let Ok(metadata) = fs::symlink_metadata(path) else { - return 0; - }; - if metadata.file_type().is_symlink() || !metadata.is_file() || metadata.len() > MAX_LEDGER_BYTES - { - return 0; - } - let Ok(file) = fs::File::open(path) else { - return 0; - }; - let mut seen_runs = HashSet::new(); - let mut count = 0i64; - for line in BufReader::new(file).lines().map_while(Result::ok) { - let Ok(item) = serde_json::from_str::(&line) else { - continue; - }; - let Some(timestamp) = parse_sanhedrin_timestamp(&item["timestamp"]) else { - continue; - }; - if timestamp < cutoff { - continue; - } - if distinct_run - && let Some(run_id) = item.get("runId").and_then(Value::as_str) - && !seen_runs.insert(run_id.to_string()) - { - continue; - } - count += 1; - } - count -} - -fn known_sanhedrin_class(class: &str) -> String { - match class { - "receipt_lock" - | "TECHNICAL" - | "BIOGRAPHICAL" - | "FINANCIAL" - | "ACHIEVEMENT" - | "TIMELINE" - | "QUANTITATIVE" - | "ATTRIBUTION" - | "CAUSAL" - | "COMPARATIVE" - | "EXISTENTIAL" - | "VAGUE-QUANTIFIER" - | "UNVERIFIED-POSITIVE" => class.to_string(), - _ => "OTHER".to_string(), - } -} - -fn ensure_sanhedrin_dirs(state_dir: &FsPath) -> Result<(), StatusCode> { - fs::create_dir_all(state_dir.join("receipts")).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR) -} - -fn mark_sanhedrin_claim( - receipt: &mut Value, - reason: &str, - note: &str, - claim_id: Option<&str>, -) -> Result { - let claim_id = claim_id.ok_or(StatusCode::BAD_REQUEST)?; - let claims = receipt - .get_mut("claims") - .and_then(Value::as_array_mut) - .ok_or(StatusCode::BAD_REQUEST)?; - - if claims.is_empty() { - return Err(StatusCode::BAD_REQUEST); - } - - let selected = claims - .iter() - .position(|claim| claim.get("id").and_then(Value::as_str) == Some(claim_id)) - .ok_or(StatusCode::NOT_FOUND)?; - - if claims - .get(selected) - .and_then(|claim| claim.get("decision")) - .and_then(Value::as_str) - != Some("veto") - { - return Err(StatusCode::CONFLICT); - } - - let claim = claims - .get_mut(selected) - .and_then(Value::as_object_mut) - .ok_or(StatusCode::BAD_REQUEST)?; - - claim.insert( - "decision".to_string(), - Value::String("appealed".to_string()), - ); - claim.insert( - "evidence_state".to_string(), - Value::String("appealed".to_string()), - ); - claim.insert( - "appeal".to_string(), - serde_json::json!({ - "status": "appealed", - "lastReason": reason, - "note": note, - "actions": ["stale", "wrong", "too_strict"], - }), - ); - - Ok(Value::Object(claim.clone())) -} - -fn set_json_field(receipt: &mut Value, key: &str, value: &str) { - if let Some(obj) = receipt.as_object_mut() { - obj.insert(key.to_string(), Value::String(value.to_string())); - } -} - -fn save_sanhedrin_receipt(state_dir: &FsPath, receipt: &Value) -> Result<(), StatusCode> { - ensure_sanhedrin_dirs(state_dir)?; - let rendered = render_sanhedrin_receipt_html(receipt); - let pretty = serde_json::to_string_pretty(receipt).map_err(|_| StatusCode::BAD_REQUEST)?; - let safe_id = receipt - .get("id") - .and_then(Value::as_str) - .map(sanitize_receipt_id) - .transpose()?; - - if let Some(safe_id) = safe_id { - write_atomic( - &state_dir.join("receipts").join(format!("{}.json", safe_id)), - pretty.as_bytes(), - )?; - write_atomic( - &state_dir.join("receipts").join(format!("{}.html", safe_id)), - rendered.as_bytes(), - )?; - } - - write_atomic(&state_dir.join("latest.json"), pretty.as_bytes())?; - write_atomic(&state_dir.join("latest.html"), rendered.as_bytes())?; - Ok(()) -} - -fn append_sanhedrin_appeal(state_dir: &FsPath, appeal: &Value) -> Result<(), StatusCode> { - ensure_sanhedrin_dirs(state_dir)?; - let mut appeals = OpenOptions::new() - .create(true) - .append(true) - .open(state_dir.join("appeals.jsonl")) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - writeln!(appeals, "{}", appeal).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR) -} - -fn sanitize_receipt_id(id: &str) -> Result<&str, StatusCode> { - if !id.is_empty() - && id - .bytes() - .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-')) - { - Ok(id) - } else { - Err(StatusCode::BAD_REQUEST) - } -} - -fn write_atomic(path: &FsPath, bytes: &[u8]) -> Result<(), StatusCode> { - let parent = path.parent().ok_or(StatusCode::INTERNAL_SERVER_ERROR)?; - fs::create_dir_all(parent).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - let tmp = path.with_extension(format!( - "{}.tmp", - Utc::now().timestamp_nanos_opt().unwrap_or_default() - )); - fs::write(&tmp, bytes).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - fs::rename(&tmp, path).map_err(|_| { - let _ = fs::remove_file(&tmp); - StatusCode::INTERNAL_SERVER_ERROR - }) -} - -fn render_sanhedrin_receipt_html(receipt: &Value) -> String { - let verdict = escape_html( - receipt - .get("verdictBar") - .and_then(Value::as_str) - .unwrap_or("PASS"), - ); - let summary = escape_html(receipt.get("summary").and_then(Value::as_str).unwrap_or("")); - let mut claims_html = String::new(); - - if let Some(claims) = receipt.get("claims").and_then(Value::as_array) { - for claim in claims { - let text = escape_html(claim.get("text").and_then(Value::as_str).unwrap_or("")); - let decision = escape_html(claim.get("decision").and_then(Value::as_str).unwrap_or("")); - let evidence_state = escape_html( - claim - .get("evidence_state") - .and_then(Value::as_str) - .unwrap_or(""), - ); - let fix = escape_html( - claim - .get("fix") - .and_then(Value::as_str) - .filter(|s| !s.is_empty()) - .unwrap_or("No change required."), - ); - let mut precedents = String::new(); - if let Some(items) = claim.get("precedent").and_then(Value::as_array) { - for item in items { - let summary = item - .get("summary") - .and_then(Value::as_str) - .unwrap_or("Precedent recorded."); - precedents.push_str(&format!("
                  1. {}
                  2. ", escape_html(summary))); - } - } - claims_html.push_str(&format!( - "
                    {} / {}

                    {}

                    Fix: {}

                    Appeal: stale | wrong | too_strict

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

                    Veto Receipt

                    {}

                    {} -"#, - verdict, summary, claims_html - ) -} - -fn escape_html(value: &str) -> String { - value - .replace('&', "&") - .replace('<', "<") - .replace('>', ">") - .replace('"', """) - .replace('\'', "'") -} - /// Get system stats pub async fn get_stats(State(state): State) -> Result, StatusCode> { let stats = state @@ -1041,12 +466,10 @@ pub async fn get_changelog( } // Connections are currently persisted as graph edges rather than as audit - // rows, so filter by created_at from the connection table. Fetch only the - // most recent `fetch_limit` connections (not the entire table) — this - // endpoint is polled once per wake and must stay cheap on a large graph. + // rows, so filter by created_at from the connection table. let connections = state .storage - .get_recent_connections(fetch_limit as usize) + .get_all_connections() .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; for conn in connections { if changelog_window_contains(conn.created_at, start.as_ref(), end.as_ref()) { @@ -1435,14 +858,23 @@ pub async fn trigger_dream(State(state): State) -> Result, // Run dream through CognitiveEngine let cog = cognitive.lock().await; - let (dream_result, new_connections) = cog.dreamer.dream_with_connections(&dream_memories).await; + // Capture start time before the dream — composite-score eviction in store_connections + // reorders the buffer, making positional slicing (pre_dream_count..) unreliable. + let dream_start = Utc::now(); + let dream_result = cog.dreamer.dream(&dream_memories).await; let insights = cog.dreamer.synthesize_insights(&dream_memories); + let all_connections = cog.dreamer.get_connections(); drop(cog); // Persist new connections + // Filter by timestamp — same approach as dream.rs to avoid positional index issues. + let new_connections: Vec<&vestige_core::DiscoveredConnection> = all_connections + .iter() + .filter(|c| c.discovered_at >= dream_start) + .collect(); let mut connections_persisted = 0u64; let now = Utc::now(); - for conn in &new_connections { + for conn in new_connections.iter() { let link_type = match conn.connection_type { vestige_core::DiscoveredConnectionType::Semantic => "semantic", vestige_core::DiscoveredConnectionType::SharedConcept => "shared_concepts", @@ -2001,869 +1433,6 @@ pub async fn deep_reference_query( Ok(Json(response)) } -// ============================================================================ -// AGENT BLACK BOX (v2.2) — replayable agent-run traces -// ============================================================================ - -#[derive(Debug, Deserialize)] -pub struct TraceListParams { - pub limit: Option, - /// Optional run filter — receipts/traces scoped to one run (B5). - pub run: Option, -} - -/// List recent agent runs (newest activity first) for the Black Box run picker. -pub async fn list_traces( - State(state): State, - Query(params): Query, -) -> Result, StatusCode> { - let limit = params.limit.unwrap_or(50).clamp(1, 500); - let runs = state - .storage - .list_agent_runs(limit) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - let runs_json: Vec = runs - .into_iter() - .map(|r| { - serde_json::json!({ - "runId": r.run_id, - "firstTool": r.first_tool, - "eventCount": r.event_count, - "retrievedCount": r.retrieved_count, - "suppressedCount": r.suppressed_count, - "writeCount": r.write_count, - "vetoCount": r.veto_count, - "startedAt": r.started_at, - "lastAt": r.last_at, - }) - }) - .collect(); - Ok(Json(serde_json::json!({ - "total": runs_json.len(), - "runs": runs_json, - }))) -} - -/// Fetch the full event timeline for one run — the black-box replay payload. -pub async fn get_trace( - State(state): State, - Path(run_id): Path, -) -> Result, StatusCode> { - let events = state - .storage - .get_trace(&run_id) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - if events.is_empty() { - return Err(StatusCode::NOT_FOUND); - } - let summary = state.storage.get_agent_run(&run_id).ok().flatten(); - Ok(Json(serde_json::json!({ - "runId": run_id, - "summary": summary.map(|s| serde_json::json!({ - "firstTool": s.first_tool, - "eventCount": s.event_count, - "retrievedCount": s.retrieved_count, - "suppressedCount": s.suppressed_count, - "writeCount": s.write_count, - "vetoCount": s.veto_count, - "startedAt": s.started_at, - "lastAt": s.last_at, - })), - "events": events, - }))) -} - -/// Export a run as a downloadable `.vestige-trace.json` artifact. -pub async fn export_trace( - State(state): State, - Path(run_id): Path, -) -> Result<([(axum::http::HeaderName, String); 2], Json), StatusCode> { - let events = state - .storage - .get_trace(&run_id) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - if events.is_empty() { - return Err(StatusCode::NOT_FOUND); - } - let summary = state.storage.get_agent_run(&run_id).ok().flatten(); - let body = serde_json::json!({ - "format": "vestige-trace", - "version": 1, - "runId": run_id, - "exportedAt": Utc::now().to_rfc3339(), - "summary": summary.map(|s| serde_json::json!({ - "firstTool": s.first_tool, - "eventCount": s.event_count, - "retrievedCount": s.retrieved_count, - "suppressedCount": s.suppressed_count, - "writeCount": s.write_count, - "vetoCount": s.veto_count, - "startedAt": s.started_at, - "lastAt": s.last_at, - })), - "events": events, - }); - // B7: sanitize the run_id before putting it in the download filename so a - // crafted run_id (quotes, path separators, control chars) can't break the - // Content-Disposition header or the filename. Falls back to "trace". - let safe: String = run_id - .chars() - .map(|c| if c.is_ascii_alphanumeric() || c == '_' || c == '-' { c } else { '_' }) - .collect(); - let safe = if safe.trim_matches('_').is_empty() { - "trace".to_string() - } else { - safe - }; - let headers = [ - ( - axum::http::header::CONTENT_TYPE, - "application/json".to_string(), - ), - ( - axum::http::header::CONTENT_DISPOSITION, - format!("attachment; filename=\"{safe}.vestige-trace.json\""), - ), - ]; - Ok((headers, Json(body))) -} - -// ============================================================================ -// MEMORY RECEIPTS (v2.2) -// ============================================================================ - -/// List recent retrieval receipts. -pub async fn list_receipts( - State(state): State, - Query(params): Query, -) -> Result, StatusCode> { - let limit = params.limit.unwrap_or(50).clamp(1, 500); - // B5: when a run is given, scope to that run's receipts so the Black Box - // panel shows only receipts that actually belong to the selected run. - let receipts = match params.run.as_deref().filter(|r| !r.is_empty()) { - Some(run_id) => state.storage.list_receipts_for_run(run_id, limit), - None => state.storage.list_receipts(limit), - } - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - Ok(Json(serde_json::json!({ - "total": receipts.len(), - "receipts": receipts, - }))) -} - -/// Fetch one receipt by id — the payload behind "Open receipt in Cinema". -pub async fn get_receipt( - State(state): State, - Path(receipt_id): Path, -) -> Result, StatusCode> { - let receipt = state - .storage - .get_receipt(&receipt_id) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? - .ok_or(StatusCode::NOT_FOUND)?; - Ok(Json(serde_json::to_value(receipt).unwrap_or_default())) -} - -// ============================================================================ -// MEMORY PRs (v2.2) — risk-gated brain-change review queue -// ============================================================================ - -#[derive(Debug, Deserialize)] -pub struct MemoryPrListParams { - pub status: Option, - pub limit: Option, -} - -/// List Memory PRs, optionally filtered by status. -pub async fn list_memory_prs( - State(state): State, - Query(params): Query, -) -> Result, StatusCode> { - let limit = params.limit.unwrap_or(100).clamp(1, 500); - let status = params.status.as_deref().and_then(|s| { - serde_json::from_value::(serde_json::Value::String( - s.to_string(), - )) - .ok() - }); - let prs = state - .storage - .list_memory_prs(status, limit) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - let pending = state.storage.count_pending_memory_prs().unwrap_or(0); - Ok(Json(serde_json::json!({ - "total": prs.len(), - "pendingCount": pending, - "mode": read_review_mode(&state).as_str(), - "prs": prs, - }))) -} - -/// Fetch one Memory PR by id. -pub async fn get_memory_pr( - State(state): State, - Path(id): Path, -) -> Result, StatusCode> { - let pr = state - .storage - .get_memory_pr(&id) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? - .ok_or(StatusCode::NOT_FOUND)?; - Ok(Json(serde_json::to_value(pr).unwrap_or_default())) -} - -/// Act on a Memory PR: promote / merge / supersede / quarantine / forget / -/// ask_agent_why. `ask_agent_why` is read-only and returns the risk signals. -pub async fn act_on_memory_pr( - State(state): State, - Path((id, action)): Path<(String, String)>, -) -> Result, StatusCode> { - let action = vestige_core::MemoryPrAction::from_label(&action) - .ok_or(StatusCode::BAD_REQUEST)?; - - // Ask Agent Why is read-only — return the self-explaining signals. - if matches!(action, vestige_core::MemoryPrAction::AskAgentWhy) { - let pr = state - .storage - .get_memory_pr(&id) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? - .ok_or(StatusCode::NOT_FOUND)?; - return Ok(Json(serde_json::json!({ - "id": pr.id, - "kind": pr.kind.as_str(), - "title": pr.title, - "why": pr.signals, - "explanation": "These are the risk signals that opened this Memory PR.", - }))); - } - - let decided = state - .storage - .decide_memory_pr(&id, action) - .map_err(|_| StatusCode::NOT_FOUND)?; - - // B1: an accept action (promote/merge/supersede) must RELEASE the subject - // memory from quarantine — gate_writes suppressed it, so deciding the PR - // without un-suppressing would leave it "promoted" yet still held out of - // retrieval. Forget/Quarantine intentionally keep it suppressed. - let mut released = false; - if action.releases_memory() - && let Some(subject_id) = decided.subject_id.as_deref() - { - // Use the UNCONDITIONAL quarantine release, not reverse_suppression: - // approving a PR must restore the memory even if reviewed days later, - // past the active-forgetting labile window (the C1 fix). - match state.storage.release_quarantine(subject_id) { - Ok(node) => { - released = true; - state.emit(VestigeEvent::MemoryUnsuppressed { - id: node.id.clone(), - remaining_count: node.suppression_count, - timestamp: Utc::now(), - }); - } - Err(e) => { - // Best-effort: the PR is decided regardless, but surface the - // failure so a stuck-suppressed memory isn't silent. - tracing::warn!( - "memory PR {} {}d but failed to release subject {}: {}", - id, - action_label(action), - subject_id, - e - ); - } - } - } - - state.emit(VestigeEvent::MemoryPrDecided { - id: decided.id.clone(), - decision: decided - .decision - .and_then(|d| serde_json::to_value(d).ok()) - .and_then(|v| v.as_str().map(String::from)) - .unwrap_or_default(), - status: decided.status.as_str().to_string(), - timestamp: Utc::now(), - }); - - let mut out = serde_json::to_value(&decided).unwrap_or_default(); - if let Some(obj) = out.as_object_mut() { - obj.insert("subjectReleased".to_string(), serde_json::json!(released)); - } - Ok(Json(out)) -} - -/// Short label for a Memory PR action, for log lines. -fn action_label(action: vestige_core::MemoryPrAction) -> &'static str { - use vestige_core::MemoryPrAction::*; - match action { - Promote => "promote", - Merge => "merge", - Supersede => "supersede", - Quarantine => "quarantine", - Forget => "forget", - AskAgentWhy => "ask_agent_why", - } -} - -#[derive(Debug, Deserialize)] -pub struct ReviewModeBody { - pub mode: String, -} - -/// Get the current review mode (fast / risk_gated / paranoid). -pub async fn get_review_mode(State(state): State) -> Json { - let mode = read_review_mode(&state); - Json(serde_json::json!({ - "mode": mode.as_str(), - "pendingCount": state.storage.count_pending_memory_prs().unwrap_or(0), - })) -} - -/// Set the review mode. Persisted to a small JSON file in the data dir so it -/// survives restarts (local-first, no extra config service). -pub async fn set_review_mode( - State(state): State, - Json(body): Json, -) -> Result, StatusCode> { - let mode = vestige_core::ReviewMode::from_label(&body.mode); - let path = review_mode_path(&state); - let payload = serde_json::json!({ "mode": mode.as_str() }); - // B7: atomic write (temp + rename) so a concurrent read can never see a - // partially-written / corrupt review_mode.json, reusing the same helper the - // Sanhedrin receipt path uses. - write_atomic(&path, &serde_json::to_vec_pretty(&payload).unwrap_or_default())?; - Ok(Json(serde_json::json!({ "mode": mode.as_str() }))) -} - -/// Path to the persisted review-mode file. -fn review_mode_path(state: &AppState) -> PathBuf { - state.storage.data_dir().join("review_mode.json") -} - -/// Read the persisted review mode, defaulting to RiskGated. -pub fn read_review_mode(state: &AppState) -> vestige_core::ReviewMode { - let path = review_mode_path(state); - fs::read_to_string(&path) - .ok() - .and_then(|s| serde_json::from_str::(&s).ok()) - .and_then(|v| v.get("mode").and_then(|m| m.as_str()).map(String::from)) - .map(|s| vestige_core::ReviewMode::from_label(&s)) - .unwrap_or_default() -} - -// ============================================================================ -// MEMORY HYGIENE + INTELLIGENCE SURFACES (live-wire endpoints) -// -// GET /api/duplicates — cosine duplicate clusters (dedup tool reshaped) -// GET /api/contradictions — trust-weighted contradiction pairs -// GET /api/patterns/cross-project — CrossProjectLearner hydrated on demand -// GET /api/memories/{id}/audit — per-memory audit trail (state transitions) -// ============================================================================ - -#[derive(Debug, Deserialize)] -pub struct DuplicatesParams { - pub threshold: Option, - pub limit: Option, -} - -/// Duplicate clusters for the dashboard. Reuses the `dedup` tool's -/// union-find clustering, then re-fetches each member node so the response -/// carries full content and nodeType (the tool only keeps a 120-char preview). -pub async fn list_duplicates( - State(state): State, - Query(params): Query, -) -> Result, StatusCode> { - let threshold = params.threshold.unwrap_or(0.80).clamp(0.5, 0.99); - let limit = params.limit.unwrap_or(20).clamp(1, 50); - - let args = serde_json::json!({ - "similarity_threshold": threshold, - "limit": limit, - }); - let raw = crate::tools::dedup::execute(&state.storage, Some(args)) - .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - - let mut clusters: Vec = Vec::new(); - if let Some(raw_clusters) = raw.get("clusters").and_then(|v| v.as_array()) { - for cluster in raw_clusters { - let members = cluster - .get("members") - .and_then(|v| v.as_array()) - .cloned() - .unwrap_or_default(); - - // Representative pair similarity: max member similarity-to-anchor, - // skipping the anchor itself (members[0], always exactly 1.0). - let similarity = members - .iter() - .skip(1) - .filter_map(|m| { - m.get("similarityToAnchor") - .and_then(|s| s.as_str()) - .and_then(|s| s.parse::().ok()) - }) - .fold(0.0_f64, f64::max); - - let memories: Vec = members - .iter() - .filter_map(|m| { - let id = m.get("id").and_then(|v| v.as_str())?; - let node = state.storage.get_node(id).ok().flatten()?; - Some(serde_json::json!({ - "id": node.id, - "content": node.content, - "nodeType": node.node_type, - "tags": node.tags, - "retention": node.retention_strength, - "createdAt": node.created_at.to_rfc3339(), - })) - }) - .collect(); - - if memories.len() < 2 { - continue; - } - - clusters.push(serde_json::json!({ - "similarity": similarity, - "suggestedAction": if similarity >= 0.9 { "merge" } else { "review" }, - "memories": memories, - })); - } - } - - Ok(Json(serde_json::json!({ - "threshold": threshold, - "total": clusters.len(), - "clusters": clusters, - }))) -} - -#[derive(Debug, Deserialize)] -pub struct ContradictionsParams { - pub topic: Option, - pub min_trust: Option, - pub limit: Option, -} - -/// Contradiction pairs for the dashboard. Same pairing primitives as the -/// `contradictions` MCP tool (topic overlap >= 0.4, negation/correction -/// heuristics, trust gate), but built directly against the nodes so the -/// response carries nodeType/createdAt and chronological a→b ordering -/// (a = older, b = newer — b is the memory that supersedes a). -pub async fn list_contradictions( - State(state): State, - Query(params): Query, -) -> Result, StatusCode> { - use crate::tools::cross_reference::{appears_contradictory, compute_trust, topic_overlap}; - - let limit = params.limit.unwrap_or(50).clamp(2, 200); - let min_trust = params.min_trust.unwrap_or(0.3).clamp(0.0, 1.0); - let topic_param = params - .topic - .as_deref() - .map(str::trim) - .filter(|s| !s.is_empty()) - .map(String::from); - - let memories = if let Some(topic) = topic_param.as_deref() { - state - .storage - .hybrid_search(topic, limit, 0.3, 0.7) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? - .into_iter() - .map(|result| result.node) - .collect::>() - } else { - state - .storage - .get_all_nodes(limit, 0) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? - }; - - let mut pairs: Vec<(f32, Value)> = Vec::new(); - for i in 0..memories.len() { - for j in (i + 1)..memories.len() { - let x = &memories[i]; - let y = &memories[j]; - let overlap = topic_overlap(&x.content, &y.content); - if overlap < 0.4 || !appears_contradictory(&x.content, &y.content) { - continue; - } - - let x_trust = compute_trust(x.retention_strength, x.stability, x.reps, x.lapses); - let y_trust = compute_trust(y.retention_strength, y.stability, y.reps, y.lapses); - if x_trust.min(y_trust) < min_trust { - continue; - } - - // a = older, b = newer (b supersedes a). - let ((a, trust_a), (b, trust_b)) = if x.created_at <= y.created_at { - ((x, x_trust), (y, y_trust)) - } else { - ((y, y_trust), (x, x_trust)) - }; - let date_diff_days = (b.created_at - a.created_at).num_days().abs(); - let topic_label = topic_param - .clone() - .unwrap_or_else(|| shared_keywords(&a.content, &b.content)); - - pairs.push(( - overlap, - serde_json::json!({ - "memory_a_id": a.id, - "memory_b_id": b.id, - "memory_a_preview": a.content.chars().take(200).collect::(), - "memory_b_preview": b.content.chars().take(200).collect::(), - "memory_a_type": a.node_type, - "memory_b_type": b.node_type, - "memory_a_created": a.created_at.to_rfc3339(), - "memory_b_created": b.created_at.to_rfc3339(), - "memory_a_tags": a.tags, - "memory_b_tags": b.tags, - "trust_a": (trust_a * 100.0).round() / 100.0, - "trust_b": (trust_b * 100.0).round() / 100.0, - "similarity": overlap, - "date_diff_days": date_diff_days, - "topic": topic_label, - }), - )); - } - } - - pairs.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal)); - let contradictions: Vec = pairs.into_iter().map(|(_, v)| v).collect(); - - Ok(Json(serde_json::json!({ - "memoriesAnalyzed": memories.len(), - "total": contradictions.len(), - "contradictions": contradictions, - }))) -} - -/// Top shared substantive keywords between two contents — same tokenization -/// as `topic_overlap` (lowercase, whitespace split, length > 3), trimmed of -/// punctuation for display. Deterministic: longest-first, then alphabetical. -fn shared_keywords(a: &str, b: &str) -> String { - let a_lower = a.to_lowercase(); - let b_lower = b.to_lowercase(); - let tokenize = |s: &'_ str| -> HashSet { - s.split_whitespace() - .map(|w| { - w.trim_matches(|c: char| !c.is_alphanumeric()) - .to_string() - }) - .filter(|w| w.len() > 3) - .collect() - }; - let a_words = tokenize(&a_lower); - let b_words = tokenize(&b_lower); - let mut shared: Vec<&String> = a_words.intersection(&b_words).collect(); - shared.sort_by(|x, y| y.len().cmp(&x.len()).then_with(|| x.cmp(y))); - shared - .into_iter() - .take(3) - .cloned() - .collect::>() - .join(" ") -} - -/// Cross-project pattern transfer. The `CrossProjectLearner` state is not -/// persisted, so hydrate on demand: fetch the 500 most recent memories, infer -/// project + category for each, run `learn_from_memories`, then map the -/// discovered patterns to the dashboard DTO. Only the six frontend categories -/// are emitted; sparse or empty results are expected on small stores. -pub async fn get_cross_project_patterns( - State(state): State, -) -> Result, StatusCode> { - use std::collections::HashMap; - use vestige_core::advanced::cross_project::{ - CrossProjectLearner, MemoryForLearning, PatternCategory, - }; - - let nodes = state - .storage - .get_all_nodes(500, 0) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - - let mut projects: Vec = Vec::new(); - // Earliest memory creation per project — used to order projects_seen_in - // (the learner stores them in HashSet order, which is nondeterministic). - let mut project_first_seen: HashMap> = HashMap::new(); - let mut memories: Vec = Vec::new(); - - for node in &nodes { - // Memories with no real project attribution are skipped — patterns - // need genuine cross-project evidence, not an "unknown" bucket. - let Some(project) = infer_project(node) else { - continue; - }; - if !projects.contains(&project) { - projects.push(project.clone()); - } - project_first_seen - .entry(project.clone()) - .and_modify(|t| { - if node.created_at < *t { - *t = node.created_at; - } - }) - .or_insert(node.created_at); - memories.push(MemoryForLearning { - id: node.id.clone(), - content: node.content.clone(), - project_name: project, - category: infer_category(node), - }); - } - - let learner = CrossProjectLearner::new(); - learner.learn_from_memories(&memories); - - let mut patterns: Vec = learner - .get_all_patterns() - .into_iter() - .filter_map(|p| { - let category = match p.pattern.category { - PatternCategory::ErrorHandling => "ErrorHandling", - PatternCategory::AsyncConcurrency => "AsyncConcurrency", - PatternCategory::Testing => "Testing", - PatternCategory::Architecture => "Architecture", - PatternCategory::Performance => "Performance", - PatternCategory::Security => "Security", - // Frontend tracks exactly six categories; drop the rest. - _ => return None, - }; - - let mut seen_in = p.projects_seen_in.clone(); - seen_in.sort_by_key(|proj| { - project_first_seen - .get(proj) - .copied() - .unwrap_or_else(Utc::now) - }); - let origin_project = seen_in.first().cloned()?; - let transferred_to: Vec = seen_in.into_iter().skip(1).collect(); - - Some(serde_json::json!({ - "name": p.pattern.name, - "category": category, - "origin_project": origin_project, - "transferred_to": transferred_to, - "transfer_count": transferred_to.len(), - "last_used": p.last_seen.to_rfc3339(), - "confidence": p.confidence, - })) - }) - .collect(); - - // Deterministic order: confidence desc, then name. - patterns.sort_by(|a, b| { - let ca = a["confidence"].as_f64().unwrap_or(0.0); - let cb = b["confidence"].as_f64().unwrap_or(0.0); - cb.partial_cmp(&ca) - .unwrap_or(std::cmp::Ordering::Equal) - .then_with(|| a["name"].as_str().cmp(&b["name"].as_str())) - }); - - Ok(Json(serde_json::json!({ - "projects": projects, - "patterns": patterns, - }))) -} - -/// Infer which project a memory belongs to. Priority order: -/// 1. connector envelope's `source_project` (authoritative) -/// 2. a `codebase:{name}` / `project:{name}` tag (codebase tool convention) -/// 3. the `source` field when it is a bare project identifier -/// -/// Returns None when no real attribution exists — such memories are skipped. -fn infer_project(node: &vestige_core::KnowledgeNode) -> Option { - if let Some(project) = node - .source_envelope - .as_ref() - .and_then(|e| e.source_project.as_deref()) - .filter(|p| !p.trim().is_empty()) - { - return Some(project.trim().to_string()); - } - - for tag in &node.tags { - for prefix in ["codebase:", "project:"] { - if let Some(name) = tag.strip_prefix(prefix).filter(|n| !n.trim().is_empty()) { - return Some(name.trim().to_string()); - } - } - } - - // `codebase` tool sets source to the bare codebase name. Reject paths, - // URLs, and generic provenance labels. - if let Some(source) = node.source.as_deref().map(str::trim) - && !source.is_empty() - && !source.contains('/') - && !source.contains(':') - && !source.contains(char::is_whitespace) - && !matches!( - source.to_lowercase().as_str(), - "conversation" | "chat" | "manual" | "user" | "unknown" | "file" - ) - { - return Some(source.to_string()); - } - - None -} - -/// Map a memory's tags + content onto a `PatternCategory` via the keyword -/// conventions the dashboard tracks. First match wins; memories with no -/// category return None and are skipped by the learner. -fn infer_category( - node: &vestige_core::KnowledgeNode, -) -> Option { - use vestige_core::advanced::cross_project::PatternCategory; - - let haystack = format!("{} {}", node.tags.join(" "), node.content).to_lowercase(); - let rules: &[(&[&str], PatternCategory)] = &[ - ( - &["error", "panic", "result", "unwrap"], - PatternCategory::ErrorHandling, - ), - ( - &["async", "tokio", "race", "concurrency", "deadlock"], - PatternCategory::AsyncConcurrency, - ), - ( - &["test", "vitest", "cargo test", "playwright"], - PatternCategory::Testing, - ), - ( - &["architecture", "module", "design"], - PatternCategory::Architecture, - ), - ( - &["perf", "latency", "optimize", "benchmark"], - PatternCategory::Performance, - ), - ( - &["security", "auth", "secret", "vulnerability"], - PatternCategory::Security, - ), - ]; - - rules - .iter() - .find(|(keywords, _)| keywords.iter().any(|k| haystack.contains(k))) - .map(|(_, category)| category.clone()) -} - -#[derive(Debug, Deserialize)] -pub struct AuditParams { - pub limit: Option, -} - -/// Per-memory audit trail: state transitions mapped to the 8 dashboard -/// `AuditAction`s, plus a synthetic `created` event at node creation time. -/// Events are returned newest-first (matches `splitVisible` in the frontend). -pub async fn get_memory_audit( - State(state): State, - Path(id): Path, - Query(params): Query, -) -> Result, StatusCode> { - let limit = params.limit.unwrap_or(100).clamp(1, 500); - - let node = state - .storage - .get_node(&id) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? - .ok_or(StatusCode::NOT_FOUND)?; - - let transitions = state - .storage - .get_state_transitions(&id, limit) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - - let mut events: Vec<(DateTime, Value)> = Vec::new(); - for t in &transitions { - let Some(action) = audit_action_for(&t.reason_type, &t.from_state, &t.to_state) else { - continue; - }; - let triggered_by = match t.reason_type.as_str() { - "user_suppression" | "manual_override" => "user", - _ => "system", - }; - let reason = t - .reason_data - .clone() - .unwrap_or_else(|| format!("{} → {} ({})", t.from_state, t.to_state, t.reason_type)); - events.push(( - t.timestamp, - serde_json::json!({ - "action": action, - "timestamp": t.timestamp.to_rfc3339(), - "reason": reason, - "triggered_by": triggered_by, - }), - )); - } - - // Synthetic anchor: every memory's trail begins with its creation. - events.push(( - node.created_at, - serde_json::json!({ - "action": "created", - "timestamp": node.created_at.to_rfc3339(), - "reason": "Memory ingested", - "triggered_by": "system", - }), - )); - - // Newest-first, matching the frontend's expected ordering. - events.sort_by_key(|(ts, _)| Reverse(*ts)); - let events: Vec = events.into_iter().map(|(_, v)| v).collect(); - - Ok(Json(serde_json::json!({ - "memoryId": node.id, - "events": events, - }))) -} - -/// Map a `state_transitions.reason_type` onto a dashboard `AuditAction`. -/// -/// Real reason_type values (see migrations.rs V-schema comment): access, -/// time_decay, cue_reactivation, competition_loss, interference_resolved, -/// user_suppression, suppression_expired, manual_override, system_init. -/// Unknown reasons fall back to the state-rank direction (up → promoted, -/// down → demoted, flat → accessed). `system_init` is dropped because the -/// synthetic `created` event already anchors the trail. -fn audit_action_for(reason_type: &str, from_state: &str, to_state: &str) -> Option<&'static str> { - match reason_type { - "access" | "cue_reactivation" => Some("accessed"), - "time_decay" | "competition_loss" => Some("demoted"), - "user_suppression" => Some("suppressed"), - "suppression_expired" | "interference_resolved" => Some("reconsolidated"), - "manual_override" => Some("edited"), - "system_init" => None, - other if other.contains("dream") => Some("dreamed"), - _ => { - let rank = |s: &str| match s { - "active" => 3, - "dormant" => 2, - "silent" => 1, - "unavailable" => 0, - _ => 2, - }; - Some(match rank(to_state).cmp(&rank(from_state)) { - std::cmp::Ordering::Greater => "promoted", - std::cmp::Ordering::Less => "demoted", - std::cmp::Ordering::Equal => "accessed", - }) - } - } -} - #[cfg(test)] mod tests { use super::*; @@ -3026,284 +1595,4 @@ mod tests { let connected_err = default_center_id(&storage, GraphSort::Connected).unwrap_err(); assert_eq!(connected_err, StatusCode::NOT_FOUND); } - - // ======================================================================== - // Live-wire endpoints: /api/duplicates, /api/contradictions, - // /api/patterns/cross-project, /api/memories/{id}/audit - // ======================================================================== - - #[tokio::test] - async fn duplicates_returns_contract_shape_when_no_embeddings() { - let (_dir, storage) = seed_storage(); - ingest(&storage, "first memory about parsers"); - ingest(&storage, "second memory about parsers"); - let state = AppState::new(storage, None); - - let Json(body) = list_duplicates( - State(state), - Query(DuplicatesParams { - threshold: None, - limit: None, - }), - ) - .await - .unwrap(); - - // Field names are load-bearing — the frontend interface is shipped. - assert_eq!(body["threshold"], 0.8); - assert_eq!(body["total"], 0); - assert!(body["clusters"].as_array().unwrap().is_empty()); - } - - #[tokio::test] - async fn duplicates_clamps_threshold_and_limit() { - let (_dir, storage) = seed_storage(); - let state = AppState::new(storage, None); - - let Json(body) = list_duplicates( - State(state), - Query(DuplicatesParams { - threshold: Some(1.5), - limit: Some(9999), - }), - ) - .await - .unwrap(); - - assert_eq!(body["threshold"], 0.99); - } - - #[tokio::test] - async fn contradictions_reports_pair_with_contract_fields() { - let (_dir, storage) = seed_storage(); - let first = ingest( - &storage, - "For the release workflow we always run cargo test before publishing Vestige", - ); - let second = ingest( - &storage, - "Correction: for the release workflow we never run cargo test before publishing Vestige", - ); - let state = AppState::new(storage, None); - - let Json(body) = list_contradictions( - State(state), - Query(ContradictionsParams { - topic: None, - min_trust: Some(0.0), - limit: None, - }), - ) - .await - .unwrap(); - - assert_eq!(body["memoriesAnalyzed"], 2); - assert_eq!(body["total"], 1); - let pair = &body["contradictions"][0]; - // Exact frontend field names (ContradictionArcs.svelte interface). - for field in [ - "memory_a_id", - "memory_b_id", - "memory_a_preview", - "memory_b_preview", - "memory_a_type", - "memory_b_type", - "memory_a_created", - "memory_b_created", - "memory_a_tags", - "memory_b_tags", - "trust_a", - "trust_b", - "similarity", - "date_diff_days", - "topic", - ] { - assert!( - !pair[field].is_null(), - "missing contract field: {}", - field - ); - } - // a = older, b = newer (chronological). - let ids: Vec<&str> = vec![ - pair["memory_a_id"].as_str().unwrap(), - pair["memory_b_id"].as_str().unwrap(), - ]; - assert!(ids.contains(&first.as_str())); - assert!(ids.contains(&second.as_str())); - assert!( - pair["memory_a_created"].as_str().unwrap() - <= pair["memory_b_created"].as_str().unwrap(), - "a must be the older memory" - ); - assert!(pair["similarity"].as_f64().unwrap() >= 0.4); - assert!(!pair["topic"].as_str().unwrap().is_empty()); - } - - fn ingest_project_memory(storage: &Storage, content: &str, project: &str) { - storage - .ingest(IngestInput { - content: content.to_string(), - node_type: "fact".to_string(), - tags: vec![format!("codebase:{}", project)], - ..Default::default() - }) - .unwrap(); - } - - #[tokio::test] - async fn cross_project_patterns_finds_transfer_across_two_projects() { - let (_dir, storage) = seed_storage(); - // Same ErrorHandling keyword ("timeout", > 5 chars) in two projects - // — exactly what the learner needs to mint a universal pattern. - ingest_project_memory( - &storage, - "Fixed timeout error by wrapping the retry loop in a Result", - "alpha", - ); - ingest_project_memory( - &storage, - "The API client needs a timeout guard or the error propagates", - "beta", - ); - let state = AppState::new(storage, None); - - let Json(body) = get_cross_project_patterns(State(state)).await.unwrap(); - - let projects: Vec<&str> = body["projects"] - .as_array() - .unwrap() - .iter() - .map(|p| p.as_str().unwrap()) - .collect(); - assert!(projects.contains(&"alpha")); - assert!(projects.contains(&"beta")); - - let patterns = body["patterns"].as_array().unwrap(); - assert!(!patterns.is_empty(), "expected at least one pattern"); - let pattern = &patterns[0]; - // Exact frontend field names (patterns/+page.svelte interface). - assert!(pattern["name"].is_string()); - assert_eq!(pattern["category"], "ErrorHandling"); - assert!(pattern["origin_project"].is_string()); - assert!(pattern["transferred_to"].is_array()); - assert_eq!( - pattern["transfer_count"].as_u64().unwrap() as usize, - pattern["transferred_to"].as_array().unwrap().len() - ); - assert!(pattern["last_used"].is_string()); - assert!(pattern["confidence"].is_number()); - } - - #[tokio::test] - async fn cross_project_patterns_empty_without_project_attribution() { - let (_dir, storage) = seed_storage(); - ingest(&storage, "memory with no project attribution at all"); - let state = AppState::new(storage, None); - - let Json(body) = get_cross_project_patterns(State(state)).await.unwrap(); - - assert!(body["projects"].as_array().unwrap().is_empty()); - assert!(body["patterns"].as_array().unwrap().is_empty()); - } - - #[tokio::test] - async fn memory_audit_prepends_created_and_maps_time_decay_to_demoted() { - let (_dir, storage) = seed_storage(); - let id = ingest(&storage, "audit trail test memory"); - // Create the memory_states row, then force a recorded transition. - storage.record_memory_access(&id).unwrap(); - storage - .update_memory_state(&id, "dormant", "time_decay") - .unwrap(); - let state = AppState::new(storage, None); - - let Json(body) = get_memory_audit( - State(state), - Path(id.clone()), - Query(AuditParams { limit: None }), - ) - .await - .unwrap(); - - assert_eq!(body["memoryId"], id.as_str()); - let events = body["events"].as_array().unwrap(); - assert_eq!(events.len(), 2); - // Newest-first: the transition, then the synthetic created anchor. - assert_eq!(events[0]["action"], "demoted"); - assert_eq!(events[1]["action"], "created"); - for event in events { - assert!(event["action"].is_string()); - assert!(event["timestamp"].is_string()); - assert!(event["reason"].is_string()); - assert!(event["triggered_by"].is_string()); - } - } - - #[tokio::test] - async fn memory_audit_unknown_id_is_404() { - let (_dir, storage) = seed_storage(); - let state = AppState::new(storage, None); - - let err = get_memory_audit( - State(state), - Path("00000000-0000-0000-0000-000000000000".to_string()), - Query(AuditParams { limit: None }), - ) - .await - .unwrap_err(); - - assert_eq!(err, StatusCode::NOT_FOUND); - } - - #[test] - fn audit_action_mapping_covers_real_reason_types() { - assert_eq!(audit_action_for("access", "dormant", "active"), Some("accessed")); - assert_eq!( - audit_action_for("cue_reactivation", "silent", "active"), - Some("accessed") - ); - assert_eq!( - audit_action_for("time_decay", "active", "dormant"), - Some("demoted") - ); - assert_eq!( - audit_action_for("competition_loss", "active", "unavailable"), - Some("demoted") - ); - assert_eq!( - audit_action_for("user_suppression", "active", "unavailable"), - Some("suppressed") - ); - assert_eq!( - audit_action_for("suppression_expired", "unavailable", "dormant"), - Some("reconsolidated") - ); - assert_eq!( - audit_action_for("interference_resolved", "dormant", "dormant"), - Some("reconsolidated") - ); - assert_eq!( - audit_action_for("manual_override", "silent", "active"), - Some("edited") - ); - assert_eq!(audit_action_for("system_init", "active", "active"), None); - // Unknown reasons fall back to state-rank direction. - assert_eq!( - audit_action_for("mystery", "silent", "active"), - Some("promoted") - ); - assert_eq!( - audit_action_for("mystery", "active", "silent"), - Some("demoted") - ); - assert_eq!( - audit_action_for("mystery", "active", "active"), - Some("accessed") - ); - assert_eq!( - audit_action_for("rem_dream_replay", "active", "active"), - Some("dreamed") - ); - } } diff --git a/crates/vestige-mcp/src/dashboard/mod.rs b/crates/vestige-mcp/src/dashboard/mod.rs index 2a4f88a..6bf9bc1 100644 --- a/crates/vestige-mcp/src/dashboard/mod.rs +++ b/crates/vestige-mcp/src/dashboard/mod.rs @@ -176,50 +176,6 @@ fn build_router_inner(state: AppState, port: u16) -> (Router, AppState) { // Wraps crate::tools::cross_reference::execute. Emits // DeepReferenceCompleted so Graph3D can glide, pulse, and arc. .route("/api/deep_reference", post(handlers::deep_reference_query)) - // Sanhedrin receipts: latest local hook verdict + appeal training. - .route("/api/sanhedrin/latest", get(handlers::get_sanhedrin_latest)) - .route( - "/api/sanhedrin/telemetry", - get(handlers::get_sanhedrin_telemetry), - ) - .route("/api/sanhedrin/appeal", post(handlers::appeal_sanhedrin)) - // ============================================================ - // AGENT BLACK BOX (v2.2) — replayable agent-run traces - // ============================================================ - .route("/api/traces", get(handlers::list_traces)) - .route("/api/traces/{run_id}", get(handlers::get_trace)) - .route("/api/traces/{run_id}/export", get(handlers::export_trace)) - // ============================================================ - // MEMORY RECEIPTS (v2.2) — the nutrition label for a retrieval - // ============================================================ - .route("/api/receipts", get(handlers::list_receipts)) - .route("/api/receipts/{receipt_id}", get(handlers::get_receipt)) - // ============================================================ - // MEMORY HYGIENE + INTELLIGENCE (live-wire) — duplicates, - // contradictions, cross-project patterns, per-memory audit - // ============================================================ - .route("/api/duplicates", get(handlers::list_duplicates)) - .route("/api/contradictions", get(handlers::list_contradictions)) - .route( - "/api/patterns/cross-project", - get(handlers::get_cross_project_patterns), - ) - .route("/api/memories/{id}/audit", get(handlers::get_memory_audit)) - // ============================================================ - // MEMORY PRs (v2.2) — risk-gated brain-change review queue - // ============================================================ - .route("/api/memory-prs", get(handlers::list_memory_prs)) - // Static `/mode` routes declared BEFORE the dynamic `/{id}` route (B7 - // hygiene). axum 0.8/matchit already prioritizes static segments, but - // declaring them first makes the intent unambiguous and guards against - // a future router that doesn't. - .route("/api/memory-prs/mode", get(handlers::get_review_mode)) - .route("/api/memory-prs/mode", post(handlers::set_review_mode)) - .route("/api/memory-prs/{id}", get(handlers::get_memory_pr)) - .route( - "/api/memory-prs/{id}/{action}", - post(handlers::act_on_memory_pr), - ) .layer( ServiceBuilder::new() .concurrency_limit(50) diff --git a/crates/vestige-mcp/src/lib.rs b/crates/vestige-mcp/src/lib.rs index 1df4fc5..8784409 100644 --- a/crates/vestige-mcp/src/lib.rs +++ b/crates/vestige-mcp/src/lib.rs @@ -9,4 +9,3 @@ pub mod protocol; pub mod resources; pub mod server; pub mod tools; -pub mod trace_recorder; diff --git a/crates/vestige-mcp/src/main.rs b/crates/vestige-mcp/src/main.rs index 062e750..9aa59d0 100644 --- a/crates/vestige-mcp/src/main.rs +++ b/crates/vestige-mcp/src/main.rs @@ -1,4 +1,4 @@ -//! Vestige MCP Server - local cognitive memory for MCP agents. +//! Vestige MCP Server v1.0 - Cognitive Memory for Claude //! //! A bleeding-edge Rust MCP (Model Context Protocol) server that provides //! Claude and other AI assistants with long-term memory capabilities @@ -54,7 +54,6 @@ const DATABASE_FILE: &str = "vestige.db"; struct Config { data_dir: Option, http_port: u16, - http_enabled: bool, dashboard_enabled: bool, } @@ -80,9 +79,6 @@ fn parse_args_from(args: Vec, env_data_dir: Option) -> Config .ok() .and_then(|s| s.parse().ok()) .unwrap_or(3928); - let mut http_enabled = std::env::var("VESTIGE_HTTP_ENABLED") - .map(|v| v.eq_ignore_ascii_case("true") || v == "1") - .unwrap_or(false); let dashboard_enabled = std::env::var("VESTIGE_DASHBOARD_ENABLED") .map(|v| v.eq_ignore_ascii_case("true") || v == "1") .unwrap_or(false); @@ -105,9 +101,7 @@ fn parse_args_from(args: Vec, env_data_dir: Option) -> Config println!( " --data-dir Custom data directory (overrides VESTIGE_DATA_DIR)" ); - println!(" --http Enable Streamable HTTP transport"); - println!(" --no-http Disable Streamable HTTP transport"); - println!(" --http-port HTTP transport port (also enables HTTP)"); + println!(" --http-port HTTP transport port (default: 3928)"); println!(); println!("ENVIRONMENT:"); println!( @@ -117,14 +111,10 @@ fn parse_args_from(args: Vec, env_data_dir: Option) -> Config " RUST_LOG Log level filter (e.g., debug, info, warn, error)" ); println!( - " VESTIGE_AUTH_TOKEN Override the bearer token for HTTP transport" + " VESTIGE_AUTH_TOKEN Override the bearer token for HTTP transport" ); - println!(" VESTIGE_HTTP_ENABLED Enable HTTP transport (default: false)"); - println!(" VESTIGE_HTTP_PORT HTTP transport port (default: 3928)"); - println!( - " VESTIGE_HTTP_ALLOWED_ORIGINS Comma-separated browser origins allowed for HTTP" - ); - println!(" VESTIGE_DASHBOARD_ENABLED Enable dashboard (default: disabled)"); + println!(" VESTIGE_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)" @@ -134,7 +124,7 @@ fn parse_args_from(args: Vec, env_data_dir: Option) -> Config println!(" vestige-mcp"); println!(" vestige-mcp --data-dir /custom/path"); println!(" VESTIGE_DATA_DIR=~/.vestige vestige-mcp"); - println!(" vestige-mcp --http --http-port 8080"); + println!(" vestige-mcp --http-port 8080"); println!(" RUST_LOG=debug vestige-mcp"); std::process::exit(0); } @@ -166,14 +156,7 @@ fn parse_args_from(args: Vec, env_data_dir: Option) -> Config } data_dir = Some(PathBuf::from(path)); } - "--http" => { - http_enabled = true; - } - "--no-http" => { - http_enabled = false; - } "--http-port" => { - http_enabled = true; i += 1; if i >= args.len() { eprintln!("error: --http-port requires a port number"); @@ -190,7 +173,6 @@ fn parse_args_from(args: Vec, env_data_dir: Option) -> Config }; } arg if arg.starts_with("--http-port=") => { - http_enabled = true; let val = arg.strip_prefix("--http-port=").unwrap_or(""); http_port = match val.parse() { Ok(p) => p, @@ -213,7 +195,6 @@ fn parse_args_from(args: Vec, env_data_dir: Option) -> Config Config { data_dir, http_port, - http_enabled, dashboard_enabled, } } @@ -243,26 +224,10 @@ fn prepare_storage_path(data_dir: Option) -> io::Result }; let data_dir = expand_tilde(data_dir); - - // Check if path exists and is a file (not a directory) - if data_dir.exists() && !data_dir.is_dir() { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - format!( - "Data directory path exists but is not a directory: {}", - data_dir.display() - ), - )); - } - - // Only create if it doesn't exist (avoids "File exists" error on existing directories) - let created = !data_dir.exists(); - if created { - fs::create_dir_all(&data_dir)?; - } + fs::create_dir_all(&data_dir)?; #[cfg(unix)] - if created { + { use std::os::unix::fs::PermissionsExt; let _ = fs::set_permissions(&data_dir, fs::Permissions::from_mode(0o700)); } @@ -301,6 +266,21 @@ async fn main() { let storage = match Storage::new(storage_path) { Ok(s) => { info!("Storage initialized successfully"); + + // Try to initialize embeddings early and log any issues + #[cfg(feature = "embeddings")] + { + 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 ~/.cache/vestige/fastembed is writable" + ); + } else { + info!("Embedding service initialized successfully"); + } + } + Arc::new(s) } Err(e) => { @@ -309,40 +289,6 @@ async fn main() { } }; - // Initialize embeddings in the background so MCP clients can complete the - // stdio handshake quickly. First-run model downloads can otherwise exceed - // short client startup timeouts. - #[cfg(feature = "embeddings")] - { - let storage_clone = Arc::clone(&storage); - tokio::task::spawn_blocking(move || { - if let Err(e) = storage_clone.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 ~/.cache/vestige/fastembed is writable" - ); - } else { - info!("Embedding service initialized successfully"); - - #[cfg(feature = "vector-search")] - match storage_clone.generate_embeddings(None, false) { - Ok(result) => { - if result.successful > 0 || result.failed > 0 { - info!( - embeddings_generated = result.successful, - embeddings_failed = result.failed, - embeddings_skipped = result.skipped, - "Background embedding backfill complete" - ); - } - } - Err(e) => warn!("Background embedding backfill failed: {}", e), - } - } - }); - } - // Spawn periodic auto-consolidation so FSRS-6 decay scores stay fresh. // Runs on startup (if needed) and then every N hours (default: 6). // Configurable via VESTIGE_CONSOLIDATION_INTERVAL_HOURS env var. @@ -484,8 +430,8 @@ async fn main() { info!("Dashboard disabled by VESTIGE_DASHBOARD_ENABLED=false"); } - // Start optional HTTP MCP transport for clients that need Streamable HTTP. - if config.http_enabled { + // Start HTTP MCP transport (Streamable HTTP for Claude.ai / remote clients) + { let http_storage = Arc::clone(&storage); let http_cognitive = Arc::clone(&cognitive); let http_event_tx = event_tx.clone(); @@ -496,9 +442,7 @@ async fn main() { let bind = std::env::var("VESTIGE_HTTP_BIND").unwrap_or_else(|_| "127.0.0.1".to_string()); eprintln!("Vestige HTTP transport: http://{}:{}/mcp", bind, http_port); - if let Ok(path) = protocol::auth::token_path() { - eprintln!("Auth token file: {}", path.display()); - } + eprintln!("Auth token: {}...", &token[..token.len().min(8)]); tokio::spawn(async move { if let Err(e) = protocol::http::start_http_transport( http_storage, @@ -520,12 +464,10 @@ async fn main() { ); } } - } else { - info!("HTTP MCP transport disabled; set VESTIGE_HTTP_ENABLED=1 or pass --http to enable"); } // Load cross-encoder reranker in the background (downloads ~150MB on first run) - #[cfg(all(feature = "vector-search", feature = "embeddings"))] + #[cfg(feature = "embeddings")] { let cog_clone = Arc::clone(&cognitive); tokio::spawn(async move { @@ -569,7 +511,6 @@ mod tests { ); assert_eq!(config.data_dir, Some(PathBuf::from("/tmp/vestige-env"))); - assert!(!config.http_enabled); } #[test] @@ -582,16 +523,6 @@ mod tests { assert_eq!(config.data_dir, Some(PathBuf::from("/tmp/vestige-cli"))); } - #[test] - fn http_is_opt_in_and_port_flag_enables_it() { - let disabled = parse_args_from(os_args(&["vestige-mcp"]), None); - assert!(!disabled.http_enabled); - - let enabled = parse_args_from(os_args(&["vestige-mcp", "--http-port", "8080"]), None); - assert!(enabled.http_enabled); - assert_eq!(enabled.http_port, 8080); - } - #[test] fn prepare_storage_path_creates_dir_and_points_to_vestige_db() { let temp = tempfile::tempdir().unwrap(); @@ -603,34 +534,6 @@ mod tests { assert_eq!(db_path, Some(data_dir.join(DATABASE_FILE))); } - #[test] - fn prepare_storage_path_reuses_existing_data_dir() { - let temp = tempfile::tempdir().unwrap(); - let data_dir = temp.path().join("existing"); - fs::create_dir_all(&data_dir).unwrap(); - - let db_path = prepare_storage_path(Some(data_dir.clone())).unwrap(); - - assert_eq!(db_path, Some(data_dir.join(DATABASE_FILE))); - } - - #[cfg(unix)] - #[test] - fn prepare_storage_path_preserves_existing_data_dir_permissions() { - use std::os::unix::fs::PermissionsExt; - - let temp = tempfile::tempdir().unwrap(); - let data_dir = temp.path().join("shared"); - fs::create_dir_all(&data_dir).unwrap(); - fs::set_permissions(&data_dir, fs::Permissions::from_mode(0o755)).unwrap(); - - let db_path = prepare_storage_path(Some(data_dir.clone())).unwrap(); - let mode = fs::metadata(&data_dir).unwrap().permissions().mode() & 0o777; - - assert_eq!(db_path, Some(data_dir.join(DATABASE_FILE))); - assert_eq!(mode, 0o755); - } - #[test] fn expand_tilde_expands_current_users_home_only() { let home = BaseDirs::new().unwrap().home_dir().to_path_buf(); diff --git a/crates/vestige-mcp/src/protocol/auth.rs b/crates/vestige-mcp/src/protocol/auth.rs index 955336b..dce821b 100644 --- a/crates/vestige-mcp/src/protocol/auth.rs +++ b/crates/vestige-mcp/src/protocol/auth.rs @@ -19,7 +19,7 @@ use tracing::{info, warn}; const MIN_TOKEN_LENGTH: usize = 32; /// Return the auth token file path inside the Vestige data directory. -pub fn token_path() -> Result> { +fn token_path() -> Result> { let dirs = ProjectDirs::from("com", "vestige", "core") .ok_or("could not determine project directories")?; Ok(dirs.data_dir().join("auth_token")) diff --git a/crates/vestige-mcp/src/protocol/http.rs b/crates/vestige-mcp/src/protocol/http.rs index ac5b325..e90c313 100644 --- a/crates/vestige-mcp/src/protocol/http.rs +++ b/crates/vestige-mcp/src/protocol/http.rs @@ -12,7 +12,7 @@ use std::sync::Arc; use std::time::{Duration, Instant}; use axum::extract::{DefaultBodyLimit, State}; -use axum::http::{HeaderMap, HeaderValue, StatusCode, header}; +use axum::http::{HeaderMap, StatusCode}; use axum::response::IntoResponse; use axum::routing::{delete, post}; use axum::{Json, Router}; @@ -48,7 +48,6 @@ const MAX_BODY_SIZE: usize = 256 * 1024; struct Session { server: McpServer, last_active: Instant, - protocol_version: String, } /// Shared state cloned into every axum handler. @@ -59,7 +58,6 @@ pub struct HttpTransportState { cognitive: Arc>, event_tx: broadcast::Sender, auth_token: String, - allowed_origins: Arc>, } /// Start the HTTP MCP transport on `127.0.0.1:`. @@ -78,7 +76,6 @@ pub async fn start_http_transport( cognitive, event_tx, auth_token, - allowed_origins: Arc::new(allowed_origins(port)), }; // Spawn session reaper @@ -108,12 +105,6 @@ pub async fn start_http_transport( }); } - let cors_origins = state - .allowed_origins - .iter() - .filter_map(|origin| origin.parse().ok()) - .collect::>(); - let app = Router::new() .route("/mcp", post(post_mcp)) .route("/mcp", delete(delete_mcp)) @@ -123,7 +114,15 @@ pub async fn start_http_transport( .layer(ConcurrencyLimitLayer::new(CONCURRENCY_LIMIT)) .layer( CorsLayer::new() - .allow_origin(cors_origins) + .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, @@ -132,12 +131,6 @@ pub async fn start_http_transport( .allow_headers([ axum::http::header::CONTENT_TYPE, axum::http::header::AUTHORIZATION, - axum::http::HeaderName::from_static("mcp-protocol-version"), - axum::http::HeaderName::from_static("mcp-session-id"), - ]) - .expose_headers([ - axum::http::HeaderName::from_static("mcp-protocol-version"), - axum::http::HeaderName::from_static("mcp-session-id"), ]), ), ) @@ -174,17 +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"))?; - // The auth-scheme token is case-insensitive per RFC 7235/6750, so match - // "Bearer" case-insensitively (accepting "bearer", "BEARER", ...) while - // preserving the token's original case. - let token = header - .split_once(' ') - .filter(|(scheme, _)| scheme.eq_ignore_ascii_case("Bearer")) - .map(|(_, token)| token.trim_start()) - .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 @@ -201,109 +187,6 @@ fn validate_auth(headers: &HeaderMap, expected: &str) -> Result<(), (StatusCode, Ok(()) } -fn allowed_origins(port: u16) -> Vec { - if let Ok(configured) = std::env::var("VESTIGE_HTTP_ALLOWED_ORIGINS") { - let origins: Vec = configured - .split(',') - .map(str::trim) - .filter(|origin| !origin.is_empty()) - .map(ToOwned::to_owned) - .collect(); - if !origins.is_empty() { - return origins; - } - } - - vec![ - format!("http://127.0.0.1:{}", port), - format!("http://localhost:{}", port), - ] -} - -fn validate_origin( - headers: &HeaderMap, - allowed_origins: &[String], -) -> Result<(), (StatusCode, &'static str)> { - let Some(origin) = headers.get(header::ORIGIN).and_then(|v| v.to_str().ok()) else { - return Ok(()); - }; - - if allowed_origins.iter().any(|allowed| allowed == origin) { - Ok(()) - } else { - Err((StatusCode::FORBIDDEN, "Origin not allowed")) - } -} - -fn validate_accept(headers: &HeaderMap) -> Result<(), (StatusCode, &'static str)> { - let Some(accept) = headers.get(header::ACCEPT).and_then(|v| v.to_str().ok()) else { - return Err(( - StatusCode::NOT_ACCEPTABLE, - "Accept must include application/json and text/event-stream", - )); - }; - - let mut accepts_json = false; - let mut accepts_sse = false; - for mime in accept - .split(',') - .map(|part| part.trim().split(';').next().unwrap_or("").trim()) - { - // `*/*` accepts anything; `application/*` / `text/*` accept a whole type. - // Honoring these lets generic HTTP clients (curl's default Accept: */*) - // reach /mcp instead of getting a hard 406. - accepts_json |= - mime == "application/json" || mime == "application/*" || mime == "*/*"; - accepts_sse |= mime == "text/event-stream" || mime == "text/*" || mime == "*/*"; - } - - if accepts_json && accepts_sse { - Ok(()) - } else { - Err(( - StatusCode::NOT_ACCEPTABLE, - "Accept must include application/json and text/event-stream", - )) - } -} - -fn protocol_version_from_headers(headers: &HeaderMap) -> Option<&str> { - headers - .get("mcp-protocol-version") - .and_then(|v| v.to_str().ok()) -} - -fn validate_protocol_version( - headers: &HeaderMap, - expected: &str, -) -> Result<(), (StatusCode, &'static str)> { - let Some(version) = protocol_version_from_headers(headers) else { - return Err(( - StatusCode::BAD_REQUEST, - "MCP-Protocol-Version header required", - )); - }; - - if version == expected { - Ok(()) - } else { - Err((StatusCode::BAD_REQUEST, "MCP-Protocol-Version mismatch")) - } -} - -fn response_protocol_version(response: &crate::protocol::types::JsonRpcResponse) -> Option { - if response.error.is_some() { - return None; - } - - response - .result - .as_ref() - .and_then(|result| result.get("protocolVersion")) - .and_then(|value| value.as_str()) - .map(ToOwned::to_owned) -} - /// Extract and validate the `Mcp-Session-Id` header value. /// /// Only accepts valid UUID v4 format (8-4-4-4-12 hex) to prevent header @@ -326,13 +209,6 @@ async fn post_mcp( headers: HeaderMap, Json(request): Json, ) -> impl IntoResponse { - if let Err((status, msg)) = validate_origin(&headers, &state.allowed_origins) { - return (status, HeaderMap::new(), msg.to_string()).into_response(); - } - if let Err((status, msg)) = validate_accept(&headers) { - return (status, HeaderMap::new(), msg.to_string()).into_response(); - } - // Auth check if let Err((status, msg)) = validate_auth(&headers, &state.auth_token) { return (status, HeaderMap::new(), msg.to_string()).into_response(); @@ -359,7 +235,6 @@ async fn post_mcp( let session = Arc::new(Mutex::new(Session { server, last_active: Instant::now(), - protocol_version: crate::protocol::types::MCP_VERSION.to_string(), })); // Handle the initialize request @@ -368,18 +243,12 @@ async fn post_mcp( sess.server.handle_request(request).await }; + // Insert session while still holding write lock — atomic check-and-insert + sessions.insert(session_id.clone(), session); + drop(sessions); + match response { Some(resp) => { - let Some(protocol_version) = response_protocol_version(&resp) else { - drop(sessions); - return (StatusCode::OK, HeaderMap::new(), Json(resp)).into_response(); - }; - { - let mut sess = session.lock().await; - sess.protocol_version = protocol_version.clone(); - } - sessions.insert(session_id.clone(), session); - drop(sessions); let mut resp_headers = HeaderMap::new(); resp_headers.insert( "mcp-session-id", @@ -387,14 +256,18 @@ async fn post_mcp( .parse() .unwrap_or_else(|_| axum::http::HeaderValue::from_static("invalid")), ); - if let Ok(value) = HeaderValue::from_str(&protocol_version) { - resp_headers.insert("mcp-protocol-version", value); - } (StatusCode::OK, resp_headers, Json(resp)).into_response() } None => { - drop(sessions); - (StatusCode::ACCEPTED, HeaderMap::new()).into_response() + // Notifications return 202 + let mut resp_headers = HeaderMap::new(); + resp_headers.insert( + "mcp-session-id", + session_id + .parse() + .unwrap_or_else(|_| axum::http::HeaderValue::from_static("invalid")), + ); + (StatusCode::ACCEPTED, resp_headers).into_response() } } } else { @@ -422,14 +295,8 @@ async fn post_mcp( } }; - let session_protocol_version; let response = { let mut sess = session.lock().await; - if let Err((status, msg)) = validate_protocol_version(&headers, &sess.protocol_version) - { - return (status, msg).into_response(); - } - session_protocol_version = sess.protocol_version.clone(); sess.last_active = Instant::now(); sess.server.handle_request(request).await }; @@ -441,9 +308,6 @@ async fn post_mcp( .parse() .unwrap_or_else(|_| axum::http::HeaderValue::from_static("invalid")), ); - if let Ok(value) = HeaderValue::from_str(&session_protocol_version) { - resp_headers.insert("mcp-protocol-version", value); - } match response { Some(resp) => (StatusCode::OK, resp_headers, Json(resp)).into_response(), @@ -457,9 +321,6 @@ async fn delete_mcp( State(state): State, headers: HeaderMap, ) -> impl IntoResponse { - if let Err((status, msg)) = validate_origin(&headers, &state.allowed_origins) { - return (status, msg).into_response(); - } if let Err((status, msg)) = validate_auth(&headers, &state.auth_token) { return (status, msg).into_response(); } @@ -475,22 +336,6 @@ async fn delete_mcp( } }; - let session = { - let sessions = state.sessions.read().await; - sessions.get(&session_id).cloned() - }; - let Some(session) = session else { - return (StatusCode::NOT_FOUND, "Session not found").into_response(); - }; - - let protocol_version = { - let sess = session.lock().await; - sess.protocol_version.clone() - }; - if let Err((status, msg)) = validate_protocol_version(&headers, &protocol_version) { - return (status, msg).into_response(); - } - let mut sessions = state.sessions.write().await; if sessions.remove(&session_id).is_some() { info!("Session {} deleted via DELETE /mcp", &session_id[..8]); @@ -499,79 +344,3 @@ async fn delete_mcp( (StatusCode::NOT_FOUND, "Session not found").into_response() } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn origin_validation_allows_absent_and_configured_origin() { - let allowed = vec!["http://127.0.0.1:3928".to_string()]; - let mut headers = HeaderMap::new(); - assert!(validate_origin(&headers, &allowed).is_ok()); - - headers.insert( - header::ORIGIN, - HeaderValue::from_static("http://127.0.0.1:3928"), - ); - assert!(validate_origin(&headers, &allowed).is_ok()); - - headers.insert( - header::ORIGIN, - HeaderValue::from_static("http://evil.example"), - ); - assert_eq!( - validate_origin(&headers, &allowed).unwrap_err().0, - StatusCode::FORBIDDEN - ); - } - - #[test] - fn accept_validation_rejects_incompatible_clients() { - let mut headers = HeaderMap::new(); - assert_eq!( - validate_accept(&headers).unwrap_err().0, - StatusCode::NOT_ACCEPTABLE - ); - - headers.insert( - header::ACCEPT, - HeaderValue::from_static("application/json, text/event-stream"), - ); - assert!(validate_accept(&headers).is_ok()); - - headers.insert(header::ACCEPT, HeaderValue::from_static("application/json")); - assert_eq!( - validate_accept(&headers).unwrap_err().0, - StatusCode::NOT_ACCEPTABLE - ); - } - - #[test] - fn protocol_header_must_match_session_when_present() { - let mut headers = HeaderMap::new(); - assert_eq!( - validate_protocol_version(&headers, "2025-11-25") - .unwrap_err() - .0, - StatusCode::BAD_REQUEST - ); - - headers.insert( - "mcp-protocol-version", - HeaderValue::from_static("2025-11-25"), - ); - assert!(validate_protocol_version(&headers, "2025-11-25").is_ok()); - - headers.insert( - "mcp-protocol-version", - HeaderValue::from_static("2024-11-05"), - ); - assert_eq!( - validate_protocol_version(&headers, "2025-11-25") - .unwrap_err() - .0, - StatusCode::BAD_REQUEST - ); - } -} diff --git a/crates/vestige-mcp/src/protocol/messages.rs b/crates/vestige-mcp/src/protocol/messages.rs index 8f7e459..b4d6fe1 100644 --- a/crates/vestige-mcp/src/protocol/messages.rs +++ b/crates/vestige-mcp/src/protocol/messages.rs @@ -82,25 +82,13 @@ pub struct ServerCapabilities { // ============================================================================ /// Tool description for tools/list -#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ToolDescription { pub name: String, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, pub input_schema: Value, - /// Per-tool `_meta` annotations from the MCP wire spec. - /// - /// Notable keys recognized by Claude Code (v2.1.91+): - /// - `anthropic/maxResultSizeChars` (integer, up to 500_000): - /// per-tool override of the 50K default `CallToolResult` truncation - /// ceiling. Pinned on the Tool definition; applies to every invocation. - /// - /// Free-form `serde_json::Value` (typically an object) so additional - /// vendor-specific `_meta` keys can be added without further schema - /// changes. - #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, } /// Result of tools/list @@ -125,8 +113,6 @@ pub struct CallToolRequest { pub struct CallToolResult { pub content: Vec, #[serde(skip_serializing_if = "Option::is_none")] - pub structured_content: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub is_error: Option, } diff --git a/crates/vestige-mcp/src/protocol/stdio.rs b/crates/vestige-mcp/src/protocol/stdio.rs index 12d4706..3d36b3e 100644 --- a/crates/vestige-mcp/src/protocol/stdio.rs +++ b/crates/vestige-mcp/src/protocol/stdio.rs @@ -1,9 +1,10 @@ //! stdio Transport for MCP //! //! Handles JSON-RPC communication over stdin/stdout. -//! v1.9.2: Async tokio I/O with error resilience. +//! v1.9.2: Async tokio I/O with heartbeat and error resilience. use std::io; +use std::time::Duration; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tracing::{debug, error, info, warn}; @@ -13,6 +14,9 @@ use crate::server::McpServer; /// Maximum consecutive I/O errors before giving up const MAX_CONSECUTIVE_ERRORS: u32 = 5; +/// Heartbeat interval — sends a ping notification to keep the connection alive +const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(30); + /// stdio Transport for MCP server pub struct StdioTransport; @@ -21,7 +25,7 @@ impl StdioTransport { Self } - /// Run the MCP server over stdio with error resilience. + /// Run the MCP server over stdio with heartbeat and error resilience pub async fn run(self, mut server: McpServer) -> Result<(), io::Error> { let stdin = tokio::io::stdin(); let stdout = tokio::io::stdout(); @@ -107,10 +111,25 @@ impl StdioTransport { break; } // Brief pause before retrying - tokio::time::sleep(std::time::Duration::from_millis(100)).await; + tokio::time::sleep(Duration::from_millis(100)).await; } } } + _ = tokio::time::sleep(HEARTBEAT_INTERVAL) => { + // Send a heartbeat ping notification to keep the connection alive + let ping = "{\"jsonrpc\":\"2.0\",\"method\":\"notifications/ping\"}\n"; + if let Err(e) = stdout.write_all(ping.as_bytes()).await { + warn!("Failed to send heartbeat ping: {}", e); + consecutive_errors += 1; + if consecutive_errors >= MAX_CONSECUTIVE_ERRORS { + error!("Too many consecutive errors, shutting down"); + break; + } + } else { + let _ = stdout.flush().await; + debug!("Heartbeat ping sent"); + } + } } } diff --git a/crates/vestige-mcp/src/protocol/types.rs b/crates/vestige-mcp/src/protocol/types.rs index fa489dc..57f5a10 100644 --- a/crates/vestige-mcp/src/protocol/types.rs +++ b/crates/vestige-mcp/src/protocol/types.rs @@ -114,10 +114,6 @@ impl JsonRpcError { Self::new(ErrorCode::MethodNotFound, message) } - pub fn invalid_request(message: &str) -> Self { - Self::new(ErrorCode::InvalidRequest, message) - } - pub fn invalid_params(message: &str) -> Self { Self::new(ErrorCode::InvalidParams, message) } diff --git a/crates/vestige-mcp/src/resources/mod.rs b/crates/vestige-mcp/src/resources/mod.rs index 63b728f..e021c06 100644 --- a/crates/vestige-mcp/src/resources/mod.rs +++ b/crates/vestige-mcp/src/resources/mod.rs @@ -4,4 +4,3 @@ pub mod codebase; pub mod memory; -pub mod trace; diff --git a/crates/vestige-mcp/src/resources/trace.rs b/crates/vestige-mcp/src/resources/trace.rs deleted file mode 100644 index e6f0e35..0000000 --- a/crates/vestige-mcp/src/resources/trace.rs +++ /dev/null @@ -1,103 +0,0 @@ -//! Agent Black Box Resources -//! -//! `trace://` URI scheme — exposes replayable agent-run traces as MCP resources -//! so a coding agent can read its *own* black box back. This closes the trace -//! correlation spine on the MCP side: the same `runId` an agent received in a -//! tool result's `traceUri` resolves here to the full event timeline. -//! -//! - `trace://{runId}` — the full ordered event log for a run. -//! - `trace://{runId}/summary` — just the roll-up counts. -//! - `trace://runs` — recent runs (the run picker). -//! - `trace://latest` — the most recently active run's full trace. - -use std::sync::Arc; - -use vestige_core::Storage; - -/// Read a `trace://` resource. -pub async fn read(storage: &Arc, uri: &str) -> Result { - let path = uri.strip_prefix("trace://").unwrap_or(""); - let (path, _query) = match path.split_once('?') { - Some((p, q)) => (p, Some(q)), - None => (path, None), - }; - - match path { - "" | "runs" => read_runs(storage).await, - "latest" => read_latest(storage).await, - other => { - if let Some(run_id) = other.strip_suffix("/summary") { - read_summary(storage, run_id).await - } else { - read_run(storage, other).await - } - } - } -} - -async fn read_runs(storage: &Arc) -> Result { - let runs = storage.list_agent_runs(50).map_err(|e| e.to_string())?; - let json: Vec<_> = runs - .into_iter() - .map(|r| { - serde_json::json!({ - "runId": r.run_id, - "firstTool": r.first_tool, - "eventCount": r.event_count, - "retrievedCount": r.retrieved_count, - "suppressedCount": r.suppressed_count, - "writeCount": r.write_count, - "vetoCount": r.veto_count, - "startedAt": r.started_at, - "lastAt": r.last_at, - }) - }) - .collect(); - serde_json::to_string_pretty(&serde_json::json!({ "runs": json })) - .map_err(|e| e.to_string()) -} - -async fn read_latest(storage: &Arc) -> Result { - let runs = storage.list_agent_runs(1).map_err(|e| e.to_string())?; - let run = runs - .into_iter() - .next() - .ok_or_else(|| "No agent runs recorded yet".to_string())?; - read_run(storage, &run.run_id).await -} - -async fn read_run(storage: &Arc, run_id: &str) -> Result { - let events = storage.get_trace(run_id).map_err(|e| e.to_string())?; - if events.is_empty() { - return Err(format!("No trace found for run: {run_id}")); - } - let summary = storage.get_agent_run(run_id).ok().flatten(); - let body = serde_json::json!({ - "runId": run_id, - "summary": summary.map(summary_json), - "events": events, - }); - serde_json::to_string_pretty(&body).map_err(|e| e.to_string()) -} - -async fn read_summary(storage: &Arc, run_id: &str) -> Result { - let summary = storage - .get_agent_run(run_id) - .map_err(|e| e.to_string())? - .ok_or_else(|| format!("No run: {run_id}"))?; - serde_json::to_string_pretty(&summary_json(summary)).map_err(|e| e.to_string()) -} - -fn summary_json(s: vestige_core::AgentRunSummary) -> serde_json::Value { - serde_json::json!({ - "runId": s.run_id, - "firstTool": s.first_tool, - "eventCount": s.event_count, - "retrievedCount": s.retrieved_count, - "suppressedCount": s.suppressed_count, - "writeCount": s.write_count, - "vetoCount": s.veto_count, - "startedAt": s.started_at, - "lastAt": s.last_at, - }) -} diff --git a/crates/vestige-mcp/src/server.rs b/crates/vestige-mcp/src/server.rs index b47d561..00c2bdd 100644 --- a/crates/vestige-mcp/src/server.rs +++ b/crates/vestige-mcp/src/server.rs @@ -20,7 +20,7 @@ use crate::protocol::messages::{ use crate::protocol::types::{JsonRpcError, JsonRpcRequest, JsonRpcResponse, MCP_VERSION}; use crate::resources; use crate::tools; -use vestige_core::{OutputConfig, Storage, VestigeConfig}; +use vestige_core::Storage; /// Build the MCP `instructions` string injected into every connecting client's /// system prompt. @@ -31,9 +31,11 @@ use vestige_core::{OutputConfig, Storage, VestigeConfig}; /// 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. It can misfire -/// on trivial retrievals for a general audience, so it is opt-in via -/// `VESTIGE_SYSTEM_PROMPT_MODE=full`. +/// Composing / Never-composed / Recommendation response shape, 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 { @@ -43,7 +45,7 @@ fn build_instructions() -> String { Every retrieval MUST be composed into a recommendation, never summarized.\ \n\nCOMPOSITION MANDATE: When you receive memories from search, deep_reference, \ cross_reference, or explore_connections, your response MUST follow this shape. \ - (a) Composing: [memory IDs], followed by a brief composition rationale \ + (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.' \ @@ -64,10 +66,6 @@ fn build_instructions() -> String { } } -fn supported_protocol_versions() -> &'static [&'static str] { - &["2024-11-05", "2025-03-26", "2025-06-18", MCP_VERSION] -} - /// MCP Server implementation pub struct McpServer { storage: Arc, @@ -77,31 +75,17 @@ pub struct McpServer { tool_call_count: AtomicU64, /// Optional event broadcast channel for dashboard real-time updates. event_tx: Option>, - /// Resolved output config from `/vestige.toml` (Phase 2). Tools - /// use it as the fallback for detail/limit when no explicit MCP param is - /// given; explicit params always win. - output_config: Arc, -} - -/// Load `vestige.toml` from the storage's data directory and resolve it to an -/// effective [`OutputConfig`]. A missing/malformed file yields the built-in -/// default, which preserves historical behavior. -fn load_output_config(storage: &Arc) -> Arc { - let config = VestigeConfig::load_from_data_dir(storage.data_dir()); - Arc::new(config.output()) } impl McpServer { #[allow(dead_code)] pub fn new(storage: Arc, cognitive: Arc>) -> Self { - let output_config = load_output_config(&storage); Self { storage, cognitive, initialized: false, tool_call_count: AtomicU64::new(0), event_tx: None, - output_config, } } @@ -111,14 +95,12 @@ impl McpServer { cognitive: Arc>, event_tx: broadcast::Sender, ) -> Self { - let output_config = load_output_config(&storage); Self { storage, cognitive, initialized: false, tool_call_count: AtomicU64::new(0), event_tx: Some(event_tx), - output_config, } } @@ -133,13 +115,6 @@ impl McpServer { pub async fn handle_request(&mut self, request: JsonRpcRequest) -> Option { debug!("Handling request: {}", request.method); - if request.id.is_none() { - if request.method != "notifications/initialized" { - debug!("Dropping JSON-RPC notification '{}'", request.method); - } - return None; - } - // Check initialization for non-initialize requests if !self.initialized && request.method != "initialize" @@ -157,9 +132,10 @@ impl McpServer { let result = match request.method.as_str() { "initialize" => self.handle_initialize(request.params).await, - "notifications/initialized" => Err(JsonRpcError::invalid_request( - "notifications/initialized must be sent without an id", - )), + "notifications/initialized" => { + // Notification, no response needed + return None; + } "tools/list" => self.handle_tools_list().await, "tools/call" => self.handle_tools_call(request.params).await, "resources/list" => self.handle_resources_list().await, @@ -185,27 +161,20 @@ impl McpServer { let request: InitializeRequest = match params { Some(p) => serde_json::from_value(p) .map_err(|e| JsonRpcError::invalid_params(&e.to_string()))?, - None => { - return Err(JsonRpcError::invalid_params( - "initialize params are required", - )); - } + None => InitializeRequest::default(), }; - let negotiated_version = - if supported_protocol_versions().contains(&request.protocol_version.as_str()) { - info!( - "Client requested supported protocol version {}, using it", - request.protocol_version - ); - request.protocol_version.clone() - } else { - info!( - "Client requested unsupported protocol version {}, using {}", - request.protocol_version, MCP_VERSION - ); - MCP_VERSION.to_string() - }; + // Version negotiation: use client's version if older than server's + // Claude Desktop rejects servers with newer protocol versions + let negotiated_version = if request.protocol_version.as_str() < MCP_VERSION { + info!( + "Client requested older protocol version {}, using it", + request.protocol_version + ); + request.protocol_version.clone() + } else { + MCP_VERSION.to_string() + }; self.initialized = true; info!( @@ -240,42 +209,32 @@ impl McpServer { /// Handle tools/list request async fn handle_tools_list(&self) -> Result { - // v2.2: 12 advertised tools after Layer-1 Tool Consolidation - // (verified by `tools.len() == 12` in test_tools_list_returns_all_tools). - // 22 deprecated/folded names still work as hidden redirects in - // handle_tools_call. See docs/launch/tool-consolidation-v2.2.0.md. - let mut tools = vec![ - // ================================================================ - // RECALL — unified retrieval tool (v2.2). HOT PATH. - // Folds search + deep_reference + cross_reference + contradictions. - // mode='lookup' (default) is a zero-overhead pass-through to search. - // ================================================================ - ToolDescription { - name: "recall".to_string(), - description: Some("Retrieve from memory. Modes: 'lookup' (default — fast hybrid search: keyword + semantic + convex fusion, auto-strengthens on access; use for plain recall), 'reason' (deep cognitive reasoning across memories with FSRS-6 trust scoring, spreading activation, supersession, and contradiction analysis; use when accuracy matters, needs 'query'), 'contradictions' (surface trust-weighted disagreement pairs for a 'topic'). Default mode is fast — only 'reason' pays the deep-analysis cost.".to_string()), - input_schema: tools::recall::schema(), - ..Default::default() - }, + // 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+) // ================================================================ + ToolDescription { + name: "search".to_string(), + description: Some("Unified search tool. Uses hybrid search (keyword + semantic + convex combination fusion) internally. Auto-strengthens memories on access (Testing Effect).".to_string()), + input_schema: tools::search_unified::schema(), + }, ToolDescription { name: "memory".to_string(), - description: Some("Unified memory management tool. Actions: 'get' (retrieve full node), 'purge' (irreversibly remove content/embeddings with confirm=true), 'delete' (legacy alias for purge), 'state' (get accessibility state), 'promote' (thumbs up — increases retrieval strength), 'demote' (thumbs down — decreases retrieval strength, does NOT delete), 'edit' (update content in-place, preserves FSRS state).".to_string()), + description: Some("Unified memory management tool. Actions: 'get' (retrieve full node), 'delete' (remove memory), 'state' (get accessibility state), 'promote' (thumbs up — increases retrieval strength), 'demote' (thumbs down — decreases retrieval strength, does NOT delete), 'edit' (update content in-place, preserves FSRS state).".to_string()), input_schema: tools::memory_unified::schema(), - ..Default::default() }, ToolDescription { name: "codebase".to_string(), description: Some("Unified codebase tool. Actions: 'remember_pattern' (store code pattern), 'remember_decision' (store architectural decision), 'get_context' (retrieve patterns and decisions).".to_string()), input_schema: tools::codebase_unified::schema(), - ..Default::default() }, ToolDescription { name: "intention".to_string(), description: Some("Unified intention management tool. Actions: 'set' (create), 'check' (find triggered), 'update' (complete/snooze/cancel), 'list' (show intentions).".to_string()), input_schema: tools::intention_unified::schema(), - ..Default::default() }, // ================================================================ // CORE MEMORY (v1.7: smart_ingest absorbs ingest + checkpoint) @@ -284,88 +243,121 @@ impl McpServer { name: "smart_ingest".to_string(), description: Some("INTELLIGENT memory ingestion with Prediction Error Gating. Single mode: provide 'content' to auto-decide CREATE/UPDATE/SUPERSEDE. Batch mode: provide 'items' array (max 20) for session-end saves — each item runs the full cognitive pipeline (importance scoring, intent detection, synaptic tagging).".to_string()), input_schema: tools::smart_ingest::schema(), - ..Default::default() }, // ================================================================ - // EXTERNAL-SOURCE CONNECTORS (#57) + // TEMPORAL TOOLS (v1.2+) // ================================================================ ToolDescription { - name: "source_sync".to_string(), - description: Some("Index an external system into Vestige as a durable, offline, semantically-searchable index that cites back to the canonical record. GitHub: source='github', repo='owner/name' (auth via GITHUB_TOKEN env). Redmine: source='redmine', project='' (host via REDMINE_URL, auth via REDMINE_API_KEY env). Idempotent: re-running updates changed issues without duplicating; set reconcile=true to tombstone issues removed upstream.".to_string()), - input_schema: tools::source_sync::schema(), - ..Default::default() + name: "memory_timeline".to_string(), + description: Some("Browse memories chronologically. Returns memories in a time range, grouped by day. Defaults to last 7 days.".to_string()), + input_schema: tools::timeline::schema(), + }, + ToolDescription { + name: "memory_changelog".to_string(), + description: Some("View audit trail of memory changes. Per-memory: state transitions. System-wide: consolidations + recent state changes.".to_string()), + input_schema: tools::changelog::schema(), }, // ================================================================ - // STATUS / TEMPORAL — unified `memory_status` tool (v2.2) - // Folds system_status + memory_health + memory_timeline + - // memory_changelog into one view-dispatched surface. + // MAINTENANCE TOOLS (v1.7: system_status replaces health_check + stats) // ================================================================ ToolDescription { - name: "memory_status".to_string(), - description: Some("Memory status & history. Views: 'health' (default — full system health + stats + FSRS preview + cognitive-module health + warnings + recommendations), 'retention' (lightweight retention dashboard: avg, distribution, trend), 'timeline' (browse memories chronologically, grouped by day), 'changelog' (audit trail of memory state changes — per-memory transitions or system-wide).".to_string()), - input_schema: tools::memory_status::schema(), - ..Default::default() + name: "system_status".to_string(), + description: Some("Combined system health and statistics. Returns status (healthy/degraded/critical/empty), full stats, FSRS preview, cognitive module health, state distribution, warnings, and recommendations.".to_string()), + input_schema: tools::maintenance::system_status_schema(), + }, + ToolDescription { + name: "consolidate".to_string(), + description: Some("Run FSRS-6 memory consolidation cycle. Applies decay, generates embeddings, and performs maintenance. Use when memories seem stale.".to_string()), + input_schema: tools::maintenance::consolidate_schema(), + }, + ToolDescription { + name: "backup".to_string(), + description: Some("Create a SQLite database backup. Returns the backup file path.".to_string()), + input_schema: tools::maintenance::backup_schema(), + }, + ToolDescription { + name: "export".to_string(), + description: Some("Export memories as JSON or JSONL. Supports tag and date filters.".to_string()), + input_schema: tools::maintenance::export_schema(), + }, + ToolDescription { + name: "gc".to_string(), + description: Some("Garbage collect stale memories below retention threshold. Defaults to dry_run=true for safety.".to_string()), + input_schema: tools::maintenance::gc_schema(), }, // ================================================================ - // MAINTAIN — unified maintenance/lifecycle tool (v2.2) - // Folds consolidate + dream + gc + importance_score + backup + - // export + restore into one action-dispatched surface. + // AUTO-SAVE & DEDUP TOOLS (v1.3+) // ================================================================ ToolDescription { - name: "maintain".to_string(), - description: Some("Memory maintenance & lifecycle. Actions: 'consolidate' (run FSRS-6 decay/embedding cycle), 'dream' (replay memories → insights/connections + strengthen patterns), 'gc' (garbage-collect stale memories; dry_run=true by default for safety), 'importance_score' (4-channel neuroscience score for 'content'), 'backup' (SQLite DB backup), 'export' (memories as JSON/JSONL with tag/date filters), 'restore' (restore from a JSON backup at 'path').".to_string()), - input_schema: tools::maintain::schema(), - ..Default::default() + name: "importance_score".to_string(), + description: Some("Score content importance using 4-channel neuroscience model (novelty/arousal/reward/attention). Returns composite score, channel breakdown, encoding boost, and explanations.".to_string()), + input_schema: tools::importance::schema(), }, - // ================================================================ - // DEDUP / MERGE / SUPERSEDE — unified `dedup` tool (v2.2) - // Folds find_duplicates + the 7 Phase-3 merge tools into one - // action-dispatched surface. Diff-previewed, confidence-gated, - // reversible, never silent; bitemporal-never-delete preserved. - // ================================================================ ToolDescription { - name: "dedup".to_string(), - description: Some("Deduplication & merge/supersede. Actions: 'scan' (default — surface duplicate clusters via cosine + merge candidates via Fellegi-Sunter, read-only), 'plan_merge' (preview a reversible merge plan for 2+ member_ids → plan_id), 'plan_supersede' (preview superseding old_id with new_id → plan_id), 'apply' (execute a plan_id; 'possible'/'non_match' need confirm=true), 'undo' (reverse an operation_id, or omit to list the reflog), 'protect' (pin a memory against auto-merge/supersede/forget), 'policy' (get/set Fellegi-Sunter thresholds). Old memories are invalidated, never deleted.".to_string()), - input_schema: tools::dedup::unified_schema(), - ..Default::default() + name: "find_duplicates".to_string(), + description: Some("Find duplicate and near-duplicate memory clusters using cosine similarity on embeddings. Returns clusters with suggested actions (merge/review). Use to clean up redundant memories.".to_string()), + input_schema: tools::dedup::schema(), }, // ================================================================ // COGNITIVE TOOLS (v1.5+) - // (dream folded into `maintain` action='dream' in v2.2) - // ================================================================ - // ================================================================ - // GRAPH — unified graph/association/prediction tool (v2.2) - // Folds explore_connections + predict + memory_graph + composed_graph. // ================================================================ ToolDescription { - name: "graph".to_string(), - description: Some("Memory graph & associations. Actions: 'chain' (reasoning path from→to), 'associations' (related memories via spreading activation, needs 'from'), 'bridges' (connectors between from/to), 'predict' (what memories you'll need next, from 'context'), 'memory_graph' (force-directed subgraph for viz, from center_id or query), 'recent'/'get'/'memory'/'neighbors'/'never_composed'/'bounty_mode' (composition topology), 'label' (record a composition outcome — the only write).".to_string()), - input_schema: tools::graph_unified::schema(), - ..Default::default() + name: "dream".to_string(), + description: Some("Trigger memory dreaming — replays recent memories to discover hidden connections, synthesize insights, and strengthen important patterns. Returns insights, connections, and dream stats.".to_string()), + input_schema: tools::dream::schema(), + }, + ToolDescription { + name: "explore_connections".to_string(), + description: Some("Graph exploration tool for memory connections. Actions: 'chain' (build reasoning path between memories), 'associations' (find related memories via spreading activation + hippocampal index), 'bridges' (find connecting memories between two nodes).".to_string()), + input_schema: tools::explore::schema(), + }, + ToolDescription { + name: "predict".to_string(), + description: Some("Proactive memory prediction — predicts what memories you'll need next based on context, recent activity, and learned patterns. Returns predictions, suggestions, and speculative retrievals.".to_string()), + input_schema: tools::predict::schema(), }, // ================================================================ // RESTORE TOOL (v1.5+) - // (folded into `maintain` action='restore' in v2.2) // ================================================================ + ToolDescription { + name: "restore".to_string(), + description: Some("Restore memories from a JSON backup file. Supports MCP wrapper format, RecallResult format, and direct memory array format.".to_string()), + input_schema: tools::restore::schema(), + }, // ================================================================ // CONTEXT PACKETS (v1.8+) // ================================================================ ToolDescription { - name: "session_start".to_string(), - description: Some("One-call session initialization. Combines search, intentions, status, predictions, and codebase context into a single token-budgeted response. Call this once at the start of a session instead of 5 separate calls. (Renamed from 'session_context' in v2.2.)".to_string()), + name: "session_context".to_string(), + description: Some("One-call session initialization. Combines search, intentions, status, predictions, and codebase context into a single token-budgeted response. Replaces 5 separate calls at session start.".to_string()), input_schema: tools::session_context::schema(), - ..Default::default() }, // ================================================================ // AUTONOMIC TOOLS (v1.9+) - // (memory_health → `memory_status` view='retention'; - // memory_graph + composed_graph → `graph`, all in v2.2) // ================================================================ + ToolDescription { + name: "memory_health".to_string(), + description: Some("Retention dashboard. Returns avg retention, retention distribution (buckets: 0-20%, 20-40%, etc.), trend (improving/declining/stable), and recommendation. Lightweight alternative to full system_status focused on memory quality.".to_string()), + input_schema: tools::health::schema(), + }, + ToolDescription { + name: "memory_graph".to_string(), + description: Some("Subgraph export for visualization. Input: center_id or query, depth (1-3), max_nodes. Returns nodes with force-directed layout positions and edges with weights. Powers memory graph visualization.".to_string()), + input_schema: tools::graph::schema(), + }, // ================================================================ - // DEEP REFERENCE (v2.0.4+) — folded into `recall` (mode='reason' / - // 'contradictions') in v2.2. deep_reference/cross_reference/ - // contradictions remain hidden dispatch aliases. + // 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 @@ -374,68 +366,9 @@ impl McpServer { name: "suppress".to_string(), description: Some("Actively suppress a memory via top-down inhibitory control (Anderson 2025 SIF + Davis Rac1). Distinct from delete: the memory persists but is inhibited from retrieval and actively decays. Each call compounds. A background Rac1 worker cascades decay to co-activated neighbors. Reversible within 24 hours via reverse=true.".to_string()), input_schema: tools::suppress::schema(), - ..Default::default() - }, - // ================================================================ - // RETROACTIVE SALIENCE BACKFILL — Cai 2024 Nature - // "Memory with hindsight": failure -> backward causal reach. - // A flagship v2.2 capability, kept as its own advertised tool — it - // is a distinct cognitive primitive (backward causal promotion), - // not a maintenance op that folds into `maintain`. - // ================================================================ - ToolDescription { - name: "backfill".to_string(), - description: Some("Memory with hindsight. When a FAILURE (bug/crash/regression) is recorded, reach BACKWARD in time and promote the quiet earlier memory that caused it — the root cause a vector search structurally cannot surface because it isn't similar to the failure, only causally upstream (shares an entity: same file/env-var/service). Faithful port of Cai 2024 Nature; backward-only by construction. Pass failure_id (or it auto-finds the latest failure), manual=true to force, promote=false for a dry run.".to_string()), - input_schema: tools::backfill::schema(), - ..Default::default() }, ]; - // Per-tool result-size annotation `_meta["anthropic/maxResultSizeChars"]`. - // - // Claude Code v2.1.91+ honors this annotation to override its 50K default - // `CallToolResult` truncation. Without it, large Vestige payloads - // (`search` with `detail_level="full"` at `limit=20` has been observed - // at ~135K chars; `memory_timeline` at `limit=30` at ~84K chars) are - // silently truncated and spilled to disk, forcing the parent agent to - // chunk-read them. - // - // Per-tool caps below are sized at ~2× observed peak with growth - // headroom; max permitted by Anthropic is 500_000. Only the - // high-payload tools carry the annotation (recall, memory_status, - // memory, codebase, dedup, graph); the remaining advertised tools - // deliberately do NOT (cargo-cult prevention — annotating a - // small-payload tool dilutes the signal). - // - // Other tools that COULD plausibly grow into the annotated set with - // future workload (`deep_reference`, `cross_reference`, `memory_graph`, - // `explore_connections`, `session_context`) are left unannotated until - // empirical measurement shows truncation under realistic use. - for tool in tools.iter_mut() { - let max_chars: Option = match tool.name.as_str() { - // v2.2: search folded into recall (mode='lookup'); annotation moved. - "recall" => Some(300_000), - "memory_status" => Some(200_000), - "memory" => Some(100_000), - "codebase" => Some(100_000), - // v2.2: dedup action='scan' returns duplicate clusters + - // merge candidates + policy in one payload. - "dedup" => Some(150_000), - // v2.2: graph action='memory_graph' (force-directed layout) and - // 'bounty_mode' pagination can both produce large payloads. - "graph" => Some(250_000), - _ => None, - }; - if let Some(n) = max_chars { - let mut meta = serde_json::Map::new(); - meta.insert( - "anthropic/maxResultSizeChars".to_string(), - serde_json::Value::from(n), - ); - tool.meta = Some(serde_json::Value::Object(meta)); - } - } - let result = ListToolsResult { tools }; serde_json::to_value(result).map_err(|e| JsonRpcError::internal_error(&e.to_string())) } @@ -450,13 +383,6 @@ impl McpServer { .map_err(|e| JsonRpcError::invalid_params(&e.to_string()))?, None => return Err(JsonRpcError::invalid_params("Missing tool call parameters")), }; - if let Some(arguments) = &request.arguments - && !arguments.is_object() - { - return Err(JsonRpcError::invalid_params( - "tools/call arguments must be an object", - )); - } // Record activity on every tool call (non-blocking) if let Ok(mut cog) = self.cognitive.try_lock() { @@ -464,64 +390,28 @@ impl McpServer { cog.consolidation_scheduler.record_activity(); } - // Capture the args for tracing/event emission BEFORE tool dispatch - // consumes request.arguments. The Black Box must populate even in pure - // stdio mode (no dashboard socket), so this is NOT gated on event_tx — - // only the WebSocket broadcast inside record()/emit is. - let saved_args = request.arguments.clone(); - - // Agent Black Box: record the opening mcp.call event for this tool - // invocation. run_id groups the events of one agent turn; it is derived - // from the args (an explicit runId, or a fresh id) so record_result below - // can attach the downstream memory events (retrieve/suppress/veto) to the - // same run. - let trace_run_id = crate::trace_recorder::run_id_for(&saved_args); - crate::trace_recorder::record_call( - &self.storage, - self.event_tx.as_ref(), - &trace_run_id, - &request.name, - &saved_args, - ); + // 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 result = match request.name.as_str() { // ================================================================ // UNIFIED TOOLS (v1.1+) - Preferred API // ================================================================ - // RECALL — unified retrieval tool (v2.2). HOT PATH. - // mode = lookup (default, zero-overhead) | reason | contradictions - "recall" => { - tools::recall::execute( - &self.storage, - &self.cognitive, - &self.output_config, - request.arguments, - ) - .await - } - // DEPRECATED (v2.2): folded into `recall` (mode='lookup'). Hidden alias. "search" => { - warn!("Tool 'search' is deprecated in v2.2. Use 'recall' (mode='lookup', the default)."); - tools::search_unified::execute( - &self.storage, - &self.cognitive, - &self.output_config, - request.arguments, - ) - .await + 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, - &self.output_config, - request.arguments, - ) - .await + tools::codebase_unified::execute(&self.storage, &self.cognitive, request.arguments) + .await } "intention" => { tools::intention_unified::execute(&self.storage, &self.cognitive, request.arguments) @@ -536,16 +426,6 @@ impl McpServer { .await } - // ================================================================ - // External-source connectors (#57) - // ================================================================ - "source_sync" => tools::source_sync::execute(&self.storage, request.arguments).await, - - // ================================================================ - // Retroactive Salience Backfill (Cai 2024 Nature) — flagship v2.2 - // ================================================================ - "backfill" => tools::backfill::execute(&self.storage, request.arguments).await, - // ================================================================ // DEPRECATED (v1.7): ingest → smart_ingest // ================================================================ @@ -625,23 +505,9 @@ impl McpServer { } // ================================================================ - // MEMORY STATUS — unified status/temporal tool (v2.2) - // view = health (default) | retention | timeline | changelog + // SYSTEM STATUS (v1.7: replaces health_check + stats) // ================================================================ - "memory_status" => { - tools::memory_status::execute( - &self.storage, - &self.cognitive, - &self.output_config, - request.arguments, - ) - .await - } - - // DEPRECATED (v2.2): folded into `memory_status`. Hidden aliases — - // each calls the same underlying handler verbatim. "system_status" => { - warn!("Tool 'system_status' is deprecated in v2.2. Use 'memory_status' (view='health')."); tools::maintenance::execute_system_status( &self.storage, &self.cognitive, @@ -653,21 +519,15 @@ impl McpServer { "mark_reviewed" => tools::review::execute(&self.storage, request.arguments).await, // ================================================================ - // DEPRECATED: legacy search aliases — redirect to `recall` lookup. - // ('recall' itself is now the unified retrieval tool, handled above.) + // DEPRECATED: Search tools - redirect to unified 'search' // ================================================================ - "semantic_search" | "hybrid_search" => { + "recall" | "semantic_search" | "hybrid_search" => { warn!( - "Tool '{}' is deprecated. Use 'recall' (mode='lookup') instead.", + "Tool '{}' is deprecated. Use 'search' instead.", request.name ); - tools::search_unified::execute( - &self.storage, - &self.cognitive, - &self.output_config, - request.arguments, - ) - .await + tools::search_unified::execute(&self.storage, &self.cognitive, request.arguments) + .await } // ================================================================ @@ -691,19 +551,14 @@ impl McpServer { } "delete_knowledge" => { warn!( - "Tool 'delete_knowledge' is deprecated. Use 'memory' with action='purge', confirm=true instead." + "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); - let confirm = args - .get("confirm") - .cloned() - .unwrap_or(serde_json::Value::Bool(false)); Some(serde_json::json!({ "action": "delete", - "id": id, - "confirm": confirm + "id": id })) } None => None, @@ -747,13 +602,7 @@ impl McpServer { } None => Some(serde_json::json!({"action": "remember_pattern"})), }; - tools::codebase_unified::execute( - &self.storage, - &self.cognitive, - &self.output_config, - unified_args, - ) - .await + tools::codebase_unified::execute(&self.storage, &self.cognitive, unified_args).await } "remember_decision" => { warn!( @@ -772,13 +621,7 @@ impl McpServer { } None => Some(serde_json::json!({"action": "remember_decision"})), }; - tools::codebase_unified::execute( - &self.storage, - &self.cognitive, - &self.output_config, - unified_args, - ) - .await + tools::codebase_unified::execute(&self.storage, &self.cognitive, unified_args).await } "get_codebase_context" => { warn!( @@ -794,13 +637,7 @@ impl McpServer { } None => Some(serde_json::json!({"action": "get_context"})), }; - tools::codebase_unified::execute( - &self.storage, - &self.cognitive, - &self.output_config, - unified_args, - ) - .await + tools::codebase_unified::execute(&self.storage, &self.cognitive, unified_args).await } // ================================================================ @@ -930,115 +767,36 @@ impl McpServer { } // ================================================================ - // TEMPORAL TOOLS (v1.2+) — DEPRECATED (v2.2): folded into - // `memory_status` (view='timeline' / view='changelog'). Hidden aliases. + // TEMPORAL TOOLS (v1.2+) // ================================================================ - "memory_timeline" => { - warn!("Tool 'memory_timeline' is deprecated in v2.2. Use 'memory_status' (view='timeline')."); - tools::timeline::execute(&self.storage, &self.output_config, request.arguments) - .await - } - "memory_changelog" => { - warn!("Tool 'memory_changelog' is deprecated in v2.2. Use 'memory_status' (view='changelog')."); - tools::changelog::execute(&self.storage, request.arguments).await - } + "memory_timeline" => tools::timeline::execute(&self.storage, request.arguments).await, + "memory_changelog" => tools::changelog::execute(&self.storage, request.arguments).await, // ================================================================ - // MAINTAIN — unified maintenance/lifecycle tool (v2.2) - // action = consolidate | dream | gc | importance_score | backup - // | export | restore - // ================================================================ - "maintain" => { - // Mirror the pre-dispatch *Started* events that the standalone - // consolidate/dream arms emit, keyed off the action. - match request - .arguments - .as_ref() - .and_then(|a| a.get("action")) - .and_then(|v| v.as_str()) - { - Some("consolidate") => self.emit(VestigeEvent::ConsolidationStarted { - timestamp: chrono::Utc::now(), - }), - Some("dream") => self.emit(VestigeEvent::DreamStarted { - memory_count: self - .storage - .get_stats() - .map(|s| s.total_nodes as usize) - .unwrap_or(0), - timestamp: chrono::Utc::now(), - }), - _ => {} - } - tools::maintain::execute(&self.storage, &self.cognitive, request.arguments).await - } - - // ================================================================ - // MAINTENANCE TOOLS (v1.2+) — DEPRECATED (v2.2): folded into - // `maintain`. Hidden aliases; pre-emit Started events preserved. + // MAINTENANCE TOOLS (v1.2+, non-deprecated) // ================================================================ "consolidate" => { - warn!("Tool 'consolidate' is deprecated in v2.2. Use 'maintain' (action='consolidate')."); self.emit(VestigeEvent::ConsolidationStarted { timestamp: chrono::Utc::now(), }); tools::maintenance::execute_consolidate(&self.storage, request.arguments).await } - "backup" => { - warn!("Tool 'backup' is deprecated in v2.2. Use 'maintain' (action='backup')."); - tools::maintenance::execute_backup(&self.storage, request.arguments).await - } - "export" => { - warn!("Tool 'export' is deprecated in v2.2. Use 'maintain' (action='export')."); - tools::maintenance::execute_export(&self.storage, request.arguments).await - } - "gc" => { - warn!("Tool 'gc' is deprecated in v2.2. Use 'maintain' (action='gc')."); - tools::maintenance::execute_gc(&self.storage, request.arguments).await - } + "backup" => tools::maintenance::execute_backup(&self.storage, request.arguments).await, + "export" => tools::maintenance::execute_export(&self.storage, request.arguments).await, + "gc" => tools::maintenance::execute_gc(&self.storage, request.arguments).await, // ================================================================ // AUTO-SAVE & DEDUP TOOLS (v1.3+) // ================================================================ - // DEPRECATED (v2.2): folded into `maintain` (action='importance_score'). "importance_score" => { - warn!("Tool 'importance_score' is deprecated in v2.2. Use 'maintain' (action='importance_score')."); tools::importance::execute(&self.storage, &self.cognitive, request.arguments).await } - // ================================================================ - // DEDUP / MERGE / SUPERSEDE — unified `dedup` tool (v2.2) - // ================================================================ - "dedup" => tools::dedup::execute_unified(&self.storage, request.arguments).await, - - // DEPRECATED (v2.2): folded into `dedup`. Kept as hidden back-compat - // aliases (≥1 minor release) — they call the same underlying handlers - // verbatim, so envelopes/plan_id/confirm-gating/bitemporal are intact. - "find_duplicates" => { - warn!("Tool 'find_duplicates' is deprecated in v2.2. Use 'dedup' with action='scan'."); - tools::dedup::execute(&self.storage, request.arguments).await - } - "merge_candidates" | "plan_merge" | "plan_supersede" | "apply_plan" | "merge_undo" - | "protect" | "merge_policy" => { - warn!( - "Tool '{}' is deprecated in v2.2. Use 'dedup' (action={}).", - request.name, - match request.name.as_str() { - "merge_candidates" => "scan", - "apply_plan" => "apply", - "merge_undo" => "undo", - "merge_policy" => "policy", - other => other, - } - ); - tools::merge::execute(&self.storage, request.name.as_str(), request.arguments).await - } + "find_duplicates" => tools::dedup::execute(&self.storage, request.arguments).await, // ================================================================ - // COGNITIVE TOOLS (v1.5+) — DEPRECATED (v2.2): dream folded into - // `maintain` (action='dream'). Hidden alias; DreamStarted preserved. + // COGNITIVE TOOLS (v1.5+) // ================================================================ "dream" => { - warn!("Tool 'dream' is deprecated in v2.2. Use 'maintain' (action='dream')."); self.emit(VestigeEvent::DreamStarted { memory_count: self .storage @@ -1049,79 +807,31 @@ impl McpServer { }); tools::dream::execute(&self.storage, &self.cognitive, request.arguments).await } - // ================================================================ - // GRAPH — unified graph/association/prediction tool (v2.2) - // ================================================================ - "graph" => { - tools::graph_unified::execute(&self.storage, &self.cognitive, request.arguments) - .await - } - // DEPRECATED (v2.2): folded into `graph`. Hidden aliases. "explore_connections" => { - warn!("Tool 'explore_connections' is deprecated in v2.2. Use 'graph' (action='chain'|'associations'|'bridges')."); tools::explore::execute(&self.storage, &self.cognitive, request.arguments).await } "predict" => { - warn!("Tool 'predict' is deprecated in v2.2. Use 'graph' (action='predict')."); tools::predict::execute(&self.storage, &self.cognitive, request.arguments).await } - // DEPRECATED (v2.2): folded into `maintain` (action='restore'). - "restore" => { - warn!("Tool 'restore' is deprecated in v2.2. Use 'maintain' (action='restore')."); - tools::restore::execute(&self.storage, request.arguments).await - } + "restore" => tools::restore::execute(&self.storage, request.arguments).await, // ================================================================ - // CONTEXT PACKETS (v1.8+) — `session_start` (renamed v2.2) + // CONTEXT PACKETS (v1.8+) // ================================================================ - "session_start" => { - tools::session_context::execute( - &self.storage, - &self.cognitive, - &self.output_config, - request.arguments, - ) - .await - } - // DEPRECATED (v2.2): renamed to `session_start`. Hidden alias. "session_context" => { - warn!("Tool 'session_context' is deprecated in v2.2. Use 'session_start'."); - tools::session_context::execute( - &self.storage, - &self.cognitive, - &self.output_config, - request.arguments, - ) - .await + tools::session_context::execute(&self.storage, &self.cognitive, request.arguments) + .await } // ================================================================ // AUTONOMIC TOOLS (v1.9+) // ================================================================ - // DEPRECATED (v2.2): folded into `memory_status` (view='retention'). - "memory_health" => { - warn!("Tool 'memory_health' is deprecated in v2.2. Use 'memory_status' (view='retention')."); - tools::health::execute(&self.storage, request.arguments).await - } - // DEPRECATED (v2.2): folded into `graph`. Hidden aliases. - "memory_graph" => { - warn!("Tool 'memory_graph' is deprecated in v2.2. Use 'graph' (action='memory_graph')."); - tools::graph::execute(&self.storage, request.arguments).await - } - "composed_graph" => { - warn!("Tool 'composed_graph' is deprecated in v2.2. Use 'graph' (action='recent'|'get'|'memory'|'neighbors'|'never_composed'|'bounty_mode'|'label')."); - tools::composed_graph::execute(&self.storage, request.arguments).await - } - // DEPRECATED (v2.2): folded into `recall`. Hidden aliases. + "memory_health" => tools::health::execute(&self.storage, request.arguments).await, + "memory_graph" => tools::graph::execute(&self.storage, request.arguments).await, "deep_reference" | "cross_reference" => { - warn!("Tool '{}' is deprecated in v2.2. Use 'recall' (mode='reason').", request.name); tools::cross_reference::execute(&self.storage, &self.cognitive, request.arguments) .await } - "contradictions" => { - warn!("Tool 'contradictions' is deprecated in v2.2. Use 'recall' (mode='contradictions')."); - tools::contradictions::execute(&self.storage, request.arguments).await - } // ================================================================ // ACTIVE FORGETTING (v2.0.5) — top-down suppression @@ -1129,7 +839,7 @@ impl McpServer { "suppress" => tools::suppress::execute(&self.storage, request.arguments).await, name => { - return Err(JsonRpcError::invalid_params(&format!( + return Err(JsonRpcError::method_not_found_with_message(&format!( "Unknown tool: {}", name ))); @@ -1142,17 +852,6 @@ impl McpServer { // ================================================================ if let Ok(ref content) = result { self.emit_tool_event(&request.name, &saved_args, content); - // Agent Black Box: inspect the successful result and record the - // downstream memory events (retrieve/suppress/veto/dream) under the - // same run_id as the opening mcp.call, so /api/traces, /api/receipts - // and the trace:// resource are actually populated. - crate::trace_recorder::record_result( - &self.storage, - self.event_tx.as_ref(), - &trace_run_id, - &request.name, - content, - ); } let response = match result { @@ -1163,20 +862,17 @@ impl McpServer { text: serde_json::to_string_pretty(&content) .unwrap_or_else(|_| content.to_string()), }], - structured_content: Some(content), is_error: Some(false), }; serde_json::to_value(call_result) .map_err(|e| JsonRpcError::internal_error(&e.to_string())) } Err(e) => { - let error_content = serde_json::json!({ "error": e }); let call_result = CallToolResult { content: vec![crate::protocol::messages::ToolResultContent { content_type: "text".to_string(), - text: error_content.to_string(), + text: serde_json::json!({ "error": e }).to_string(), }], - structured_content: Some(error_content), is_error: Some(true), }; serde_json::to_value(call_result) @@ -1341,15 +1037,7 @@ impl McpServer { serde_json::to_value(result) .map_err(|e| JsonRpcError::internal_error(&e.to_string())) } - Err(e) => { - if e.to_ascii_lowercase().contains("unknown") - || e.to_ascii_lowercase().contains("not found") - { - Err(JsonRpcError::resource_not_found(uri)) - } else { - Err(JsonRpcError::internal_error(&e)) - } - } + Err(e) => Err(JsonRpcError::internal_error(&e)), } } @@ -1365,31 +1053,6 @@ impl McpServer { } let now = Utc::now(); - // v2.2: the unified `maintain` tool folds consolidate/dream/importance_score - // (the three maintenance actions that emit). Normalize its name to the - // effective action so the existing emit arms below fire unchanged. Old - // standalone names still arrive verbatim and match directly. - let tool_name = if tool_name == "maintain" { - args.as_ref() - .and_then(|a| a.get("action")) - .and_then(|v| v.as_str()) - .unwrap_or("maintain") - } else if tool_name == "recall" { - // The unified `recall` tool fires SearchPerformed only for the lookup - // path (the former `search`). reason/contradictions do not emit, so - // map them to a non-emitting name. - match args - .as_ref() - .and_then(|a| a.get("mode")) - .and_then(|v| v.as_str()) - { - Some("reason") | Some("contradictions") => "recall_noemit", - _ => "search", // lookup (default) → SearchPerformed - } - } else { - tool_name - }; - match tool_name { // -- smart_ingest: memory created/updated -- "smart_ingest" | "ingest" | "session_checkpoint" => { @@ -1501,21 +1164,8 @@ impl McpServer { .unwrap_or("") .to_string(); match action { - "delete" | "purge" - if result - .get("success") - .and_then(|value| value.as_bool()) - .unwrap_or(false) => - { - let node_id = result - .get("nodeId") - .and_then(|value| value.as_str()) - .unwrap_or(&id) - .to_string(); - self.emit(VestigeEvent::MemoryDeleted { - id: node_id, - timestamp: now, - }); + "delete" => { + self.emit(VestigeEvent::MemoryDeleted { id, timestamp: now }); } "promote" => { let retention = result @@ -1729,26 +1379,6 @@ mod tests { } } - fn make_notification(method: &str, params: Option) -> JsonRpcRequest { - JsonRpcRequest { - jsonrpc: "2.0".to_string(), - id: None, - method: method.to_string(), - params, - } - } - - fn init_params() -> serde_json::Value { - serde_json::json!({ - "protocolVersion": MCP_VERSION, - "capabilities": {}, - "clientInfo": { - "name": "test-client", - "version": "1.0.0" - } - }) - } - // ======================================================================== // INITIALIZATION TESTS // ======================================================================== @@ -1800,31 +1430,13 @@ mod tests { } #[tokio::test] - async fn test_initialize_unsupported_protocol_falls_back_to_latest() { - let (mut server, _dir) = test_server().await; - let params = serde_json::json!({ - "protocolVersion": "1.0.0", - "capabilities": {}, - "clientInfo": { "name": "test", "version": "1.0" } - }); - let request = make_request("initialize", Some(params)); - - let response = server.handle_request(request).await.unwrap(); - let result = response.result.unwrap(); - - assert_eq!(result["protocolVersion"], MCP_VERSION); - } - - #[tokio::test] - async fn test_initialize_missing_params_returns_error() { + async fn test_initialize_with_default_params() { let (mut server, _dir) = test_server().await; let request = make_request("initialize", None); let response = server.handle_request(request).await.unwrap(); - assert!(response.result.is_none()); - assert!(response.error.is_some()); - assert_eq!(response.error.unwrap().code, -32602); - assert!(!server.initialized); + assert!(response.result.is_some()); + assert!(response.error.is_none()); } // ======================================================================== @@ -1864,39 +1476,17 @@ mod tests { let (mut server, _dir) = test_server().await; // First initialize - let init_request = make_request("initialize", Some(init_params())); + let init_request = make_request("initialize", None); server.handle_request(init_request).await; // Send initialized notification - let notification = make_notification("notifications/initialized", None); + let notification = make_request("notifications/initialized", None); let response = server.handle_request(notification).await; // Notifications should return None assert!(response.is_none()); } - #[tokio::test] - async fn test_initialized_notification_with_id_returns_invalid_request() { - let (mut server, _dir) = test_server().await; - - let request = make_request("notifications/initialized", None); - let response = server.handle_request(request).await.unwrap(); - - assert!(response.error.is_some()); - assert_eq!(response.error.unwrap().code, -32600); - } - - #[tokio::test] - async fn test_notification_does_not_emit_response_or_side_effect() { - let (mut server, _dir) = test_server().await; - - let notification = make_notification("initialize", None); - let response = server.handle_request(notification).await; - - assert!(response.is_none()); - assert!(!server.initialized); - } - // ======================================================================== // TOOLS/LIST TESTS // ======================================================================== @@ -1906,7 +1496,7 @@ mod tests { let (mut server, _dir) = test_server().await; // Initialize first - let init_request = make_request("initialize", Some(init_params())); + let init_request = make_request("initialize", None); server.handle_request(init_request).await; let request = make_request("tools/list", None); @@ -1915,35 +1505,19 @@ mod tests { let result = response.result.unwrap(); let tools = result["tools"].as_array().unwrap(); - // v2.2 Tool Consolidation (Layer 1): 34 → 27 after `dedup` folds - // find_duplicates + the 7 Phase-3 merge tools (8 → 1). Old names remain - // dispatchable as hidden back-compat aliases but drop off the advertised list. - assert_eq!( - tools.len(), - 13, - "Expected exactly 13 tools after v2.2 Layer-1 consolidation \ - (12 consolidated: dedup + memory_status + graph + maintain + recall; \ - session_context renamed) plus the flagship `backfill` (Retroactive \ - Salience, Cai 2024 Nature), a distinct cognitive primitive" - ); + // 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(); // Unified tools - // (search folded into `recall` mode='lookup' in v2.2) - assert!(tool_names.contains(&"recall")); + assert!(tool_names.contains(&"search")); assert!(tool_names.contains(&"memory")); assert!(tool_names.contains(&"codebase")); assert!(tool_names.contains(&"intention")); - // Flagship retroactive-salience backfill stays advertised (not folded). - assert!(tool_names.contains(&"backfill")); - // Core memory (smart_ingest absorbs ingest + checkpoint in v1.7) assert!(tool_names.contains(&"smart_ingest")); - - // External-source connectors (#57) - assert!(tool_names.contains(&"source_sync")); assert!( !tool_names.contains(&"ingest"), "ingest should be removed in v1.7" @@ -1963,21 +1537,12 @@ mod tests { "demote_memory should be removed in v1.7" ); - // Status / temporal — unified `memory_status` tool (v2.2). - // system_status + memory_health + memory_timeline + memory_changelog - // folded in; old names dispatch as hidden aliases but are off the list. - assert!(tool_names.contains(&"memory_status")); - for old in [ - "system_status", - "memory_health", - "memory_timeline", - "memory_changelog", - ] { - assert!( - !tool_names.contains(&old), - "{old} should be folded into 'memory_status' in v2.2" - ); - } + // Temporal tools (v1.2) + assert!(tool_names.contains(&"memory_timeline")); + assert!(tool_names.contains(&"memory_changelog")); + + // 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" @@ -1986,338 +1551,41 @@ mod tests { !tool_names.contains(&"stats"), "stats should be removed in v1.7" ); - // Maintenance / lifecycle — unified `maintain` tool (v2.2). - // consolidate + dream + gc + importance_score + backup + export + restore - // folded in; old names dispatch as hidden aliases but are off the list. - assert!(tool_names.contains(&"maintain")); - for old in [ - "consolidate", - "dream", - "gc", - "importance_score", - "backup", - "export", - "restore", - ] { - assert!( - !tool_names.contains(&old), - "{old} should be folded into 'maintain' in v2.2" - ); - } + assert!(tool_names.contains(&"consolidate")); + assert!(tool_names.contains(&"backup")); + assert!(tool_names.contains(&"export")); + assert!(tool_names.contains(&"gc")); - // Dedup / merge / supersede — unified `dedup` tool (v2.2). - // find_duplicates + the 7 Phase-3 merge tools folded in; still - // dispatchable as hidden back-compat aliases, but off the advertised list. - assert!(tool_names.contains(&"dedup")); - for old in [ - "find_duplicates", - "merge_candidates", - "plan_merge", - "plan_supersede", - "apply_plan", - "merge_undo", - "protect", - "merge_policy", - ] { - assert!( - !tool_names.contains(&old), - "{old} should be folded into 'dedup' in v2.2" - ); - } + // Auto-save & dedup tools (v1.3) + assert!(tool_names.contains(&"importance_score")); + assert!(tool_names.contains(&"find_duplicates")); - // Cognitive tools (v1.5): explore_connections + predict → `graph`; - // dream + restore → `maintain` (all v2.2). Nothing left advertised here. + // Cognitive tools (v1.5) + assert!(tool_names.contains(&"dream")); + assert!(tool_names.contains(&"explore_connections")); + assert!(tool_names.contains(&"predict")); + assert!(tool_names.contains(&"restore")); - // Context packets (v1.8) — renamed session_context → session_start (v2.2) - assert!(tool_names.contains(&"session_start")); - assert!( - !tool_names.contains(&"session_context"), - "session_context renamed to 'session_start' in v2.2" - ); + // Context packets (v1.8) + assert!(tool_names.contains(&"session_context")); - // Graph — unified `graph` tool (v2.2). explore_connections + predict + - // memory_graph + composed_graph folded in; old names dispatch as hidden - // aliases but are off the advertised list. (memory_health → memory_status.) - assert!(tool_names.contains(&"graph")); - for old in [ - "explore_connections", - "predict", - "memory_graph", - "composed_graph", - ] { - assert!( - !tool_names.contains(&old), - "{old} should be folded into 'graph' in v2.2" - ); - } + // Autonomic tools (v1.9) + assert!(tool_names.contains(&"memory_health")); + assert!(tool_names.contains(&"memory_graph")); - // Retrieval — unified `recall` tool (v2.2). search + deep_reference + - // cross_reference + contradictions folded in; old names dispatch as - // hidden aliases but are off the advertised list. - for old in [ - "search", - "deep_reference", - "cross_reference", - "contradictions", - ] { - assert!( - !tool_names.contains(&old), - "{old} should be folded into 'recall' in v2.2" - ); - } + // 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")); } - /// v2.2: the 8 tools folded into `dedup` must still dispatch (hidden - /// back-compat aliases), i.e. they must NOT return the "Unknown tool" - /// InvalidParams (-32602) error. Read-only/list-style actions are used so - /// the call resolves without mutating or requiring extra setup. - #[tokio::test] - async fn test_deprecated_dedup_aliases_redirect() { - let (mut server, _dir) = test_server().await; - let init_request = make_request("initialize", Some(init_params())); - server.handle_request(init_request).await; - - // (tool name, args) — all read-only / list-style so they resolve cleanly. - let calls: Vec<(&str, serde_json::Value)> = vec![ - ("find_duplicates", serde_json::json!({})), - ("merge_candidates", serde_json::json!({})), - ("merge_undo", serde_json::json!({})), // no operation_id => lists the reflog - ("merge_policy", serde_json::json!({})), // no args => returns current policy - ("dedup", serde_json::json!({"action": "policy"})), - ("dedup", serde_json::json!({})), // default action = scan - ]; - - for (name, args) in calls { - let request = make_request( - "tools/call", - Some(serde_json::json!({ "name": name, "arguments": args })), - ); - let response = server.handle_request(request).await.unwrap(); - // The call may succeed (result) or fail for a domain reason, but it - // must NOT be the unknown-tool InvalidParams error. - if let Some(err) = response.error { - assert_ne!( - err.code, -32602, - "'{name}' should still dispatch (hidden alias), got unknown-tool error: {}", - err.message - ); - } - } - } - - /// v2.2: the 4 tools folded into `memory_status` must still dispatch, and - /// each `view` of the new tool must resolve. - #[tokio::test] - async fn test_memory_status_views_and_aliases() { - let (mut server, _dir) = test_server().await; - let init_request = make_request("initialize", Some(init_params())); - server.handle_request(init_request).await; - - let calls: Vec<(&str, serde_json::Value)> = vec![ - // Deprecated aliases must still dispatch. - ("system_status", serde_json::json!({})), - ("memory_health", serde_json::json!({})), - ("memory_timeline", serde_json::json!({})), - ("memory_changelog", serde_json::json!({})), - // New unified views. - ("memory_status", serde_json::json!({})), // default view = health - ("memory_status", serde_json::json!({"view": "retention"})), - ("memory_status", serde_json::json!({"view": "timeline"})), - ("memory_status", serde_json::json!({"view": "changelog"})), - ]; - - for (name, args) in calls { - let request = make_request( - "tools/call", - Some(serde_json::json!({ "name": name, "arguments": args })), - ); - let response = server.handle_request(request).await.unwrap(); - assert!( - response.error.is_none(), - "'{name}' {args} should resolve, got error: {:?}", - response.error - ); - } - } - - /// v2.2: the 4 tools folded into `graph` must still dispatch, and the - /// read-only `graph` actions must resolve. (memory_graph is sync — this also - /// guards the no-`.await` facade branch.) - #[tokio::test] - async fn test_graph_actions_and_aliases() { - let (mut server, _dir) = test_server().await; - let init_request = make_request("initialize", Some(init_params())); - server.handle_request(init_request).await; - - let calls: Vec<(&str, serde_json::Value)> = vec![ - // Deprecated aliases must still dispatch (not unknown-tool). - ("predict", serde_json::json!({})), - ("memory_graph", serde_json::json!({})), - ("composed_graph", serde_json::json!({"action": "recent"})), - // New unified actions (read-only). - ("graph", serde_json::json!({"action": "predict"})), - ("graph", serde_json::json!({"action": "memory_graph"})), - ("graph", serde_json::json!({"action": "recent"})), - ("graph", serde_json::json!({"action": "never_composed"})), - ]; - - for (name, args) in calls { - let request = make_request( - "tools/call", - Some(serde_json::json!({ "name": name, "arguments": args })), - ); - let response = server.handle_request(request).await.unwrap(); - if let Some(err) = response.error { - assert_ne!( - err.code, -32602, - "'{name}' {args} should dispatch (not unknown-tool): {}", - err.message - ); - } - } - } - - /// v2.2: the 7 tools folded into `maintain` must still dispatch, the new - /// actions must resolve, gc must default to dry_run, and restore must keep - /// path validation (a nonexistent path errors rather than silently no-op). - #[tokio::test] - async fn test_maintain_actions_and_safety() { - let (mut server, _dir) = test_server().await; - let init_request = make_request("initialize", Some(init_params())); - server.handle_request(init_request).await; - - // Aliases + safe new actions must dispatch (not unknown-tool). - let dispatch_ok: Vec<(&str, serde_json::Value)> = vec![ - ("consolidate", serde_json::json!({})), - ("backup", serde_json::json!({})), - ("dream", serde_json::json!({})), - ("maintain", serde_json::json!({"action": "consolidate"})), - ("maintain", serde_json::json!({"action": "gc"})), - ("maintain", serde_json::json!({"action": "backup"})), - ]; - for (name, args) in dispatch_ok { - let request = make_request( - "tools/call", - Some(serde_json::json!({ "name": name, "arguments": args })), - ); - let response = server.handle_request(request).await.unwrap(); - if let Some(err) = response.error { - assert_ne!(err.code, -32602, "'{name}' {args} should dispatch: {}", err.message); - } - } - - // gc via maintain defaults to dry_run=true (no deletion). - let gc_req = make_request( - "tools/call", - Some(serde_json::json!({ "name": "maintain", "arguments": {"action": "gc"} })), - ); - let gc_resp = server.handle_request(gc_req).await.unwrap(); - let text = gc_resp.result.unwrap()["content"][0]["text"] - .as_str() - .unwrap() - .to_string(); - assert!( - text.contains("\"dryRun\": true") || text.contains("\"dryRun\":true"), - "maintain action=gc must default to dry_run=true; got: {text}" - ); - - // restore keeps path validation: a missing file must error, not no-op. - let restore_req = make_request( - "tools/call", - Some(serde_json::json!({ - "name": "maintain", - "arguments": {"action": "restore", "path": "/nonexistent/vestige-backup-xyz.json"} - })), - ); - let restore_resp = server.handle_request(restore_req).await.unwrap(); - // Either a JSON-RPC error or an error envelope is acceptable; a silent - // success is NOT (that would mean confinement/validation was bypassed). - let validated = restore_resp.error.is_some() - || restore_resp - .result - .map(|r| { - r["content"][0]["text"] - .as_str() - .map(|t| t.to_lowercase().contains("not found") || t.to_lowercase().contains("error")) - .unwrap_or(false) - }) - .unwrap_or(false); - assert!(validated, "maintain action=restore must validate a missing path"); - } - - /// v2.2 HOT PATH: `recall` defaults to mode='lookup' (search), the folded - /// names still dispatch, and the reason/contradictions modes resolve. - #[tokio::test] - async fn test_recall_modes_and_aliases() { - let (mut server, _dir) = test_server().await; - let init_request = make_request("initialize", Some(init_params())); - server.handle_request(init_request).await; - - let calls: Vec<(&str, serde_json::Value)> = vec![ - // Deprecated aliases must still dispatch. - ("search", serde_json::json!({"query": "x"})), - ("deep_reference", serde_json::json!({"query": "x"})), - ("cross_reference", serde_json::json!({"query": "x"})), - ("contradictions", serde_json::json!({})), - ("semantic_search", serde_json::json!({"query": "x"})), - // New unified modes. - ("recall", serde_json::json!({"query": "x"})), // default mode = lookup - ("recall", serde_json::json!({"mode": "lookup", "query": "x"})), - ("recall", serde_json::json!({"mode": "reason", "query": "x"})), - ("recall", serde_json::json!({"mode": "contradictions"})), - ]; - - for (name, args) in calls { - let request = make_request( - "tools/call", - Some(serde_json::json!({ "name": name, "arguments": args })), - ); - let response = server.handle_request(request).await.unwrap(); - assert!( - response.error.is_none(), - "'{name}' {args} should resolve, got error: {:?}", - response.error - ); - } - } - - /// v2.2: `recall` mode='lookup' (the default) must produce the same result - /// shape as the former standalone `search` — i.e. the no-mode default is a - /// faithful pass-through, not a reasoning call. - #[tokio::test] - async fn test_recall_lookup_matches_search_shape() { - let (mut server, _dir) = test_server().await; - let init_request = make_request("initialize", Some(init_params())); - server.handle_request(init_request).await; - - let args = serde_json::json!({ "query": "anything" }); - let via_recall = make_request( - "tools/call", - Some(serde_json::json!({ "name": "recall", "arguments": args })), - ); - let via_search = make_request( - "tools/call", - Some(serde_json::json!({ "name": "search", "arguments": args })), - ); - let r1 = server.handle_request(via_recall).await.unwrap(); - let r2 = server.handle_request(via_search).await.unwrap(); - assert!(r1.error.is_none() && r2.error.is_none()); - // The unified-tool wrapper text (the search payload) must match. - assert_eq!( - r1.result.unwrap()["content"][0]["text"], - r2.result.unwrap()["content"][0]["text"], - "recall(mode=lookup) must equal search byte-for-byte" - ); - } - #[tokio::test] async fn test_tools_have_descriptions_and_schemas() { let (mut server, _dir) = test_server().await; - let init_request = make_request("initialize", Some(init_params())); + let init_request = make_request("initialize", None); server.handle_request(init_request).await; let request = make_request("tools/list", None); @@ -2347,7 +1615,7 @@ mod tests { async fn test_resources_list_returns_all_resources() { let (mut server, _dir) = test_server().await; - let init_request = make_request("initialize", Some(init_params())); + let init_request = make_request("initialize", None); server.handle_request(init_request).await; let request = make_request("resources/list", None); @@ -2376,7 +1644,7 @@ mod tests { async fn test_resources_have_descriptions() { let (mut server, _dir) = test_server().await; - let init_request = make_request("initialize", Some(init_params())); + let init_request = make_request("initialize", None); server.handle_request(init_request).await; let request = make_request("resources/list", None); @@ -2404,7 +1672,7 @@ mod tests { let (mut server, _dir) = test_server().await; // Initialize first - let init_request = make_request("initialize", Some(init_params())); + let init_request = make_request("initialize", None); server.handle_request(init_request).await; let request = make_request("unknown/method", None); @@ -2420,7 +1688,7 @@ mod tests { async fn test_unknown_tool_returns_error() { let (mut server, _dir) = test_server().await; - let init_request = make_request("initialize", Some(init_params())); + let init_request = make_request("initialize", None); server.handle_request(init_request).await; let request = make_request( @@ -2433,7 +1701,7 @@ mod tests { let response = server.handle_request(request).await.unwrap(); assert!(response.error.is_some()); - assert_eq!(response.error.unwrap().code, -32602); + assert_eq!(response.error.unwrap().code, -32601); } // ======================================================================== @@ -2444,7 +1712,7 @@ mod tests { async fn test_ping_returns_empty_object() { let (mut server, _dir) = test_server().await; - let init_request = make_request("initialize", Some(init_params())); + let init_request = make_request("initialize", None); server.handle_request(init_request).await; let request = make_request("ping", None); @@ -2463,7 +1731,7 @@ mod tests { async fn test_tools_call_missing_params_returns_error() { let (mut server, _dir) = test_server().await; - let init_request = make_request("initialize", Some(init_params())); + let init_request = make_request("initialize", None); server.handle_request(init_request).await; let request = make_request("tools/call", None); @@ -2473,40 +1741,11 @@ mod tests { assert_eq!(response.error.unwrap().code, -32602); // InvalidParams } - #[tokio::test] - async fn test_tool_call_populates_agent_black_box_trace() { - // Regression (#117): the trace recorder was dead code — no production - // caller — so agent_traces/receipts/memory_prs never populated. A tool - // call must now record at least the opening mcp.call event under the - // supplied runId. - let (mut server, _dir) = test_server().await; - server - .handle_request(make_request("initialize", Some(init_params()))) - .await; - - let run_id = "test-run-blackbox"; - let request = make_request( - "tools/call", - Some(serde_json::json!({ - "name": "search", - "arguments": { "query": "anything", "runId": run_id } - })), - ); - let response = server.handle_request(request).await.unwrap(); - assert!(response.error.is_none(), "tool call should succeed"); - - let events = server.storage.get_trace(run_id).expect("read trace"); - assert!( - !events.is_empty(), - "the Black Box must record at least the mcp.call event for this run" - ); - } - #[tokio::test] async fn test_tools_call_invalid_params_returns_error() { let (mut server, _dir) = test_server().await; - let init_request = make_request("initialize", Some(init_params())); + let init_request = make_request("initialize", None); server.handle_request(init_request).await; let request = make_request( @@ -2520,170 +1759,4 @@ mod tests { assert!(response.error.is_some()); assert_eq!(response.error.unwrap().code, -32602); } - - #[tokio::test] - async fn test_tools_call_rejects_non_object_arguments() { - let (mut server, _dir) = test_server().await; - - let init_request = make_request("initialize", Some(init_params())); - server.handle_request(init_request).await; - - let request = make_request( - "tools/call", - Some(serde_json::json!({ - "name": "search", - "arguments": "not-an-object" - })), - ); - - let response = server.handle_request(request).await.unwrap(); - assert!(response.error.is_some()); - assert_eq!(response.error.unwrap().code, -32602); - } - - // ======================================================================== - // Per-tool result-size annotation tests - // (`_meta["anthropic/maxResultSizeChars"]`, CC v2.1.91+) - // - // The annotation lives on the Tool definition in `tools/list`, so CC reads - // it once when the MCP session opens and applies the override to every - // invocation of that tool. These tests pin the wire-form so a future - // refactor of `ToolDescription` cannot silently drop the annotation. - // ======================================================================== - - /// Expected per-tool caps. Returns `Some(cap)` for tools the discipline - /// annotates, `None` for tools that MUST NOT carry the annotation - /// (cargo-cult prevention). - fn expected_max_result_size(name: &str) -> Option { - match name { - // v2.2: search folded into recall (mode='lookup'); annotation moved. - "recall" => Some(300_000), - // v2.2: memory_timeline folded into memory_status (view='timeline'); - // the high-payload annotation moved with it. - "memory_status" => Some(200_000), - "memory" => Some(100_000), - "codebase" => Some(100_000), - // v2.2: dedup action='scan' returns clusters + candidates + policy. - "dedup" => Some(150_000), - // v2.2: graph memory_graph layout + bounty_mode pagination. - "graph" => Some(250_000), - _ => None, - } - } - - #[tokio::test] - async fn test_high_payload_tools_have_max_result_size_annotation() { - let (mut server, _dir) = test_server().await; - let init_request = make_request("initialize", Some(init_params())); - server.handle_request(init_request).await; - - let request = make_request("tools/list", None); - let response = server.handle_request(request).await.unwrap(); - let result = response.result.unwrap(); - let tools = result["tools"].as_array().unwrap(); - - for name in ["recall", "memory_status", "memory", "codebase", "dedup", "graph"] { - let tool = tools - .iter() - .find(|t| t["name"].as_str() == Some(name)) - .unwrap_or_else(|| panic!("Tool '{}' missing from tools/list", name)); - - let expected = expected_max_result_size(name).unwrap(); - let meta = tool.get("_meta").unwrap_or_else(|| { - panic!("Tool '{}' is missing the `_meta` field on the wire", name) - }); - let actual = meta - .get("anthropic/maxResultSizeChars") - .and_then(|v| v.as_u64()) - .unwrap_or_else(|| { - panic!( - "Tool '{}' _meta lacks integer 'anthropic/maxResultSizeChars'", - name - ) - }); - assert_eq!( - actual, expected, - "Tool '{}' cap drift: expected {} got {}", - name, expected, actual - ); - assert!( - actual <= 500_000, - "Tool '{}' cap {} exceeds Anthropic 500K ceiling", - name, - actual - ); - } - } - - #[tokio::test] - async fn test_other_tools_do_not_carry_max_result_size_annotation() { - // Cargo-cult prevention. Dynamically derived from tools/list so this - // test is robust to new tools being added: any tool that is NOT in - // the discipline-prescribed set MUST NOT carry the annotation. - // Adding the annotation to a small-payload tool dilutes the signal - // and trains future maintainers that the value is arbitrary. - let (mut server, _dir) = test_server().await; - let init_request = make_request("initialize", Some(init_params())); - server.handle_request(init_request).await; - - let request = make_request("tools/list", None); - let response = server.handle_request(request).await.unwrap(); - let result = response.result.unwrap(); - let tools = result["tools"].as_array().unwrap(); - - for tool in tools { - let name = tool["name"].as_str().unwrap(); - if expected_max_result_size(name).is_some() { - continue; // covered by the annotated-tools test - } - - // Either the `_meta` key is absent OR it is an object without the - // anthropic key — both are acceptable. The forbidden case is the - // anthropic key present on this tool. - let has_max_size = tool - .get("_meta") - .and_then(|m| m.get("anthropic/maxResultSizeChars")) - .is_some(); - assert!( - !has_max_size, - "Tool '{}' should NOT carry maxResultSizeChars annotation \ - (not in the discipline-prescribed set: search, memory_timeline, \ - memory, codebase). If this tool's realistic max-payload now \ - routinely exceeds 50K, update expected_max_result_size() + the \ - annotation loop in handle_tools_list together.", - name - ); - } - } - - #[tokio::test] - async fn test_meta_wire_shape_uses_underscore_meta_field() { - // Anthropic's MCP spec is explicit: the field on the wire is `_meta`, - // NOT `meta`. The Rust struct uses `meta: Option` with - // `#[serde(rename = "_meta")]` — assert the rename actually fired. - let (mut server, _dir) = test_server().await; - let init_request = make_request("initialize", Some(init_params())); - server.handle_request(init_request).await; - - let request = make_request("tools/list", None); - let response = server.handle_request(request).await.unwrap(); - let result = response.result.unwrap(); - let tools = result["tools"].as_array().unwrap(); - - // v2.2: `recall` is the annotated retrieval tool (search folded in). - let recall_tool = tools - .iter() - .find(|t| t["name"].as_str() == Some("recall")) - .expect("'recall' tool present"); - - // Wire-form: `_meta` must exist; `meta` (un-renamed) must NOT exist. - assert!( - recall_tool.get("_meta").is_some(), - "recall tool missing `_meta` key (serde rename to _meta did not apply)" - ); - assert!( - recall_tool.get("meta").is_none(), - "recall tool has un-renamed `meta` key (regression — serde rename broke)" - ); - } } diff --git a/crates/vestige-mcp/src/tools/backfill.rs b/crates/vestige-mcp/src/tools/backfill.rs deleted file mode 100644 index c6850ff..0000000 --- a/crates/vestige-mcp/src/tools/backfill.rs +++ /dev/null @@ -1,317 +0,0 @@ -//! # Retroactive Salience Backfill — MCP tool -//! -//! Memory with hindsight. When a salient FAILURE memory exists (a bug/crash/ -//! regression — the "aversive event"), this reaches BACKWARD across history and -//! promotes the quiet earlier memory that caused it: the root cause a vector -//! search structurally cannot surface because it is not *similar* to the -//! failure, only causally upstream. -//! -//! Faithful port of Zaki/Cai et al. (2024) Nature 637:145-155. The core logic -//! lives in `vestige_core::advanced::retroactive_backfill`; this tool wires it -//! to real storage: builds candidates from `KnowledgeNode`s (entities drawn from -//! tags and content), runs the backward reach, and PROMOTES the surfaced cause -//! so it stops decaying and resurfaces next time. - -use serde::Deserialize; -use serde_json::{Value, json}; -use std::sync::Arc; - -use vestige_core::advanced::retroactive_backfill::{ - self, BackfillCandidate, FailureEvent, RetroactiveBackfill, -}; -use vestige_core::advanced::prediction_error::cosine_similarity; -use vestige_core::{KnowledgeNode, Storage}; - -pub fn schema() -> Value { - json!({ - "type": "object", - "properties": { - "failure_id": { - "type": "string", - "description": "ID of the failure/'aversive event' memory to backfill from. If omitted, the most recent memory that looks like a failure is used." - }, - "manual": { - "type": "boolean", - "description": "Force the backfill even if the event isn't auto-detected as salient (manual override). Default false.", - "default": false - }, - "lookback_days": { - "type": "integer", - "description": "How many days back to reach for the cause. Default 30.", - "minimum": 1, - "maximum": 365, - "default": 30 - }, - "promote": { - "type": "boolean", - "description": "Whether to actually promote (boost) the surfaced cause(s) in storage. Default true. Set false for a dry-run preview.", - "default": true - }, - "scan_limit": { - "type": "integer", - "description": "Max memories to scan as candidate causes. Default 500.", - "minimum": 10, - "maximum": 5000, - "default": 500 - } - } - }) -} - -#[derive(Deserialize, Default)] -struct Args { - failure_id: Option, - #[serde(default)] - manual: bool, - lookback_days: Option, - promote: Option, - scan_limit: Option, -} - -/// Pull entities out of a memory: its tags, plus heuristic code-ish tokens from -/// content (UPPER_SNAKE env vars, dotted/slashed file paths). These are the -/// shared-entity join keys the backward reach follows. -/// -/// Thin `&KnowledgeNode` adapter over the single core definition -/// [`retroactive_backfill::extract_entities`] so the MCP tool, CLI, and the -/// offline consolidation pass all extract entities identically (no drift). -fn extract_entities(node: &KnowledgeNode) -> Vec { - retroactive_backfill::extract_entities(&node.content, &node.tags) -} - -/// Heuristic: does this memory read like a failure/"aversive event"? Checks both -/// content AND tags against the full FAILURE_MARKERS list. Public so the CLI and -/// any caller share ONE failure-detection definition (no drifting subsets). -/// -/// Thin `&KnowledgeNode` adapter over [`retroactive_backfill::looks_like_failure`]. -pub fn looks_like_failure(node: &KnowledgeNode) -> bool { - retroactive_backfill::looks_like_failure(&node.content, &node.tags) -} - -pub async fn execute(storage: &Arc, args: Option) -> Result { - let args: Args = match args { - Some(v) => serde_json::from_value(v).map_err(|e| e.to_string())?, - None => Args::default(), - }; - // Clamp numeric inputs to the documented schema bounds. The MCP dispatch - // layer does NOT enforce the JSON-schema min/max, so a caller can send - // scan_limit=-1 (SQLite treats a negative LIMIT as unbounded => full-table - // fetch = DoS) or values above the 5000 cap. Clamp rather than trust. - let lookback = args.lookback_days.unwrap_or(30).clamp(1, 365); - let promote = args.promote.unwrap_or(true); - let scan_limit = args.scan_limit.unwrap_or(500).clamp(10, 5000); - - // 1. Resolve the failure event. - let failure_node = match &args.failure_id { - Some(id) => storage - .get_node(id) - .map_err(|e| e.to_string())? - .ok_or_else(|| format!("failure memory '{id}' not found"))?, - None => { - // most recent memory that looks like a failure - let recent = storage.get_all_nodes(scan_limit, 0).map_err(|e| e.to_string())?; - recent - .into_iter() - .find(looks_like_failure) - .ok_or_else(|| { - "no failure-like memory found to backfill from; pass failure_id or manual=true" - .to_string() - })? - } - }; - - let failure_entities = extract_entities(&failure_node); - let failure_embedding = storage.get_node_embedding(&failure_node.id).ok().flatten(); - - // surprise/prediction-error proxy: a failure-marked memory is treated as - // high-salience; otherwise fall back to a neutral value (manual can force). - let pe = if looks_like_failure(&failure_node) { 0.9_f32 } else { 0.3_f32 }; - - let failure = FailureEvent { - id: failure_node.id.clone(), - content: failure_node.content.clone(), - entities: failure_entities.clone(), - tags: failure_node.tags.clone(), - prediction_error: pe, - manual: args.manual, - }; - - // 2. Build candidate causes from all OTHER memories (older than the failure). - let all = storage.get_all_nodes(scan_limit, 0).map_err(|e| e.to_string())?; - let mut candidates: Vec = Vec::new(); - for node in &all { - if node.id == failure_node.id { - continue; - } - let age = (failure_node.created_at - node.created_at).num_seconds() as f64 / 86_400.0; - // only consider memories strictly older than the failure (backward-only) - if age <= 0.0 { - continue; - } - let sim = match (&failure_embedding, storage.get_node_embedding(&node.id).ok().flatten()) { - (Some(f), Some(c)) if f.len() == c.len() => Some(cosine_similarity(f, &c)), - _ => None, - }; - candidates.push(BackfillCandidate { - id: node.id.clone(), - content: node.content.clone(), - entities: extract_entities(node), - age_days_before_failure: age, - stability: node.stability, - similarity_to_failure: sim, - }); - } - - // 3. Run the backward reach. - let backfill = RetroactiveBackfill { - lookback_days: lookback, - ..RetroactiveBackfill::new() - }; - let result = backfill.run(&failure, &candidates); - - if !result.triggered { - return Ok(json!({ - "tool": "backfill", - "triggered": false, - "reason": "the event was not salient (not a detected failure and manual=false). Pass manual=true to force.", - "failure_id": failure.id, - })); - } - - // 4. Promote the surfaced cause(s) so they stop decaying and resurface. - let mut promoted = Vec::new(); - for cause in &result.causes { - let content_preview = candidates - .iter() - .find(|c| c.id == cause.memory_id) - .map(|c| c.content.chars().take(140).collect::()) - .unwrap_or_default(); - let mut did_promote = false; - if promote { - // promote_memory_backfill boosts retrieval strength + reps (the FSRS - // promote knob) with a bounded stability multiply — shared with the - // step-8.5 auto-fire path (omega-backfill-safety patch, pending upstream). - did_promote = storage.promote_memory_backfill(&cause.memory_id).is_ok(); - } - promoted.push(json!({ - "memory_id": cause.memory_id, - "content_preview": content_preview, - "shared_entities": cause.shared_entities, - "age_days_before_failure": (cause.age_days * 10.0).round() / 10.0, - "similarity_rank": cause.similarity_rank, - "backfill_score": (cause.score * 100.0).round() / 100.0, - "promoted": did_promote, - "reason": cause.reason, - })); - } - - Ok(json!({ - "tool": "backfill", - "triggered": true, - "headline": format!( - "Reached back across history from the failure and surfaced {} causal memor{} that semantic search would have missed.", - result.causes.len(), - if result.causes.len() == 1 { "y" } else { "ies" } - ), - "failure": { - "id": failure.id, - "content_preview": failure.content.chars().take(160).collect::(), - "entities": failure_entities, - }, - "scanned": result.scanned, - "causes": promoted, - "note": "Causes are ranked by causal join (shared entities, backward in time), NOT semantic similarity. A high similarity_rank means a vector search would NOT have surfaced this — that is the point.", - })) -} - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::TempDir; - use vestige_core::IngestInput; - - async fn test_storage() -> (Arc, TempDir) { - let dir = TempDir::new().unwrap(); - let storage = Storage::new(Some(dir.path().join("test.db"))).unwrap(); - (Arc::new(storage), dir) - } - - /// LIVE end-to-end: plant a quiet env-var cause, a semantic distractor, and a - /// failure into a REAL SQLite store, then run the backfill MCP tool and assert - /// it surfaces the causal env-var memory by the shared API_TIMEOUT entity — - /// the root cause a vector search would never rank first. This is the - /// reproducible receipt behind the demo. - #[tokio::test] - async fn live_backfill_surfaces_root_cause_through_storage() { - let (storage, _dir) = test_storage().await; - - // 1) The quiet cause: an env-var edit (no failure words; not "similar" to a crash). - // Backdated 3 days so the backward reach can find it (the demo scenario). - let cause = storage - .ingest(IngestInput { - content: "Set API_TIMEOUT=2 in the deploy env to speed up cold starts".to_string(), - node_type: "decision".to_string(), - tags: vec!["API_TIMEOUT".to_string(), "deploy-env".to_string()], - ..Default::default() - }) - .unwrap(); - storage - .set_created_at(&cause.id, chrono::Utc::now() - chrono::Duration::days(3)) - .unwrap(); - - // 2) A semantic distractor: looks like the crash, but shares NO entity. - // Backdated 20 days (also in the past, so only the entity link decides). - let distractor = storage - .ingest(IngestInput { - content: "A 500 Internal Server Error happened in the billing service last month" - .to_string(), - node_type: "event".to_string(), - tags: vec!["billing-service".to_string()], - ..Default::default() - }) - .unwrap(); - storage - .set_created_at(&distractor.id, chrono::Utc::now() - chrono::Duration::days(20)) - .unwrap(); - - // 3) The failure, recorded last (most recent) — the "aversive event". - let failure = storage - .ingest(IngestInput { - content: "Service crashed: 500 Internal Server Error on the auth endpoint" - .to_string(), - node_type: "event".to_string(), - tags: vec!["auth-service".to_string(), "API_TIMEOUT".to_string(), "crash".to_string()], - ..Default::default() - }) - .unwrap(); - - // Run the backfill tool against the real store (auto-finds the failure). - let out = execute( - &storage, - Some(json!({ "promote": true, "manual": false })), - ) - .await - .expect("backfill must run"); - - assert_eq!(out["triggered"], json!(true), "the crash must trigger a backfill"); - let causes = out["causes"].as_array().expect("causes array"); - assert!(!causes.is_empty(), "must surface at least one cause"); - - // The top cause is the env-var memory, surfaced by the shared API_TIMEOUT entity. - let top = &causes[0]; - let content = top["content_preview"].as_str().unwrap_or(""); - assert!( - content.contains("API_TIMEOUT") && content.contains("deploy"), - "top cause must be the env-var edit, got: {content}" - ); - let shared = top["shared_entities"].as_array().unwrap(); - assert!( - shared.iter().any(|e| e.as_str() == Some("api_timeout")), - "must link via the shared API_TIMEOUT entity, got: {shared:?}" - ); - // It was actually promoted in the real store. - assert_eq!(top["promoted"], json!(true), "the cause must be promoted in storage"); - // Sanity: the failure we ingested is the one that fired. - assert_eq!(out["failure"]["id"], json!(failure.id)); - } -} diff --git a/crates/vestige-mcp/src/tools/changelog.rs b/crates/vestige-mcp/src/tools/changelog.rs index eb47139..8a2e746 100644 --- a/crates/vestige-mcp/src/tools/changelog.rs +++ b/crates/vestige-mcp/src/tools/changelog.rs @@ -278,7 +278,6 @@ mod tests { tags: vec![], valid_from: None, valid_until: None, - source_envelope: None, }) .unwrap(); node.id diff --git a/crates/vestige-mcp/src/tools/codebase_unified.rs b/crates/vestige-mcp/src/tools/codebase_unified.rs index 50491cd..726ab4b 100644 --- a/crates/vestige-mcp/src/tools/codebase_unified.rs +++ b/crates/vestige-mcp/src/tools/codebase_unified.rs @@ -9,9 +9,7 @@ use std::sync::Arc; use tokio::sync::Mutex; use crate::cognitive::CognitiveEngine; -use vestige_core::{IngestInput, OutputConfig, Storage}; - -use super::search_unified::apply_output_masks; +use vestige_core::{IngestInput, Storage}; /// Input schema for the unified codebase tool pub fn schema() -> Value { @@ -89,7 +87,6 @@ struct CodebaseArgs { pub async fn execute( storage: &Arc, cognitive: &Arc>, - output_config: &OutputConfig, args: Option, ) -> Result { let args: CodebaseArgs = match args { @@ -100,7 +97,7 @@ pub async fn execute( match args.action.as_str() { "remember_pattern" => execute_remember_pattern(storage, cognitive, &args).await, "remember_decision" => execute_remember_decision(storage, cognitive, &args).await, - "get_context" => execute_get_context(storage, cognitive, output_config, &args).await, + "get_context" => execute_get_context(storage, cognitive, &args).await, _ => Err(format!( "Invalid action '{}'. Must be one of: remember_pattern, remember_decision, get_context", args.action @@ -154,7 +151,6 @@ async fn execute_remember_pattern( tags, valid_from: None, valid_until: None, - source_envelope: None, }; let node = storage.ingest(input).map_err(|e| e.to_string())?; @@ -251,7 +247,6 @@ async fn execute_remember_decision( tags, valid_from: None, valid_until: None, - source_envelope: None, }; let node = storage.ingest(input).map_err(|e| e.to_string())?; @@ -287,11 +282,9 @@ async fn execute_remember_decision( async fn execute_get_context( storage: &Arc, cognitive: &Arc>, - output_config: &OutputConfig, args: &CodebaseArgs, ) -> Result { - // Precedence: explicit MCP param > config limit > built-in default (10). - let limit = output_config.resolve_limit(args.limit, 10).clamp(1, 50); + 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)); @@ -306,7 +299,7 @@ async fn execute_get_context( .get_nodes_by_type_and_tag("decision", tag_filter.as_deref(), limit) .unwrap_or_default(); - let mut formatted_patterns: Vec = patterns + let formatted_patterns: Vec = patterns .iter() .map(|n| { serde_json::json!({ @@ -318,9 +311,8 @@ async fn execute_get_context( }) }) .collect(); - apply_output_masks(&mut formatted_patterns, output_config); - let mut formatted_decisions: Vec = decisions + let formatted_decisions: Vec = decisions .iter() .map(|n| { serde_json::json!({ @@ -332,7 +324,6 @@ async fn execute_get_context( }) }) .collect(); - apply_output_masks(&mut formatted_decisions, output_config); // ==================================================================== // COGNITIVE: Cross-project knowledge discovery @@ -361,7 +352,6 @@ async fn execute_get_context( Ok(serde_json::json!({ "action": "get_context", "codebase": args.codebase, - "profile": output_config.profile.as_str(), "patterns": { "count": formatted_patterns.len(), "items": formatted_patterns, @@ -421,7 +411,7 @@ mod tests { #[tokio::test] async fn test_missing_args_fails() { let (storage, _dir) = test_storage().await; - let result = execute(&storage, &test_cognitive(), &OutputConfig::default(), None).await; + let result = execute(&storage, &test_cognitive(), None).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("Missing arguments")); } @@ -430,13 +420,7 @@ mod tests { async fn test_invalid_action_fails() { let (storage, _dir) = test_storage().await; let args = serde_json::json!({ "action": "invalid" }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("Invalid action")); } @@ -451,13 +435,7 @@ mod tests { "files": ["src/lib.rs"], "codebase": "vestige" }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_ok()); let value = result.unwrap(); assert_eq!(value["action"], "remember_pattern"); @@ -473,13 +451,7 @@ mod tests { "action": "remember_pattern", "description": "Some description" }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("'name' is required")); } @@ -491,13 +463,7 @@ mod tests { "action": "remember_pattern", "name": "Test Pattern" }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("'description' is required")); } @@ -510,13 +476,7 @@ mod tests { "name": " ", "description": "Some description" }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("empty")); } @@ -532,13 +492,7 @@ mod tests { "files": ["src/storage.rs"], "codebase": "vestige" }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_ok()); let value = result.unwrap(); assert_eq!(value["action"], "remember_decision"); @@ -553,13 +507,7 @@ mod tests { "action": "remember_decision", "rationale": "Some rationale" }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("'decision' is required")); } @@ -571,13 +519,7 @@ mod tests { "action": "remember_decision", "decision": "Use SQLite" }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("'rationale' is required")); } @@ -590,13 +532,7 @@ mod tests { "decision": " ", "rationale": "Something" }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("empty")); } @@ -608,13 +544,7 @@ mod tests { "action": "get_context", "codebase": "nonexistent" }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_ok()); let value = result.unwrap(); assert_eq!(value["action"], "get_context"); @@ -633,16 +563,14 @@ mod tests { "description": "A test pattern", "codebase": "myproject" }); - execute(&storage, &cog, &OutputConfig::default(), Some(save_args)) - .await - .unwrap(); + execute(&storage, &cog, Some(save_args)).await.unwrap(); // Now retrieve let get_args = serde_json::json!({ "action": "get_context", "codebase": "myproject" }); - let result = execute(&storage, &cog, &OutputConfig::default(), Some(get_args)).await; + let result = execute(&storage, &cog, Some(get_args)).await; assert!(result.is_ok()); let value = result.unwrap(); assert!(value["patterns"]["count"].as_u64().unwrap() >= 1); @@ -652,41 +580,10 @@ mod tests { async fn test_get_context_no_codebase() { let (storage, _dir) = test_storage().await; let args = serde_json::json!({ "action": "get_context" }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_ok()); let value = result.unwrap(); assert_eq!(value["action"], "get_context"); assert!(value["codebase"].is_null()); } - - /// Phase 2: the `lean` profile masks the `createdAt` timestamp from - /// get_context items, and the response echoes the active profile. - #[tokio::test] - async fn test_get_context_lean_profile_masks_timestamps() { - let (storage, _dir) = test_storage().await; - let cog = test_cognitive(); - let save_args = serde_json::json!({ - "action": "remember_pattern", - "name": "Lean Pattern", - "description": "A pattern for lean masking", - "codebase": "leanproj" - }); - execute(&storage, &cog, &OutputConfig::default(), Some(save_args)) - .await - .unwrap(); - - let cfg = vestige_core::VestigeConfig::parse("[defaults]\nprofile=lean").output(); - let get_args = serde_json::json!({ "action": "get_context", "codebase": "leanproj" }); - let value = execute(&storage, &cog, &cfg, Some(get_args)).await.unwrap(); - assert_eq!(value["profile"], "lean"); - let item = &value["patterns"]["items"][0]; - assert!(item.get("createdAt").is_none(), "lean must drop createdAt"); - assert!(item.get("content").is_some(), "content still present"); - } } diff --git a/crates/vestige-mcp/src/tools/composed_graph.rs b/crates/vestige-mcp/src/tools/composed_graph.rs deleted file mode 100644 index 58c18dd..0000000 --- a/crates/vestige-mcp/src/tools/composed_graph.rs +++ /dev/null @@ -1,906 +0,0 @@ -//! composed_graph tool — durable composition history and bounty-mode lane queue. - -use chrono::Utc; -use serde::Deserialize; -use serde_json::Value; -use std::sync::Arc; -use uuid::Uuid; -use vestige_core::{CompositionOutcomeRecord, Storage}; - -pub(crate) const OUTCOME_TYPES: &[&str] = &[ - "helpful", - "dead_end", - "submitted", - "accepted", - "rejected", - "duplicate_risk", - "needs_poc", - "bad_severity", - "user_promoted", - "user_demoted", - "closed_by_scope", - "closed_by_duplicate", - "closed_by_false_assumption", - "closed_by_user", - "expired_lane", -]; - -pub fn schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "action": { - "type": "string", - "enum": ["recent", "get", "memory", "neighbors", "never_composed", "bounty_mode", "label"], - "description": "ComposedGraph action to run." - }, - "event_id": { - "type": "string", - "description": "Composition event id for get/label actions." - }, - "memory_id": { - "type": "string", - "description": "Memory id for memory/neighbors actions." - }, - "limit": { - "type": "integer", - "description": "Maximum rows to return (default 10, max 100).", - "default": 10, - "minimum": 1, - "maximum": 100 - }, - "tags": { - "type": "array", - "items": { "type": "string" }, - "description": "Optional tag filter for never_composed and bounty_mode." - }, - "outcome_type": { - "type": "string", - "enum": ["helpful", "dead_end", "submitted", "accepted", "rejected", "duplicate_risk", "needs_poc", "bad_severity", "user_promoted", "user_demoted", "closed_by_scope", "closed_by_duplicate", "closed_by_false_assumption", "closed_by_user", "expired_lane"], - "description": "Outcome label for label action." - }, - "notes": { - "type": "string", - "description": "Optional outcome notes." - }, - "label_source": { - "type": "string", - "description": "Where the outcome label came from (default: user)." - }, - "confidence_delta": { - "type": "number", - "description": "Optional confidence adjustment for this outcome." - } - }, - "required": ["action"] - }) -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "snake_case")] -struct ComposedGraphArgs { - action: String, - event_id: Option, - memory_id: Option, - limit: Option, - tags: Option>, - outcome_type: Option, - notes: Option, - label_source: Option, - confidence_delta: Option, -} - -pub async fn execute(storage: &Arc, args: Option) -> Result { - let args: ComposedGraphArgs = match args { - Some(value) => { - serde_json::from_value(value).map_err(|e| format!("Invalid arguments: {}", e))? - } - None => return Err("Missing arguments".to_string()), - }; - let limit = args.limit.unwrap_or(10).clamp(1, 100); - - match args.action.as_str() { - "recent" => recent(storage, limit), - "get" => { - let event_id = args - .event_id - .as_deref() - .ok_or_else(|| "event_id is required for get".to_string())?; - get(storage, event_id) - } - "memory" => { - let memory_id = args - .memory_id - .as_deref() - .ok_or_else(|| "memory_id is required for memory".to_string())?; - memory(storage, memory_id, limit) - } - "neighbors" => { - let memory_id = args - .memory_id - .as_deref() - .ok_or_else(|| "memory_id is required for neighbors".to_string())?; - neighbors(storage, memory_id, limit) - } - "never_composed" => never_composed(storage, limit, args.tags.as_deref()), - "bounty_mode" => bounty_mode(storage, limit, args.tags.as_deref()), - "label" => label(storage, &args), - other => Err(format!("Unknown composed_graph action: {}", other)), - } -} - -fn recent(storage: &Storage, limit: i32) -> Result { - let events = storage - .get_recent_composition_events(limit) - .map_err(|e| e.to_string())?; - Ok(serde_json::json!({ - "action": "recent", - "events": events, - })) -} - -fn get(storage: &Storage, event_id: &str) -> Result { - let event = storage - .get_composition_event(event_id) - .map_err(|e| e.to_string())? - .ok_or_else(|| format!("composition event not found: {}", event_id))?; - let members = storage - .get_composition_members(event_id) - .map_err(|e| e.to_string())?; - let outcomes = storage - .get_composition_outcomes(event_id) - .map_err(|e| e.to_string())?; - Ok(serde_json::json!({ - "action": "get", - "event": event, - "members": members, - "outcomes": outcomes, - })) -} - -fn memory(storage: &Storage, memory_id: &str, limit: i32) -> Result { - let events = storage - .get_compositions_for_memory(memory_id, limit) - .map_err(|e| e.to_string())?; - Ok(serde_json::json!({ - "action": "memory", - "memoryId": memory_id, - "events": events, - })) -} - -fn neighbors(storage: &Storage, memory_id: &str, limit: i32) -> Result { - let neighbors = storage - .get_composition_neighbors(memory_id, limit) - .map_err(|e| e.to_string())?; - Ok(serde_json::json!({ - "action": "neighbors", - "memoryId": memory_id, - "neighbors": neighbors, - })) -} - -fn never_composed(storage: &Storage, limit: i32, tags: Option<&[String]>) -> Result { - let candidates = storage - .get_never_composed_candidates(limit, tags) - .map_err(|e| e.to_string())?; - Ok(serde_json::json!({ - "action": "never_composed", - "candidates": candidates, - })) -} - -fn bounty_mode(storage: &Storage, limit: i32, tags: Option<&[String]>) -> Result { - const PAGE_SIZE: i32 = 100; - const MAX_SCAN_EVENTS: i32 = 1_000; - - let mut offset = 0; - let mut scanned = 0; - let mut already_composed = Vec::new(); - let mut closed_doors = Vec::new(); - let mut duplicate_risk_lanes = Vec::new(); - let mut needs_poc_lanes = Vec::new(); - - loop { - let events = storage - .get_recent_composition_events_page(PAGE_SIZE, offset) - .map_err(|e| e.to_string())?; - if events.is_empty() { - break; - } - scanned += events.len() as i32; - - for event in events { - let outcomes = storage - .get_composition_outcomes(&event.id) - .map_err(|e| e.to_string())?; - let members = storage - .get_composition_members(&event.id) - .map_err(|e| e.to_string())?; - if !composition_matches_tags(storage, &event, &members, tags)? { - continue; - } - let item = serde_json::json!({ - "event": event, - "members": members, - "outcomes": outcomes, - }); - let outcome_types = item["outcomes"] - .as_array() - .map(|values| { - values - .iter() - .filter_map(|value| value.get("outcomeType").and_then(|v| v.as_str())) - .collect::>() - }) - .unwrap_or_default(); - - if outcome_types.iter().any(|kind| { - matches!( - *kind, - "dead_end" - | "rejected" - | "bad_severity" - | "closed_by_scope" - | "closed_by_duplicate" - | "closed_by_false_assumption" - | "closed_by_user" - | "expired_lane" - ) - }) { - push_limited(&mut closed_doors, item.clone(), limit); - } - if outcome_types - .iter() - .any(|kind| matches!(*kind, "duplicate_risk" | "closed_by_duplicate")) - { - push_limited(&mut duplicate_risk_lanes, item.clone(), limit); - } - if outcome_types.contains(&"needs_poc") { - push_limited(&mut needs_poc_lanes, item.clone(), limit); - } - if already_composed.len() < limit as usize { - already_composed.push(item); - } - if bounty_mode_lanes_full( - limit, - &already_composed, - &closed_doors, - &duplicate_risk_lanes, - &needs_poc_lanes, - ) { - break; - } - } - - if bounty_mode_lanes_full( - limit, - &already_composed, - &closed_doors, - &duplicate_risk_lanes, - &needs_poc_lanes, - ) || scanned >= MAX_SCAN_EVENTS - { - break; - } - offset += PAGE_SIZE; - } - - let never = storage - .get_never_composed_candidates(limit, tags) - .map_err(|e| e.to_string())?; - let top_weird_combinations = never.iter().take(3).cloned().collect::>(); - - Ok(serde_json::json!({ - "action": "bounty_mode", - "alreadyComposedLanes": already_composed, - "neverComposedLanes": never, - "closedDoors": closed_doors, - "duplicateRiskLanes": duplicate_risk_lanes, - "needsPocLanes": needs_poc_lanes, - "topWeirdCombinations": top_weird_combinations, - "guardrails": [ - "never-composed lane is not a finding", - "composition score is not severity", - "submit/reportable still needs source refs, scope fit, and PoC evidence" - ] - })) -} - -fn push_limited(items: &mut Vec, item: Value, limit: i32) { - if items.len() < limit as usize { - items.push(item); - } -} - -fn bounty_mode_lanes_full( - limit: i32, - already_composed: &[Value], - closed_doors: &[Value], - duplicate_risk_lanes: &[Value], - needs_poc_lanes: &[Value], -) -> bool { - let limit = limit as usize; - already_composed.len() >= limit - && closed_doors.len() >= limit - && duplicate_risk_lanes.len() >= limit - && needs_poc_lanes.len() >= limit -} - -fn composition_matches_tags( - storage: &Storage, - event: &vestige_core::CompositionEventRecord, - members: &[vestige_core::CompositionMemberRecord], - tags: Option<&[String]>, -) -> Result { - let Some(tags) = tags else { - return Ok(true); - }; - if tags.is_empty() { - return Ok(true); - } - - if json_value_has_tag(&event.metadata, tags) { - return Ok(true); - } - - for member in members { - if json_value_has_tag(&member.metadata, tags) { - return Ok(true); - } - if let Some(node) = storage - .get_node(&member.memory_id) - .map_err(|e| e.to_string())? - && node.tags.iter().any(|tag| tag_matches_filter(tag, tags)) - { - return Ok(true); - } - } - - Ok(false) -} - -fn json_value_has_tag(value: &Value, tags: &[String]) -> bool { - value - .get("tags") - .and_then(|tags_value| tags_value.as_array()) - .is_some_and(|values| { - values.iter().any(|value| { - value - .as_str() - .is_some_and(|tag| tag_matches_filter(tag, tags)) - }) - }) -} - -fn tag_matches_filter(tag: &str, filters: &[String]) -> bool { - filters - .iter() - .any(|wanted| tag == wanted || tag.starts_with(&format!("{wanted}:"))) -} - -fn label(storage: &Storage, args: &ComposedGraphArgs) -> Result { - let event_id = args - .event_id - .as_deref() - .ok_or_else(|| "event_id is required for label".to_string())?; - let outcome_type = args - .outcome_type - .as_deref() - .ok_or_else(|| "outcome_type is required for label".to_string())?; - if !OUTCOME_TYPES.contains(&outcome_type) { - return Err(format!("unsupported outcome_type: {}", outcome_type)); - } - if storage - .get_composition_event(event_id) - .map_err(|e| e.to_string())? - .is_none() - { - return Err(format!("composition event not found: {}", event_id)); - } - - let outcome = CompositionOutcomeRecord { - id: Uuid::new_v4().to_string(), - event_id: event_id.to_string(), - outcome_type: outcome_type.to_string(), - labeled_at: Utc::now(), - label_source: args - .label_source - .clone() - .unwrap_or_else(|| "user".to_string()), - confidence_delta: args.confidence_delta, - notes: args.notes.clone(), - metadata: serde_json::json!({}), - }; - storage - .record_composition_outcome(&outcome) - .map_err(|e| e.to_string())?; - - Ok(serde_json::json!({ - "action": "label", - "eventId": event_id, - "outcome": outcome, - })) -} - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::TempDir; - use vestige_core::{ - CompositionEventRecord, CompositionMemberRecord, CompositionOutcomeRecord, 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, tags: &[&str]) -> String { - storage - .ingest(IngestInput { - content: content.to_string(), - node_type: "fact".to_string(), - tags: tags.iter().map(|tag| tag.to_string()).collect(), - ..Default::default() - }) - .unwrap() - .id - } - - #[tokio::test] - async fn test_composed_graph_get_label_and_bounty_mode() { - let (storage, _dir) = test_storage(); - let first = ingest( - &storage, - "Oracle drift bounty lane", - &["protocolgate", "boundary-oracle", "settlement"], - ); - let second = ingest( - &storage, - "Withdrawal queue bounty lane", - &["protocolgate", "boundary-queue", "settlement"], - ); - let third = ingest( - &storage, - "Keeper role bounty lane", - &["protocolgate", "boundary-role", "settlement"], - ); - - let event = CompositionEventRecord { - id: "composed-graph-test".to_string(), - created_at: Utc::now(), - tool: "deep_reference".to_string(), - mode: "bounty".to_string(), - query: Some("oracle withdrawal".to_string()), - query_hash: Some("test".to_string()), - confidence: Some(0.8), - status: Some("resolved".to_string()), - output_preview: Some("compose oracle and withdrawal queue".to_string()), - metadata: serde_json::json!({}), - }; - storage - .save_composition( - &event, - &[ - CompositionMemberRecord { - event_id: event.id.clone(), - memory_id: first.clone(), - role: "primary".to_string(), - rank: 0, - trust: Some(0.8), - score: Some(0.9), - preview: None, - metadata: serde_json::json!({}), - }, - CompositionMemberRecord { - event_id: event.id.clone(), - memory_id: second.clone(), - role: "supporting".to_string(), - rank: 1, - trust: Some(0.7), - score: Some(0.8), - preview: None, - metadata: serde_json::json!({}), - }, - ], - &[], - ) - .unwrap(); - - let unrelated = ingest(&storage, "Personal planning lane", &["personal"]); - storage - .save_composition( - &CompositionEventRecord { - id: "unrelated-composed-graph-test".to_string(), - created_at: Utc::now() + chrono::Duration::seconds(10), - tool: "deep_reference".to_string(), - mode: "planning".to_string(), - query: Some("personal planning".to_string()), - query_hash: Some("unrelated".to_string()), - confidence: Some(0.4), - status: Some("resolved".to_string()), - output_preview: Some("unrelated composition".to_string()), - metadata: serde_json::json!({}), - }, - &[CompositionMemberRecord { - event_id: "unrelated-composed-graph-test".to_string(), - memory_id: unrelated, - role: "primary".to_string(), - rank: 0, - trust: Some(0.4), - score: Some(0.2), - preview: None, - metadata: serde_json::json!({}), - }], - &[CompositionOutcomeRecord { - id: "unrelated-composed-graph-outcome".to_string(), - event_id: "unrelated-composed-graph-test".to_string(), - outcome_type: "needs_poc".to_string(), - labeled_at: Utc::now(), - label_source: "test".to_string(), - confidence_delta: None, - notes: None, - metadata: serde_json::json!({}), - }], - ) - .unwrap(); - - let get_result = execute( - &storage, - Some(serde_json::json!({ - "action": "get", - "event_id": event.id - })), - ) - .await - .unwrap(); - assert_eq!(get_result["members"].as_array().unwrap().len(), 2); - - let label_result = execute( - &storage, - Some(serde_json::json!({ - "action": "label", - "event_id": "composed-graph-test", - "outcome_type": "submitted", - "notes": "submitted in test" - })), - ) - .await - .unwrap(); - assert_eq!( - label_result["outcome"]["outcomeType"].as_str(), - Some("submitted") - ); - let closed_label_result = execute( - &storage, - Some(serde_json::json!({ - "action": "label", - "event_id": "composed-graph-test", - "outcome_type": "closed_by_scope", - "notes": "closed in test" - })), - ) - .await - .unwrap(); - assert_eq!( - closed_label_result["outcome"]["outcomeType"].as_str(), - Some("closed_by_scope") - ); - let duplicate_label_result = execute( - &storage, - Some(serde_json::json!({ - "action": "label", - "event_id": "composed-graph-test", - "outcome_type": "closed_by_duplicate", - "notes": "duplicate family in test" - })), - ) - .await - .unwrap(); - assert_eq!( - duplicate_label_result["outcome"]["outcomeType"].as_str(), - Some("closed_by_duplicate") - ); - - let bounty = execute( - &storage, - Some(serde_json::json!({ - "action": "bounty_mode", - "tags": ["protocolgate"], - "limit": 1 - })), - ) - .await - .unwrap(); - let already = bounty["alreadyComposedLanes"].as_array().unwrap(); - assert_eq!(already.len(), 1); - assert!( - already[0]["event"]["id"].as_str() == Some("composed-graph-test"), - "tag-scoped bounty_mode should skip newer unrelated events before truncating" - ); - assert_eq!(bounty["closedDoors"].as_array().unwrap().len(), 1); - assert_eq!(bounty["duplicateRiskLanes"].as_array().unwrap().len(), 1); - assert!(bounty["needsPocLanes"].as_array().unwrap().is_empty()); - assert!( - bounty["neverComposedLanes"] - .as_array() - .unwrap() - .iter() - .any(|candidate| { - let first_id = candidate["firstId"].as_str().unwrap_or_default(); - let second_id = candidate["secondId"].as_str().unwrap_or_default(); - [first_id, second_id].contains(&third.as_str()) - }) - ); - } - - #[tokio::test] - async fn test_bounty_mode_paginates_tag_filter_and_matches_namespaced_tags() { - let (storage, _dir) = test_storage(); - let tagged = ingest( - &storage, - "Older tagged composition lane", - &["project:vestige", "composition"], - ); - let unrelated = ingest(&storage, "Newer unrelated lane", &["unrelated"]); - let base_time = Utc::now(); - - storage - .save_composition( - &CompositionEventRecord { - id: "older-tagged-composition".to_string(), - created_at: base_time, - tool: "deep_reference".to_string(), - mode: "research".to_string(), - query: Some("older tagged lane".to_string()), - query_hash: Some("fnv1a64:older".to_string()), - confidence: Some(0.8), - status: Some("resolved".to_string()), - output_preview: None, - metadata: serde_json::json!({}), - }, - &[CompositionMemberRecord { - event_id: "older-tagged-composition".to_string(), - memory_id: tagged, - role: "primary".to_string(), - rank: 0, - trust: Some(0.8), - score: Some(0.9), - preview: None, - metadata: serde_json::json!({}), - }], - &[], - ) - .unwrap(); - - for idx in 0..101 { - let event_id = format!("newer-unrelated-composition-{idx}"); - storage - .save_composition( - &CompositionEventRecord { - id: event_id.clone(), - created_at: base_time + chrono::Duration::seconds(i64::from(idx + 1)), - tool: "deep_reference".to_string(), - mode: "planning".to_string(), - query: Some(format!("newer unrelated lane {idx}")), - query_hash: Some(format!("fnv1a64:newer-{idx}")), - confidence: Some(0.3), - status: Some("resolved".to_string()), - output_preview: None, - metadata: serde_json::json!({}), - }, - &[CompositionMemberRecord { - event_id, - memory_id: unrelated.clone(), - role: "primary".to_string(), - rank: 0, - trust: Some(0.3), - score: Some(0.2), - preview: None, - metadata: serde_json::json!({}), - }], - &[], - ) - .unwrap(); - } - - let bounty = execute( - &storage, - Some(serde_json::json!({ - "action": "bounty_mode", - "tags": ["project"], - "limit": 1 - })), - ) - .await - .unwrap(); - let already = bounty["alreadyComposedLanes"].as_array().unwrap(); - assert_eq!(already.len(), 1); - assert_eq!( - already[0]["event"]["id"].as_str(), - Some("older-tagged-composition"), - "tag-filtered bounty_mode should page past newer unrelated events and match namespaced tags" - ); - } - - #[tokio::test] - async fn test_bounty_mode_uses_member_tag_snapshot_after_purge() { - let (storage, _dir) = test_storage(); - let tagged = ingest( - &storage, - "Tagged member that will be purged", - &["project:vestige", "composition"], - ); - - storage - .save_composition( - &CompositionEventRecord { - id: "purged-tagged-member-composition".to_string(), - created_at: Utc::now(), - tool: "deep_reference".to_string(), - mode: "research".to_string(), - query: Some("purged tagged lane".to_string()), - query_hash: Some("fnv1a64:purged".to_string()), - confidence: Some(0.6), - status: Some("closed".to_string()), - output_preview: None, - metadata: serde_json::json!({}), - }, - &[CompositionMemberRecord { - event_id: "purged-tagged-member-composition".to_string(), - memory_id: tagged.clone(), - role: "primary".to_string(), - rank: 0, - trust: Some(0.7), - score: Some(0.8), - preview: Some("Tagged member that will be purged".to_string()), - metadata: serde_json::json!({}), - }], - &[CompositionOutcomeRecord { - id: "purged-tagged-member-outcome".to_string(), - event_id: "purged-tagged-member-composition".to_string(), - outcome_type: "closed_by_scope".to_string(), - labeled_at: Utc::now(), - label_source: "test".to_string(), - confidence_delta: Some(-0.2), - notes: None, - metadata: serde_json::json!({}), - }], - ) - .unwrap(); - - storage - .purge_node(&tagged, Some("test purge")) - .expect("purge should succeed"); - - let get_result = execute( - &storage, - Some(serde_json::json!({ - "action": "get", - "event_id": "purged-tagged-member-composition" - })), - ) - .await - .unwrap(); - assert!( - get_result["members"][0].get("preview").is_none() - || get_result["members"][0]["preview"].is_null(), - "purge should scrub member preview from composed_graph get" - ); - - let bounty = execute( - &storage, - Some(serde_json::json!({ - "action": "bounty_mode", - "tags": ["project"], - "limit": 1 - })), - ) - .await - .unwrap(); - let already = bounty["alreadyComposedLanes"].as_array().unwrap(); - assert_eq!(already.len(), 1); - assert_eq!( - already[0]["event"]["id"].as_str(), - Some("purged-tagged-member-composition"), - "tag-filtered bounty_mode should use composition member tag snapshots after source memory purge" - ); - assert_eq!(bounty["closedDoors"].as_array().unwrap().len(), 1); - } - - #[tokio::test] - async fn test_bounty_mode_guardrail_buckets_are_not_truncated_by_already_limit() { - let (storage, _dir) = test_storage(); - let neutral = ingest(&storage, "Neutral release lane", &["project:vestige"]); - let closed = ingest(&storage, "Closed release lane", &["project:vestige"]); - let base_time = Utc::now(); - - storage - .save_composition( - &CompositionEventRecord { - id: "older-closed-lane".to_string(), - created_at: base_time, - tool: "deep_reference".to_string(), - mode: "release".to_string(), - query: Some("older closed lane".to_string()), - query_hash: Some("fnv1a64:older-closed".to_string()), - confidence: Some(0.3), - status: Some("closed".to_string()), - output_preview: None, - metadata: serde_json::json!({}), - }, - &[CompositionMemberRecord { - event_id: "older-closed-lane".to_string(), - memory_id: closed, - role: "primary".to_string(), - rank: 0, - trust: Some(0.5), - score: Some(0.4), - preview: None, - metadata: serde_json::json!({}), - }], - &[CompositionOutcomeRecord { - id: "older-closed-outcome".to_string(), - event_id: "older-closed-lane".to_string(), - outcome_type: "closed_by_false_assumption".to_string(), - labeled_at: base_time, - label_source: "test".to_string(), - confidence_delta: Some(-0.3), - notes: None, - metadata: serde_json::json!({}), - }], - ) - .unwrap(); - - storage - .save_composition( - &CompositionEventRecord { - id: "newer-neutral-lane".to_string(), - created_at: base_time + chrono::Duration::seconds(1), - tool: "deep_reference".to_string(), - mode: "release".to_string(), - query: Some("newer neutral lane".to_string()), - query_hash: Some("fnv1a64:newer-neutral".to_string()), - confidence: Some(0.7), - status: Some("resolved".to_string()), - output_preview: None, - metadata: serde_json::json!({}), - }, - &[CompositionMemberRecord { - event_id: "newer-neutral-lane".to_string(), - memory_id: neutral, - role: "primary".to_string(), - rank: 0, - trust: Some(0.8), - score: Some(0.8), - preview: None, - metadata: serde_json::json!({}), - }], - &[], - ) - .unwrap(); - - let bounty = execute( - &storage, - Some(serde_json::json!({ - "action": "bounty_mode", - "tags": ["project"], - "limit": 1 - })), - ) - .await - .unwrap(); - - assert_eq!( - bounty["alreadyComposedLanes"][0]["event"]["id"].as_str(), - Some("newer-neutral-lane") - ); - assert_eq!( - bounty["closedDoors"][0]["event"]["id"].as_str(), - Some("older-closed-lane"), - "guardrail buckets should keep scanning after alreadyComposedLanes reaches limit" - ); - } -} diff --git a/crates/vestige-mcp/src/tools/context.rs b/crates/vestige-mcp/src/tools/context.rs index 2df73c0..20b3fd1 100644 --- a/crates/vestige-mcp/src/tools/context.rs +++ b/crates/vestige-mcp/src/tools/context.rs @@ -69,17 +69,14 @@ pub async fn execute(storage: &Arc, args: Option) -> Result - // process abort under panic=abort) and a huge value overflows `limit * 2`. - let limit = args["limit"].as_i64().unwrap_or(10).clamp(1, 200) as i32; + let limit = args["limit"].as_i64().unwrap_or(10) as i32; let now = Utc::now(); // Get candidate memories let recall_input = RecallInput { query: query.to_string(), - limit: limit.saturating_mul(2), // Get more, then filter + limit: limit * 2, // Get more, then filter min_retention: 0.0, search_mode: SearchMode::Hybrid, valid_at: None, @@ -108,11 +105,7 @@ pub async fn execute(storage: &Arc, args: Option) -> Result Value { - serde_json::json!({ - "type": "object", - "properties": { - "topic": { - "type": "string", - "description": "Optional topic/query to scope contradiction detection. If omitted, scans recent memories." - }, - "since": { - "type": "string", - "description": "Optional RFC3339 timestamp; only memories updated after this time are considered." - }, - "min_trust": { - "type": "number", - "description": "Minimum trust score for both sides of a contradiction.", - "minimum": 0.0, - "maximum": 1.0, - "default": 0.3 - }, - "limit": { - "type": "integer", - "description": "Maximum memories to analyze before pairwise contradiction detection.", - "minimum": 2, - "maximum": 200, - "default": 50 - } - } - }) -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct ContradictionArgs { - topic: Option, - since: Option, - #[serde(alias = "min_trust")] - min_trust: Option, - limit: Option, -} - -pub async fn execute(storage: &Arc, args: Option) -> Result { - let args: ContradictionArgs = match args { - Some(value) => { - serde_json::from_value(value).map_err(|e| format!("Invalid arguments: {}", e))? - } - None => ContradictionArgs { - topic: None, - since: None, - min_trust: None, - limit: None, - }, - }; - - let limit = args.limit.unwrap_or(50).clamp(2, 200); - let min_trust = args.min_trust.unwrap_or(0.3).clamp(0.0, 1.0); - let since = match args.since.as_deref() { - Some(raw) => Some( - DateTime::parse_from_rfc3339(raw) - .map_err(|e| format!("Invalid since timestamp: {}", e))? - .with_timezone(&Utc), - ), - None => None, - }; - - let mut memories = if let Some(topic) = args.topic.as_deref().filter(|s| !s.trim().is_empty()) { - storage - .hybrid_search(topic, limit, 0.3, 0.7) - .map_err(|e| e.to_string())? - .into_iter() - .map(|result| result.node) - .collect::>() - } else { - storage.get_all_nodes(limit, 0).map_err(|e| e.to_string())? - }; - - if let Some(since) = since { - memories.retain(|memory| memory.updated_at >= since); - } - - let contradictions = find_contradictions(&memories, min_trust); - - Ok(serde_json::json!({ - "topic": args.topic, - "memoriesAnalyzed": memories.len(), - "minTrust": min_trust, - "contradictionsFound": contradictions.len(), - "contradictions": contradictions, - })) -} - -fn find_contradictions(memories: &[KnowledgeNode], min_trust: f64) -> Vec { - let mut contradictions = Vec::new(); - - for i in 0..memories.len() { - for j in (i + 1)..memories.len() { - let a = &memories[i]; - let b = &memories[j]; - let overlap = topic_overlap(&a.content, &b.content); - if overlap < 0.4 || !appears_contradictory(&a.content, &b.content) { - continue; - } - - let a_trust = trust_for(a); - let b_trust = trust_for(b); - if a_trust.min(b_trust) < min_trust { - continue; - } - - let (stronger, stronger_trust, weaker, weaker_trust) = if a_trust >= b_trust { - (a, a_trust, b, b_trust) - } else { - (b, b_trust, a, a_trust) - }; - - contradictions.push(serde_json::json!({ - "stronger": memory_card(stronger, stronger_trust), - "weaker": memory_card(weaker, weaker_trust), - "topicOverlap": overlap, - })); - } - } - - contradictions.sort_by(|a, b| { - let a_overlap = a["topicOverlap"].as_f64().unwrap_or(0.0); - let b_overlap = b["topicOverlap"].as_f64().unwrap_or(0.0); - b_overlap - .partial_cmp(&a_overlap) - .unwrap_or(std::cmp::Ordering::Equal) - }); - contradictions -} - -fn trust_for(memory: &KnowledgeNode) -> f64 { - compute_trust( - memory.retention_strength, - memory.stability, - memory.reps, - memory.lapses, - ) -} - -fn memory_card(memory: &KnowledgeNode, trust: f64) -> Value { - serde_json::json!({ - "id": memory.id.clone(), - "preview": memory.content.chars().take(200).collect::(), - "trust": (trust * 100.0).round() / 100.0, - "updatedAt": memory.updated_at.to_rfc3339(), - "tags": memory.tags.clone(), - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::TempDir; - use vestige_core::IngestInput; - - async fn test_storage() -> (Arc, TempDir) { - let dir = TempDir::new().unwrap(); - let storage = Storage::new(Some(dir.path().join("test.db"))).unwrap(); - (Arc::new(storage), dir) - } - - #[tokio::test] - async fn test_contradictions_reports_conflicting_memories() { - let (storage, _dir) = test_storage().await; - storage - .ingest(IngestInput { - content: - "For the release workflow we always run cargo test before publishing Vestige" - .to_string(), - node_type: "fact".to_string(), - ..Default::default() - }) - .unwrap(); - storage - .ingest(IngestInput { - content: - "Correction: for the release workflow we never run cargo test before publishing Vestige" - .to_string(), - node_type: "fact".to_string(), - ..Default::default() - }) - .unwrap(); - - let result = execute( - &storage, - Some(serde_json::json!({ - "topic": "release workflow cargo test Vestige", - "min_trust": 0.0 - })), - ) - .await - .unwrap(); - - assert_eq!(result["contradictionsFound"], 1); - assert!(result["contradictions"][0]["stronger"]["id"].is_string()); - } -} diff --git a/crates/vestige-mcp/src/tools/cross_reference.rs b/crates/vestige-mcp/src/tools/cross_reference.rs index 2a37578..531e934 100644 --- a/crates/vestige-mcp/src/tools/cross_reference.rs +++ b/crates/vestige-mcp/src/tools/cross_reference.rs @@ -20,10 +20,9 @@ use serde::Deserialize; use serde_json::Value; use std::sync::Arc; use tokio::sync::Mutex; -use uuid::Uuid; use crate::cognitive::CognitiveEngine; -use vestige_core::{CompositionEventRecord, CompositionMemberRecord, Storage}; +use vestige_core::Storage; /// Input schema for deep_reference / cross_reference tool pub fn schema() -> Value { @@ -59,7 +58,7 @@ struct DeepRefArgs { /// Compute trust score from FSRS-6 memory state. /// Higher = more trustworthy (frequently accessed, high retention, stable, few lapses). -pub(crate) fn compute_trust(retention: f64, stability: f64, reps: i32, lapses: i32) -> f64 { +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; @@ -385,7 +384,7 @@ const CORRECTION_SIGNALS: &[&str] = &[ "migrated to", ]; -pub(crate) fn appears_contradictory(a: &str, b: &str) -> bool { +fn appears_contradictory(a: &str, b: &str) -> bool { let a_lower = a.to_lowercase(); let b_lower = b.to_lowercase(); @@ -436,7 +435,7 @@ pub(crate) fn appears_contradictory(a: &str, b: &str) -> bool { false } -pub(crate) fn topic_overlap(a: &str, b: &str) -> f32 { +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> = @@ -510,12 +509,10 @@ pub async fn execute( "confidence": 0.0, "guidance": "No memories found. Use smart_ingest to add memories.", "memoriesAnalyzed": 0, - "compositionWriteStatus": "skipped_empty", })); } let mut ranked = results; - #[cfg(feature = "vector-search")] if let Ok(mut cog) = cognitive.try_lock() { let candidates: Vec<_> = ranked .iter() @@ -660,36 +657,6 @@ pub async fn execute( } } - // ==================================================================== - // STAGE 5b: CLAIM-vs-MEMORY contradiction (the structural fix). - // The original engine only compared stored memory PAIRS — it never tested - // the user's QUERY against memory, so "your claim X contradicts stored - // memory Y" was invisible (confident silence, the dangerous failure). Here - // we test args.query against each analyzed memory so a claim that conflicts - // with a high-trust memory surfaces and lowers confidence. - let mut claim_conflicts: Vec = Vec::new(); - for m in scored.iter() { - if m.trust < 0.3 { - continue; - } - let overlap = topic_overlap(&args.query, &m.content); - if overlap < 0.4 { - continue; - } - if appears_contradictory(&args.query, &m.content) { - claim_conflicts.push(serde_json::json!({ - "claim": args.query.chars().take(160).collect::(), - "conflicting_memory": { - "id": m.id, - "preview": m.content.chars().take(150).collect::(), - "trust": (m.trust * 100.0).round() / 100.0, - "date": m.updated_at.to_rfc3339(), - }, - "topic_overlap": overlap, - })); - } - } - // ==================================================================== // STAGE 6: Dream Insight Integration // ==================================================================== @@ -721,10 +688,10 @@ pub async fn execute( // it contains at least one of these terms. This catches the class of bug // where a high-trust, semantically-adjacent memory from an unrelated // domain beats the actual topic memory because the cross-encoder reranker - // over-weights token-level similarity (e.g. an unrelated security memory - // about "true positives + conservative thresholds" winning an "FSRS-6 trust + // 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 + // 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", @@ -852,7 +819,6 @@ pub async fn execute( "id": s.id, "preview": s.content.chars().take(200).collect::(), "trust": (s.trust * 100.0).round() / 100.0, - "relevanceScore": ((composite(s) * 100.0).round() / 100.0), "date": s.updated_at.to_rfc3339(), "role": if i == 0 { "primary" } else { "supporting" }, }) @@ -878,16 +844,10 @@ pub async fn execute( // 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); - // A claim that conflicts with a stored memory is the strongest possible signal - // to lower confidence (heavier penalty than an inter-memory disagreement). - let contradiction_penalty = - (contradictions.len() as f64 * 0.1) + (claim_conflicts.len() as f64 * 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 !claim_conflicts.is_empty() { - // The claim itself conflicts with stored memory — never report "resolved". - "claim_contradicts_memory" - } else if contradictions.is_empty() && confidence > 0.7 { + let status = if contradictions.is_empty() && confidence > 0.7 { "resolved" } else if !contradictions.is_empty() { "contradictions_found" @@ -897,13 +857,7 @@ pub async fn execute( "partial_evidence" }; - let guidance = if !claim_conflicts.is_empty() { - format!( - "CAUTION: your claim conflicts with {} stored memor{}. Do NOT treat this as resolved — review the conflicting memory(ies) below before acting.", - claim_conflicts.len(), - if claim_conflicts.len() == 1 { "y" } else { "ies" } - ) - } else if let Some(rec) = recommended { + let guidance = if let Some(rec) = recommended { if contradictions.is_empty() { format!( "High confidence ({:.0}%). Recommended memory (trust {:.0}%, {}) is the most reliable source.", @@ -945,10 +899,6 @@ pub async fn execute( "activationExpanded": activation_expanded, }); - if !claim_conflicts.is_empty() { - response["claim_conflicts"] = serde_json::json!(claim_conflicts); - } - if let Some(rec) = recommended { response["recommended"] = serde_json::json!({ "answer_preview": rec.content.chars().take(300).collect::(), @@ -974,163 +924,9 @@ pub async fn execute( response["related_insights"] = serde_json::json!(related_insights); } - match persist_deep_reference_composition(storage, &args.query, &intent, &response) { - Ok(Some(event_id)) => { - response["composition_event_id"] = serde_json::json!(event_id); - response["compositionWriteStatus"] = serde_json::json!("persisted"); - } - Ok(None) => { - response["compositionWriteStatus"] = serde_json::json!("skipped_empty"); - } - Err(err) => { - tracing::warn!( - "Failed to persist deep_reference composition event: {}", - err - ); - response["compositionWriteStatus"] = serde_json::json!("failed"); - } - } - Ok(response) } -fn persist_deep_reference_composition( - storage: &Arc, - query: &str, - intent: &QueryIntent, - response: &Value, -) -> Result, String> { - let event_id = Uuid::new_v4().to_string(); - let event = CompositionEventRecord { - id: event_id.clone(), - created_at: Utc::now(), - tool: "deep_reference".to_string(), - mode: "deep_reference".to_string(), - query: Some(query.to_string()), - query_hash: Some(query_hash(query)), - confidence: response.get("confidence").and_then(|v| v.as_f64()), - status: response - .get("status") - .and_then(|v| v.as_str()) - .map(ToOwned::to_owned), - output_preview: response - .get("guidance") - .and_then(|v| v.as_str()) - .map(|value| preview_text(value, 280)), - metadata: serde_json::json!({ - "intent": format!("{:?}", intent), - "memoriesAnalyzed": response.get("memoriesAnalyzed").and_then(|v| v.as_u64()).unwrap_or(0), - "activationExpanded": response.get("activationExpanded").and_then(|v| v.as_u64()).unwrap_or(0), - "reasoningPreview": response.get("reasoning").and_then(|v| v.as_str()).map(|value| preview_text(value, 600)), - }), - }; - - let mut members = Vec::new(); - if let Some(evidence) = response.get("evidence").and_then(|v| v.as_array()) { - for (idx, item) in evidence.iter().enumerate() { - let Some(memory_id) = item.get("id").and_then(|v| v.as_str()) else { - continue; - }; - let role = item - .get("role") - .and_then(|v| v.as_str()) - .unwrap_or(if idx == 0 { "primary" } else { "supporting" }); - members.push(CompositionMemberRecord { - event_id: event_id.clone(), - memory_id: memory_id.to_string(), - role: role.to_string(), - rank: idx as i32, - trust: item.get("trust").and_then(|v| v.as_f64()), - score: item - .get("relevanceScore") - .or_else(|| item.get("relevance_score")) - .and_then(|v| v.as_f64()), - preview: None, - metadata: serde_json::json!({ - "roleSource": "deep_reference_evidence", - "evidenceRank": idx, - "date": item.get("date").and_then(|v| v.as_str()), - }), - }); - } - } - - if let Some(contradictions) = response.get("contradictions").and_then(|v| v.as_array()) { - for (idx, contradiction) in contradictions.iter().enumerate() { - for side in ["stronger", "weaker"] { - let Some(item) = contradiction.get(side) else { - continue; - }; - let Some(memory_id) = item.get("id").and_then(|v| v.as_str()) else { - continue; - }; - members.push(CompositionMemberRecord { - event_id: event_id.clone(), - memory_id: memory_id.to_string(), - role: "contradicting".to_string(), - rank: idx as i32, - trust: item.get("trust").and_then(|v| v.as_f64()), - score: contradiction.get("topic_overlap").and_then(|v| v.as_f64()), - preview: None, - metadata: serde_json::json!({ - "roleSource": "deep_reference_contradiction", - "side": side, - "date": item.get("date").and_then(|v| v.as_str()), - }), - }); - } - } - } - - if let Some(superseded) = response.get("superseded").and_then(|v| v.as_array()) { - for (idx, item) in superseded.iter().enumerate() { - let Some(memory_id) = item.get("id").and_then(|v| v.as_str()) else { - continue; - }; - members.push(CompositionMemberRecord { - event_id: event_id.clone(), - memory_id: memory_id.to_string(), - role: "superseded".to_string(), - rank: idx as i32, - trust: item.get("trust").and_then(|v| v.as_f64()), - score: None, - preview: None, - metadata: serde_json::json!({ - "roleSource": "deep_reference_superseded", - "superseded_by": item.get("superseded_by").and_then(|v| v.as_str()), - "date": item.get("date").and_then(|v| v.as_str()), - }), - }); - } - } - - if members.is_empty() { - return Ok(None); - } - - storage - .save_composition(&event, &members, &[]) - .map_err(|e| e.to_string())?; - Ok(Some(event_id)) -} - -fn query_hash(query: &str) -> String { - let mut hash = 0xcbf29ce484222325u64; - for byte in query.as_bytes() { - hash ^= u64::from(*byte); - hash = hash.wrapping_mul(0x100000001b3); - } - format!("fnv1a64:{hash:016x}") -} - -fn preview_text(value: &str, max: usize) -> String { - let collapsed = value.replace('\n', " "); - if collapsed.len() <= max { - return collapsed; - } - format!("{}...", &collapsed[..collapsed.floor_char_boundary(max)]) -} - // ============================================================================ // TESTS // ============================================================================ @@ -1165,7 +961,6 @@ mod tests { tags: tags.iter().map(|s| s.to_string()).collect(), valid_from: None, valid_until: None, - source_envelope: None, }) .unwrap() .id @@ -1214,99 +1009,6 @@ mod tests { ); } - #[tokio::test] - async fn test_deep_reference_persists_composition_event() { - let (storage, _dir) = test_storage().await; - - let primary_id = ingest_one( - &storage, - "ProtocolGate control-plane composition tracks global invariant local gate bypasses.", - &["protocolgate", "boundary-scope"], - ) - .await; - let supporting_id = ingest_one( - &storage, - "ProtocolGate global invariant local gate research used Aave account-global health factor and route-local validation.", - &["protocolgate", "boundary-scope"], - ) - .await; - - let result = execute( - &storage, - &test_cognitive(), - Some(serde_json::json!({ - "query": "ProtocolGate global invariant local gate", - "depth": 10 - })), - ) - .await - .expect("execute should succeed"); - - let event_id = result["composition_event_id"] - .as_str() - .expect("deep_reference should return persisted event id"); - assert_eq!(result["compositionWriteStatus"].as_str(), Some("persisted")); - - let event = storage - .get_composition_event(event_id) - .unwrap() - .expect("composition event should be stored"); - assert_eq!(event.tool, "deep_reference"); - assert_eq!( - event.query.as_deref(), - Some("ProtocolGate global invariant local gate") - ); - - let members = storage.get_composition_members(event_id).unwrap(); - assert!(members.iter().any(|member| member.memory_id == primary_id)); - assert!( - members - .iter() - .any(|member| member.memory_id == supporting_id) - ); - assert!(members.iter().any(|member| member.role == "primary")); - assert!( - members.iter().any(|member| { - member.memory_id == primary_id - && member.score.is_some() - && member.metadata["roleSource"] == "deep_reference_evidence" - }), - "persisted members should retain relevance score and role source" - ); - } - - #[tokio::test] - async fn test_deep_reference_skips_empty_composition_event() { - let (storage, _dir) = test_storage().await; - - let result = execute( - &storage, - &test_cognitive(), - Some(serde_json::json!({ - "query": "no memories exist for this query", - "depth": 10 - })), - ) - .await - .expect("execute should succeed"); - - assert_eq!( - result["compositionWriteStatus"].as_str(), - Some("skipped_empty") - ); - assert!( - result.get("composition_event_id").is_none(), - "empty evidence should not create a composition event" - ); - assert!( - storage - .get_recent_composition_events(10) - .unwrap() - .is_empty(), - "ledger should stay empty when no memories participated" - ); - } - // ======================================================================== // Confidence sanity: must vary with query relevance. // ======================================================================== @@ -1412,90 +1114,6 @@ mod tests { )); } - // ======================================================================== - // STAGE 5b AUDIT: a NON-contradicting claim must NOT set - // status=claim_contradicts_memory; a contradicting claim MUST. - // ======================================================================== - #[tokio::test] - async fn audit_stage5b_noncontradicting_claim_is_not_flagged() { - let (storage, _dir) = test_storage().await; - - // High-overlap, AGREEING memory: same subject, same stance. - ingest_one( - &storage, - "Vestige uses USearch HNSW for vector search with cosine similarity \ - and Matryoshka truncation to 256 dimensions for storage savings.", - &["vestige", "vector-search"], - ) - .await; - - // Claim that AGREES (no negation, no correction marker, same subject). - let args = serde_json::json!({ - "query": "Vestige uses USearch HNSW for vector search with cosine \ - similarity and Matryoshka truncation to 256 dimensions" - }); - let result = execute(&storage, &test_cognitive(), Some(args)) - .await - .expect("execute should succeed"); - - // Non-vacuous: the memory MUST have been retrieved (else the assertion - // below would pass trivially via the no_memories early-return). - assert!( - result["memoriesAnalyzed"].as_i64().unwrap_or(0) >= 1, - "Expected the agreeing memory to be retrieved (memoriesAnalyzed>=1). Got {:?}", - result["memoriesAnalyzed"] - ); - assert_ne!( - result["status"].as_str(), - Some("claim_contradicts_memory"), - "A NON-contradicting (agreeing) claim must not be flagged. Got status={:?}, claim_conflicts={:?}", - result["status"], - result.get("claim_conflicts") - ); - assert!( - result.get("claim_conflicts").is_none(), - "No claim_conflicts array should be present for an agreeing claim. Got {:?}", - result.get("claim_conflicts") - ); - } - - // STAGE 5b decision predicate, tested directly. The end-to-end `execute` - // path cannot surface a genuinely-contradicting claim in a test env with no - // embeddings model loaded, because keyword retrieval is implicit-AND and a - // contradicting claim by construction carries a stance word the memory - // lacks. This asserts the exact gate STAGE 5b applies once a memory is - // retrieved: topic_overlap >= 0.4 AND appears_contradictory(query, memory). - #[test] - fn audit_stage5b_gate_predicate_distinguishes_agree_vs_contradict() { - let memory = "USearch HNSW vector search Vestige production cosine similarity \ - recall correct should always be enabled because it is fast"; - - // Agreeing claim: high overlap, NO stance flip → must NOT trip the gate. - let agree = "USearch HNSW vector search Vestige production cosine similarity \ - recall correct should always be enabled because it is fast"; - assert!( - topic_overlap(agree, memory) >= 0.4, - "agree/memory should share topic" - ); - assert!( - !appears_contradictory(agree, memory), - "An agreeing claim must NOT be flagged as contradictory (false-positive guard)" - ); - - // Contradicting claim: same subject + a negation marker ("never"/"avoid") - // present in exactly one side → must trip the gate. - let contradict = "USearch HNSW vector search Vestige production cosine similarity \ - recall avoid never enabled"; - assert!( - topic_overlap(contradict, memory) >= 0.4, - "contradict/memory should share topic" - ); - assert!( - appears_contradictory(contradict, memory), - "A same-subject negated claim MUST be flagged as contradictory" - ); - } - #[test] fn test_topic_overlap_similar() { let overlap = topic_overlap( @@ -1543,7 +1161,7 @@ mod tests { QueryIntent::Timeline ); assert_eq!( - classify_intent("How has the benchmark score evolved over time?"), + classify_intent("How has the AIMO3 score evolved over time?"), QueryIntent::Timeline ); } @@ -1575,7 +1193,7 @@ mod tests { #[test] fn test_intent_synthesis_default() { assert_eq!( - classify_intent("Tell me about the user's projects"), + classify_intent("Tell me about Sam's projects"), QueryIntent::Synthesis ); assert_eq!(classify_intent("What is Vestige?"), QueryIntent::Synthesis); diff --git a/crates/vestige-mcp/src/tools/dedup.rs b/crates/vestige-mcp/src/tools/dedup.rs index 45b7988..8456629 100644 --- a/crates/vestige-mcp/src/tools/dedup.rs +++ b/crates/vestige-mcp/src/tools/dedup.rs @@ -4,10 +4,8 @@ //! cosine similarity on stored embeddings. Uses union-find for //! efficient clustering. -#[cfg(all(feature = "embeddings", feature = "vector-search"))] use serde::Deserialize; use serde_json::Value; -#[cfg(all(feature = "embeddings", feature = "vector-search"))] use std::collections::HashMap; use std::sync::Arc; @@ -45,7 +43,6 @@ pub fn schema() -> Value { #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] -#[cfg(all(feature = "embeddings", feature = "vector-search"))] struct DedupArgs { #[serde(alias = "similarity_threshold")] similarity_threshold: Option, @@ -54,13 +51,11 @@ struct DedupArgs { } /// Simple union-find for clustering -#[cfg(all(feature = "embeddings", feature = "vector-search"))] struct UnionFind { parent: Vec, rank: Vec, } -#[cfg(all(feature = "embeddings", feature = "vector-search"))] impl UnionFind { fn new(n: usize) -> Self { Self { @@ -94,22 +89,21 @@ impl UnionFind { } pub async fn execute(storage: &Arc, args: Option) -> Result { + let args: DedupArgs = match args { + Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, + None => DedupArgs { + similarity_threshold: None, + limit: None, + tags: None, + }, + }; + + let threshold = args.similarity_threshold.unwrap_or(0.80) as f32; + let limit = args.limit.unwrap_or(20); + let tag_filter = args.tags.unwrap_or_default(); + #[cfg(all(feature = "embeddings", feature = "vector-search"))] { - let args: DedupArgs = match args { - Some(v) => { - serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))? - } - None => DedupArgs { - similarity_threshold: None, - limit: None, - tags: None, - }, - }; - let threshold = args.similarity_threshold.unwrap_or(0.80) as f32; - let limit = args.limit.unwrap_or(20); - let tag_filter = args.tags.unwrap_or_default(); - // Load all embeddings let all_embeddings = storage .get_all_embeddings() @@ -267,8 +261,6 @@ pub async fn execute(storage: &Arc, args: Option) -> Result, args: Option) -> Result Value { - serde_json::json!({ - "type": "object", - "properties": { - "action": { - "type": "string", - "enum": ["scan", "plan_merge", "plan_supersede", "apply", "undo", "protect", "policy"], - "default": "scan", - "description": "What to do. 'scan' (default): surface duplicate clusters (cosine) AND merge candidates (Fellegi-Sunter), read-only. 'plan_merge'/'plan_supersede': preview a reversible plan without applying (returns plan_id). 'apply': execute a plan_id. 'undo': reverse a prior operation (omit operation_id to list the reflog). 'protect': pin a memory against auto-merge/supersede/forget. 'policy': get/set Fellegi-Sunter thresholds." - }, - "similarity_threshold": { - "type": "number", - "description": "[scan] Minimum cosine similarity for duplicate clusters (0.5-1.0, default 0.80).", - "minimum": 0.5, "maximum": 1.0 - }, - "limit": { - "type": "integer", - "description": "[scan] Max clusters/candidates to return (default 20).", - "minimum": 1, "maximum": 100 - }, - "tags": { - "type": "array", "items": { "type": "string" }, - "description": "[scan] Optional: only consider memories with these tags (ANY match)." - }, - "member_ids": { - "type": "array", "items": { "type": "string" }, - "description": "[plan_merge] IDs of memories to merge (>= 2). Survivor kept; rest bitemporally invalidated." - }, - "survivor_id": { "type": "string", "description": "[plan_merge] Optional: which member to keep (defaults to highest-retention)." }, - "old_id": { "type": "string", "description": "[plan_supersede] Memory being superseded (kept, marked invalid)." }, - "new_id": { "type": "string", "description": "[plan_supersede] Memory that supersedes the old one." }, - "plan_id": { "type": "string", "description": "[apply] ID of a plan produced by plan_merge/plan_supersede." }, - "confirm": { "type": "boolean", "default": false, "description": "[apply] Required true for 'possible'/'non_match' plans." }, - "operation_id": { "type": "string", "description": "[undo] Operation to reverse. Omit to list the reflog." }, - "id": { "type": "string", "description": "[protect] Memory id to protect/unprotect." }, - "protected": { "type": "boolean", "default": true, "description": "[protect] true to pin, false to unpin." }, - "match_threshold": { "type": "number", "minimum": 0.0, "maximum": 1.0, "description": "[policy] Score >= this => 'match'." }, - "possible_threshold": { "type": "number", "minimum": 0.0, "maximum": 1.0, "description": "[policy] Score in [possible, match) => review." }, - "auto_apply": { "type": "boolean", "description": "[policy] Allow 'match' plans to apply without confirm. Default false." } - } - }) -} - -/// Unified dispatcher for the `dedup` tool. Routes on `action` (default `scan`). -pub async fn execute_unified(storage: &Arc, args: Option) -> Result { - let action = args - .as_ref() - .and_then(|a| a.get("action")) - .and_then(|v| v.as_str()) - .unwrap_or("scan") - .to_string(); - - match action.as_str() { - "scan" => { - // Cosine-similarity duplicate clusters (this module). - let clusters = execute(storage, args.clone()).await?; - // Fellegi-Sunter merge candidates (merge module, name-dispatched). - let candidates = - super::merge::execute(storage, "merge_candidates", args.clone()).await?; - Ok(serde_json::json!({ - "action": "scan", - "duplicateClusters": clusters, - "mergeCandidates": candidates, - "nextStep": "Use action='plan_merge' (member_ids) or action='plan_supersede' (old_id,new_id) to preview a reversible plan, then action='apply' (plan_id)." - })) - } - "plan_merge" => super::merge::execute(storage, "plan_merge", args).await, - "plan_supersede" => super::merge::execute(storage, "plan_supersede", args).await, - "apply" => super::merge::execute(storage, "apply_plan", args).await, - "undo" => super::merge::execute(storage, "merge_undo", args).await, - "protect" => super::merge::execute(storage, "protect", args).await, - "policy" => super::merge::execute(storage, "merge_policy", args).await, - other => Err(format!( - "Unknown dedup action '{other}'. Use scan|plan_merge|plan_supersede|apply|undo|protect|policy." - )), - } -} - #[cfg(test)] mod tests { use super::*; @@ -381,26 +280,6 @@ mod tests { } #[test] - fn test_unified_schema() { - let schema = unified_schema(); - assert_eq!(schema["type"], "object"); - let actions = schema["properties"]["action"]["enum"].as_array().unwrap(); - assert_eq!(actions.len(), 7); - assert_eq!(schema["properties"]["action"]["default"], "scan"); - } - - #[tokio::test] - async fn test_unified_scan_empty_storage() { - let dir = tempfile::TempDir::new().unwrap(); - let storage = Storage::new(Some(dir.path().join("test.db"))).unwrap(); - let storage = Arc::new(storage); - // Default action (scan) on empty storage must not error. - let result = execute_unified(&storage, None).await; - assert!(result.is_ok()); - } - - #[test] - #[cfg(all(feature = "embeddings", feature = "vector-search"))] fn test_union_find() { let mut uf = UnionFind::new(5); uf.union(0, 1); diff --git a/crates/vestige-mcp/src/tools/dream.rs b/crates/vestige-mcp/src/tools/dream.rs index 1456406..357315f 100644 --- a/crates/vestige-mcp/src/tools/dream.rs +++ b/crates/vestige-mcp/src/tools/dream.rs @@ -16,13 +16,6 @@ pub fn schema() -> serde_json::Value { "type": "integer", "description": "Number of recent memories to dream about (default: 50)", "default": 50 - }, - "min_similarity": { - "type": "number", - "description": "Minimum similarity for connection discovery (0.0-1.0, default: 0.5)", - "minimum": 0.0, - "maximum": 1.0, - "default": 0.5 } } }) @@ -39,11 +32,6 @@ pub async fn execute( .and_then(|v| v.as_u64()) .unwrap_or(50) .min(500) as usize; // Cap at 500 to prevent O(N^2) hang - let min_similarity = args - .as_ref() - .and_then(|a| a.get("min_similarity")) - .and_then(|v| v.as_f64()) - .map(|v| v.clamp(0.0, 1.0)); // v1.9.0: Waking SWR tagging — preferential replay of tagged memories (70/30 split) let tagged_nodes = storage @@ -107,18 +95,15 @@ pub async fn execute( .collect(); let cog = cognitive.lock().await; - let (dream_result, new_connections) = if let Some(min_similarity) = min_similarity { - let config = vestige_core::DreamConfig { - min_similarity, - ..vestige_core::DreamConfig::default() - }; - cog.dreamer - .dream_with_config_and_connections(&dream_memories, config) - .await - } else { - cog.dreamer.dream_with_connections(&dream_memories).await - }; + // 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); // v2.1.0: Persist dream insights to database (Bug #4 fix) @@ -141,10 +126,17 @@ pub async fn execute( } } + // Identify new connections from this dream by timestamp rather than buffer + // position — positional slicing is broken after composite-score eviction + // reorders the buffer. + let new_connections: Vec<&vestige_core::DiscoveredConnection> = all_connections + .iter() + .filter(|c| c.discovered_at >= dream_start) + .collect(); let mut connections_persisted = 0u64; { let now = Utc::now(); - for conn in &new_connections { + for conn in new_connections.iter() { let link_type = match conn.connection_type { vestige_core::DiscoveredConnectionType::Semantic => "semantic", vestige_core::DiscoveredConnectionType::SharedConcept => "shared_concepts", @@ -186,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, @@ -283,7 +275,6 @@ mod tests { tags: vec!["dream-test".to_string()], valid_from: None, valid_until: None, - source_envelope: None, }) .unwrap(); } @@ -295,9 +286,6 @@ mod tests { assert_eq!(s["type"], "object"); assert!(s["properties"]["memory_count"].is_object()); assert_eq!(s["properties"]["memory_count"]["default"], 50); - assert!(s["properties"]["min_similarity"].is_object()); - assert_eq!(s["properties"]["min_similarity"]["minimum"], 0.0); - assert_eq!(s["properties"]["min_similarity"]["maximum"], 1.0); } #[tokio::test] @@ -421,7 +409,6 @@ mod tests { tags: vec!["dream-roundtrip".to_string()], valid_from: None, valid_until: None, - source_envelope: None, }) .unwrap(); } @@ -487,7 +474,6 @@ mod tests { tags: vec!["save-conn-test".to_string()], valid_from: None, valid_until: None, - source_envelope: None, }) .unwrap(); ids.push(result.id); @@ -591,7 +577,6 @@ mod tests { tags: tags.iter().map(|t| t.to_string()).collect(), valid_from: None, valid_until: None, - source_envelope: None, }) .unwrap(); } @@ -631,42 +616,6 @@ mod tests { ); } - #[tokio::test] - async fn test_dream_persists_dense_connection_set_above_legacy_buffer_cap() { - let (storage, _dir) = test_storage().await; - ingest_n_memories(&storage, 50).await; - - let result = execute( - &storage, - &test_cognitive(), - Some(serde_json::json!({ - "memory_count": 50, - "min_similarity": 0.1 - })), - ) - .await - .unwrap(); - - assert_eq!(result["status"], "dreamed"); - let found = result["stats"]["new_connections_found"] - .as_u64() - .unwrap_or(0); - let persisted = result["connectionsPersisted"].as_u64().unwrap_or(0); - - assert!( - found > 1_000, - "test setup should discover more than the legacy 1,000 connection cap" - ); - assert_eq!( - persisted, found, - "dense dreams should persist every connection discovered in the run" - ); - assert_eq!( - storage.get_all_connections().unwrap().len(), - persisted as usize - ); - } - #[tokio::test] async fn test_dream_persists_insights() { let (storage, _dir) = test_storage().await; @@ -717,7 +666,6 @@ mod tests { tags: tags.iter().map(|t| t.to_string()).collect(), valid_from: None, valid_until: None, - source_envelope: None, }) .unwrap(); } diff --git a/crates/vestige-mcp/src/tools/explore.rs b/crates/vestige-mcp/src/tools/explore.rs index cabc7c9..441afd3 100644 --- a/crates/vestige-mcp/src/tools/explore.rs +++ b/crates/vestige-mcp/src/tools/explore.rs @@ -414,7 +414,6 @@ mod tests { tags: vec!["test".to_string()], valid_from: None, valid_until: None, - source_envelope: None, }) .unwrap() .id; @@ -429,7 +428,6 @@ mod tests { tags: vec!["test".to_string()], valid_from: None, valid_until: None, - source_envelope: None, }) .unwrap() .id; @@ -480,7 +478,6 @@ mod tests { tags: vec!["test".to_string()], valid_from: None, valid_until: None, - source_envelope: 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; @@ -532,7 +529,6 @@ mod tests { tags: vec!["test".to_string()], valid_from: None, valid_until: None, - source_envelope: 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; diff --git a/crates/vestige-mcp/src/tools/feedback.rs b/crates/vestige-mcp/src/tools/feedback.rs index a236bf2..438e594 100644 --- a/crates/vestige-mcp/src/tools/feedback.rs +++ b/crates/vestige-mcp/src/tools/feedback.rs @@ -313,7 +313,6 @@ mod tests { tags: vec![], valid_from: None, valid_until: None, - source_envelope: None, }) .unwrap(); node.id @@ -557,7 +556,6 @@ mod tests { tags: vec![], valid_from: None, valid_until: None, - source_envelope: None, }) .unwrap(); let node_id = node.id.clone(); diff --git a/crates/vestige-mcp/src/tools/graph.rs b/crates/vestige-mcp/src/tools/graph.rs index 904e5e4..13ca746 100644 --- a/crates/vestige-mcp/src/tools/graph.rs +++ b/crates/vestige-mcp/src/tools/graph.rs @@ -328,7 +328,6 @@ mod tests { tags: vec!["test".to_string()], valid_from: None, valid_until: None, - source_envelope: None, }) .unwrap(); @@ -356,7 +355,6 @@ mod tests { tags: vec!["science".to_string()], valid_from: None, valid_until: None, - source_envelope: None, }) .unwrap(); @@ -380,7 +378,6 @@ mod tests { tags: vec![], valid_from: None, valid_until: None, - source_envelope: None, }) .unwrap(); diff --git a/crates/vestige-mcp/src/tools/graph_unified.rs b/crates/vestige-mcp/src/tools/graph_unified.rs deleted file mode 100644 index ab8a4dc..0000000 --- a/crates/vestige-mcp/src/tools/graph_unified.rs +++ /dev/null @@ -1,140 +0,0 @@ -//! Unified `graph` Tool (v2.2 — Tool Consolidation) -//! -//! Folds four graph/association/prediction tools into one action-dispatched -//! surface: -//! -//! action ∈ { -//! chain, associations, bridges, // former explore_connections -//! predict, // former predict -//! memory_graph, // former memory_graph (viz subgraph) -//! recent, get, memory, neighbors, // former composed_graph -//! never_composed, bounty_mode, label, // " -//! } -//! -//! This is a transparent facade: each action forwards the *same* args envelope -//! to the existing handler, which re-reads its own discriminator/params. None of -//! the underlying arg structs use `deny_unknown_fields`, so unrelated fields are -//! ignored. All actions are read-only EXCEPT `label`, which writes a composition -//! outcome (the one mutator) and is logged for audit. - -use serde_json::Value; -use std::sync::Arc; -use tokio::sync::Mutex; - -use vestige_core::Storage; - -use crate::cognitive::CognitiveEngine; -// Reuse composed_graph's canonical outcome-label vocabulary (do not re-list). -use super::composed_graph::OUTCOME_TYPES; - -/// Discriminated-union schema for the unified `graph` tool. -pub fn schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "action": { - "type": "string", - "enum": [ - "chain", "associations", "bridges", - "predict", "memory_graph", - "recent", "get", "memory", "neighbors", - "never_composed", "bounty_mode", "label" - ], - "description": "Graph operation. Reasoning paths: 'chain' (from→to), 'associations' (related via spreading activation, needs 'from'), 'bridges' (connectors between from/to). 'predict' (what memories you'll need next, from 'context'). 'memory_graph' (force-directed subgraph for viz, from 'center_id' or 'query'). Composition topology: 'recent', 'get' (event_id), 'memory' (memory_id), 'neighbors' (memory_id), 'never_composed', 'bounty_mode', 'label' (record an outcome — the only write)." - }, - // --- explore (chain/associations/bridges) --- - "from": { "type": "string", "description": "[chain/associations/bridges] Source memory ID." }, - "to": { "type": "string", "description": "[chain/bridges] Target memory ID." }, - // --- predict --- - "context": { "type": "object", "description": "[predict] Current context (current_file, current_topics, codebase)." }, - // --- memory_graph (viz subgraph) --- - "center_id": { "type": "string", "description": "[memory_graph] Center node id (or use 'query')." }, - "query": { "type": "string", "description": "[memory_graph] Pick a center node by search query." }, - "depth": { "type": "integer", "minimum": 1, "maximum": 3, "description": "[memory_graph] Traversal depth (1-3, default 2)." }, - "max_nodes": { "type": "integer", "description": "[memory_graph] Max nodes (default 50, capped 200)." }, - // --- composed_graph --- - "event_id": { "type": "string", "description": "[get/label] Composition event id." }, - "memory_id": { "type": "string", "description": "[memory/neighbors] Memory id." }, - "tags": { "type": "array", "items": { "type": "string" }, "description": "[never_composed/bounty_mode] Optional tag filter." }, - "outcome_type": { - "type": "string", - "enum": OUTCOME_TYPES, - "description": "[label] Outcome to record for the composition (the only mutating action)." - }, - // --- shared --- - "limit": { "type": "integer", "description": "Max results (per-action defaults; clamped internally).", "minimum": 1, "maximum": 100 } - }, - "required": ["action"] - }) -} - -/// Unified dispatcher for `graph`. Routes on `action`. -pub async fn execute( - storage: &Arc, - cognitive: &Arc>, - args: Option, -) -> Result { - let action = args - .as_ref() - .and_then(|a| a.get("action")) - .and_then(|v| v.as_str()) - .ok_or("Missing 'action'. Use chain|associations|bridges|predict|memory_graph|recent|get|memory|neighbors|never_composed|bounty_mode|label.")? - .to_string(); - - match action.as_str() { - // explore_connections — re-reads its own `action` (chain/associations/bridges). - "chain" | "associations" | "bridges" => { - super::explore::execute(storage, cognitive, args).await - } - // predict — reads `context`, ignores `action`. - "predict" => super::predict::execute(storage, cognitive, args).await, - // memory_graph — reads center_id/query/depth, ignores `action`. - "memory_graph" => super::graph::execute(storage, args).await, - // composed_graph — re-reads its own `action`. `label` is the only write. - "recent" | "get" | "memory" | "neighbors" | "never_composed" | "bounty_mode" | "label" => { - if action == "label" { - let event_id = args - .as_ref() - .and_then(|a| a.get("event_id")) - .and_then(|v| v.as_str()) - .unwrap_or("?"); - let outcome = args - .as_ref() - .and_then(|a| a.get("outcome_type")) - .and_then(|v| v.as_str()) - .unwrap_or("?"); - tracing::info!( - event_id = %event_id, - outcome_type = %outcome, - "graph: composition outcome labeled" - ); - } - super::composed_graph::execute(storage, args).await - } - other => Err(format!( - "Unknown graph action '{other}'. Use chain|associations|bridges|predict|memory_graph|recent|get|memory|neighbors|never_composed|bounty_mode|label." - )), - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_schema_action_count() { - let s = schema(); - let actions = s["properties"]["action"]["enum"].as_array().unwrap(); - assert_eq!(actions.len(), 12); - // outcome_type enum is sourced from the canonical const. - let outcomes = s["properties"]["outcome_type"]["enum"].as_array().unwrap(); - assert_eq!(outcomes.len(), OUTCOME_TYPES.len()); - } - - #[test] - fn test_missing_action_errors() { - // Pure arg-shape check; no storage needed for the early return path. - let s = schema(); - assert_eq!(s["required"][0], "action"); - } -} diff --git a/crates/vestige-mcp/src/tools/health.rs b/crates/vestige-mcp/src/tools/health.rs index 563abb8..362f298 100644 --- a/crates/vestige-mcp/src/tools/health.rs +++ b/crates/vestige-mcp/src/tools/health.rs @@ -119,7 +119,6 @@ mod tests { tags: vec!["test".to_string()], valid_from: None, valid_until: None, - source_envelope: None, }) .unwrap(); } @@ -145,7 +144,6 @@ mod tests { tags: vec![], valid_from: None, valid_until: None, - source_envelope: None, }) .unwrap(); diff --git a/crates/vestige-mcp/src/tools/maintain.rs b/crates/vestige-mcp/src/tools/maintain.rs deleted file mode 100644 index 4231603..0000000 --- a/crates/vestige-mcp/src/tools/maintain.rs +++ /dev/null @@ -1,134 +0,0 @@ -//! Unified `maintain` Tool (v2.2 — Tool Consolidation) -//! -//! Folds the seven maintenance/lifecycle tools into one action-dispatched -//! surface: -//! -//! action = consolidate | dream | gc | importance_score | backup | export | restore -//! -//! This is a thin facade: each action forwards the *same* args envelope to the -//! existing handler. None of the underlying arg structs use -//! `deny_unknown_fields`, so the `action` discriminator is ignored by each -//! handler and per-action params validate as before. Safety defaults are -//! preserved because they live inside the callees: -//! - `gc` defaults `dry_run=true` (handler-internal), -//! - `restore` keeps path-confinement (handler-internal), -//! - `export` keeps its traversal guard (handler-internal). -//! -//! The `consolidate`/`dream` *Started* events and the -//! `consolidate`/`dream`/`importance_score` *Completed* events are emitted by -//! the server dispatch + `emit_tool_event` (which normalizes the `maintain` -//! name to its effective action) — not here. - -use serde_json::Value; -use std::sync::Arc; -use tokio::sync::Mutex; - -use vestige_core::Storage; - -use crate::cognitive::CognitiveEngine; - -/// Discriminated-union schema for the unified `maintain` tool. -pub fn schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "action": { - "type": "string", - "enum": ["consolidate", "dream", "gc", "importance_score", "backup", "export", "restore"], - "description": "Maintenance op. 'consolidate' (run FSRS-6 decay/embedding cycle), 'dream' (replay memories → insights/connections), 'gc' (garbage-collect stale memories; dry_run=true by default), 'importance_score' (4-channel neuroscience score for 'content'), 'backup' (SQLite DB backup), 'export' (memories as JSON/JSONL with filters), 'restore' (restore from a JSON backup at 'path')." - }, - // --- gc --- - "min_retention": { "type": "number", "minimum": 0.0, "maximum": 1.0, "description": "[gc] Collect memories below this retention (default 0.1)." }, - "dry_run": { "type": "boolean", "description": "[gc] Preview only. Defaults to TRUE for safety." }, - // --- importance_score --- - "content": { "type": "string", "description": "[importance_score] Content to score." }, - // --- export --- - "format": { "type": "string", "enum": ["json", "jsonl"], "description": "[export] Output format." }, - "tags": { "type": "array", "items": { "type": "string" }, "description": "[export] Tag filter." }, - "start": { "type": "string", "description": "[export] Start date filter (ISO 8601)." }, - "end": { "type": "string", "description": "[export] End date filter (ISO 8601)." }, - // --- backup / restore --- - "path": { "type": "string", "description": "[restore] Path to a JSON backup file (path-confined)." } - }, - "required": ["action"] - }) -} - -/// Unified dispatcher for `maintain`. Routes on `action` (required). -pub async fn execute( - storage: &Arc, - cognitive: &Arc>, - args: Option, -) -> Result { - // Clone the discriminator out before the args envelope is moved into a callee. - let action = args - .as_ref() - .and_then(|a| a.get("action")) - .and_then(|v| v.as_str()) - .ok_or("Missing 'action'. Use consolidate|dream|gc|importance_score|backup|export|restore.")? - .to_string(); - - match action.as_str() { - "consolidate" => super::maintenance::execute_consolidate(storage, args).await, - "dream" => super::dream::execute(storage, cognitive, args).await, - "gc" => super::maintenance::execute_gc(storage, args).await, - "importance_score" => super::importance::execute(storage, cognitive, args).await, - "backup" => super::maintenance::execute_backup(storage, args).await, - "export" => super::maintenance::execute_export(storage, args).await, - "restore" => super::restore::execute(storage, args).await, - other => Err(format!( - "Unknown maintain action '{other}'. Use consolidate|dream|gc|importance_score|backup|export|restore." - )), - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn test_storage() -> Arc { - let dir = tempfile::TempDir::new().unwrap(); - let storage = Storage::new(Some(dir.path().join("test.db"))).unwrap(); - std::mem::forget(dir); - Arc::new(storage) - } - - #[test] - fn test_schema_actions() { - let s = schema(); - let actions = s["properties"]["action"]["enum"].as_array().unwrap(); - assert_eq!(actions.len(), 7); - assert_eq!(s["required"][0], "action"); - } - - #[tokio::test] - async fn test_missing_action_errors() { - let storage = test_storage(); - let cognitive = Arc::new(Mutex::new(CognitiveEngine::new())); - let r = execute(&storage, &cognitive, None).await; - assert!(r.is_err(), "missing action must error"); - } - - #[tokio::test] - async fn test_gc_defaults_dry_run() { - let storage = test_storage(); - let cognitive = Arc::new(Mutex::new(CognitiveEngine::new())); - // No dry_run passed → handler default true → nothing is actually deleted. - let args = Some(serde_json::json!({ "action": "gc" })); - let r = execute(&storage, &cognitive, args).await.unwrap(); - // gc's envelope reports dry_run; assert it stayed true. - let dry = r - .get("dryRun") - .or(r.get("dry_run")) - .and_then(|v| v.as_bool()); - assert_eq!(dry, Some(true), "gc must default to dry_run=true via maintain"); - } - - #[tokio::test] - async fn test_consolidate_resolves() { - let storage = test_storage(); - let cognitive = Arc::new(Mutex::new(CognitiveEngine::new())); - let args = Some(serde_json::json!({ "action": "consolidate" })); - assert!(execute(&storage, &cognitive, args).await.is_ok()); - } -} diff --git a/crates/vestige-mcp/src/tools/maintenance.rs b/crates/vestige-mcp/src/tools/maintenance.rs index 814f513..892cf0a 100644 --- a/crates/vestige-mcp/src/tools/maintenance.rs +++ b/crates/vestige-mcp/src/tools/maintenance.rs @@ -6,31 +6,12 @@ use chrono::{NaiveDate, Utc}; use serde::Deserialize; use serde_json::Value; -use std::path::Path; use std::sync::Arc; use tokio::sync::Mutex; use crate::cognitive::CognitiveEngine; use vestige_core::{FSRSScheduler, Storage}; -fn create_private_file(path: &Path) -> std::io::Result { - #[cfg(unix)] - { - use std::os::unix::fs::OpenOptionsExt; - std::fs::OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .mode(0o600) - .open(path) - } - - #[cfg(not(unix))] - { - std::fs::File::create(path) - } -} - // ============================================================================ // SCHEMAS // ============================================================================ @@ -55,8 +36,8 @@ pub fn export_schema() -> Value { "properties": { "format": { "type": "string", - "description": "Export format: 'json' (default), 'jsonl', or 'portable' for exact Vestige-to-Vestige transfer", - "enum": ["json", "jsonl", "portable"], + "description": "Export format: 'json' (default) or 'jsonl'", + "enum": ["json", "jsonl"], "default": "json" }, "tags": { @@ -70,7 +51,7 @@ pub fn export_schema() -> Value { }, "path": { "type": "string", - "description": "Custom filename (not path). File is saved in the active Vestige data directory's exports/ folder. Default: memories-{timestamp}.{format}" + "description": "Custom filename (not path). File is saved in ~/.vestige/exports/. Default: memories-{timestamp}.{format}" } } }) @@ -105,24 +86,10 @@ pub fn gc_schema() -> Value { pub fn system_status_schema() -> Value { serde_json::json!({ "type": "object", - "properties": { - "schema_introspection": { - "type": "boolean", - "description": "When true, extends the response with a 'schema' block carrying the SQLite schema version, per-table row counts + column lists, and embedding-coverage convenience fields. Default: false (response shape unchanged). Use this for audit / migration-guard / downstream-upgrade scripts that otherwise have to read SQLite directly.", - "default": false - } - } + "properties": {} }) } -/// Arguments for the system_status tool. All optional. -#[derive(Debug, Default, Deserialize)] -#[serde(rename_all = "camelCase")] -struct SystemStatusArgs { - #[serde(alias = "schema_introspection")] - schema_introspection: Option, -} - // ============================================================================ // EXECUTE FUNCTIONS // ============================================================================ @@ -131,23 +98,11 @@ struct SystemStatusArgs { /// /// Returns system health status, full statistics, FSRS preview, /// cognitive module health, state distribution, and actionable recommendations. -/// -/// v2.1.24+: when `schema_introspection: true` is passed, the response -/// additionally carries a `schema` block with the live SQLite schema version, -/// per-table row counts + column lists, and embedding-coverage convenience -/// fields. Default off; response shape unchanged when omitted. pub async fn execute_system_status( storage: &Arc, cognitive: &Arc>, - args: Option, + _args: Option, ) -> Result { - // Parse arguments (all optional, including the args envelope itself). - let parsed: SystemStatusArgs = match args { - Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, - None => SystemStatusArgs::default(), - }; - let include_schema = parsed.schema_introspection.unwrap_or(false); - let stats = storage.get_stats().map_err(|e| e.to_string())?; // === Health assessment === @@ -162,7 +117,7 @@ pub async fn execute_system_status( }; let embedding_coverage = if stats.total_nodes > 0 { - (stats.nodes_with_active_embeddings as f64 / stats.total_nodes as f64) * 100.0 + (stats.nodes_with_embeddings as f64 / stats.total_nodes as f64) * 100.0 } else { 0.0 }; @@ -176,17 +131,12 @@ pub async fn execute_system_status( if stats.nodes_due_for_review > 10 { warnings.push("Many memories are due for review"); } - if stats.total_nodes > 0 && stats.nodes_with_active_embeddings == 0 { - warnings.push("No active-model embeddings generated - semantic search unavailable"); + 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"); } - if stats.nodes_with_mismatched_embeddings > 0 { - warnings.push( - "Stored embeddings from another model are present - run consolidate after changing embedding models", - ); - } let mut recommendations = Vec::new(); if status == "critical" { @@ -196,8 +146,8 @@ pub async fn execute_system_status( if stats.nodes_due_for_review > 5 { recommendations.push("Review due memories to strengthen retention."); } - if stats.nodes_with_active_embeddings < stats.total_nodes { - recommendations.push("Run 'consolidate' to generate active-model embeddings."); + 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."); @@ -283,9 +233,9 @@ pub async fn execute_system_status( Some(dt) => storage.count_memories_since(*dt).unwrap_or(0), None => stats.total_nodes, }; - let last_backup = storage.last_backup_timestamp(); + let last_backup = Storage::get_last_backup_timestamp(); - let mut response = serde_json::json!({ + Ok(serde_json::json!({ "tool": "system_status", // Health "status": status, @@ -299,11 +249,8 @@ pub async fn execute_system_status( "averageStorageStrength": stats.average_storage_strength, "averageRetrievalStrength": stats.average_retrieval_strength, "withEmbeddings": stats.nodes_with_embeddings, - "withActiveEmbeddings": stats.nodes_with_active_embeddings, - "mismatchedEmbeddings": stats.nodes_with_mismatched_embeddings, "embeddingCoverage": format!("{:.1}%", embedding_coverage), "embeddingModel": stats.embedding_model, - "activeEmbeddingModel": stats.active_embedding_model, "oldestMemory": stats.oldest_memory.map(|dt| dt.to_rfc3339()), "newestMemory": stats.newest_memory.map(|dt| dt.to_rfc3339()), // Distribution @@ -325,34 +272,7 @@ pub async fn execute_system_status( "lastBackupTimestamp": last_backup.map(|dt| dt.to_rfc3339()), "lastConsolidationTimestamp": last_consolidation.map(|dt| dt.to_rfc3339()), }, - }); - - // v2.1.24+: optional schema introspection block. Default off; response - // shape unchanged when omitted. - if include_schema { - let intro = storage.schema_introspection().map_err(|e| e.to_string())?; - let tables_json: Vec = intro - .tables - .iter() - .map(|t| { - serde_json::json!({ - "name": t.name, - "rows": t.rows, - "columns": t.columns, - }) - }) - .collect(); - response["schema"] = serde_json::json!({ - "schemaVersion": intro.schema_version, - "schemaVersionAppliedAt": intro.schema_version_applied_at.map(|dt| dt.to_rfc3339()), - "tables": tables_json, - "embeddingNullCount": intro.embedding_null_count, - "activeEmbeddingModel": intro.active_embedding_model, - "activeEmbeddingDimensions": intro.active_embedding_dimensions, - }); - } - - Ok(response) + })) } /// Consolidate tool @@ -379,7 +299,13 @@ pub async fn execute_consolidate( /// Backup tool pub async fn execute_backup(storage: &Arc, _args: Option) -> Result { // Determine backup path - let backup_dir = storage.sidecar_dir("backups"); + let vestige_dir = directories::ProjectDirs::from("com", "vestige", "core") + .ok_or("Could not determine data directory")?; + let backup_dir = vestige_dir + .data_dir() + .parent() + .unwrap_or(vestige_dir.data_dir()) + .join("backups"); std::fs::create_dir_all(&backup_dir) .map_err(|e| format!("Failed to create backup directory: {}", e))?; @@ -428,60 +354,13 @@ pub async fn execute_export(storage: &Arc, args: Option) -> Resu }; let format = args.format.unwrap_or_else(|| "json".to_string()); - if format != "json" && format != "jsonl" && format != "portable" { + if format != "json" && format != "jsonl" { return Err(format!( - "Invalid format '{}'. Must be 'json', 'jsonl', or 'portable'.", + "Invalid format '{}'. Must be 'json' or 'jsonl'.", format )); } - if format == "portable" { - if args.tags.as_ref().is_some_and(|tags| !tags.is_empty()) || args.since.is_some() { - return Err( - "Portable export is exact and does not support tags or since filters.".to_string(), - ); - } - - let export_dir = storage.sidecar_dir("exports"); - std::fs::create_dir_all(&export_dir) - .map_err(|e| format!("Failed to create export directory: {}", e))?; - - let export_path = match args.path { - Some(ref p) => { - let filename = std::path::Path::new(p) - .file_name() - .ok_or("Invalid export filename: must be a simple filename, not a path")?; - let name_str = filename.to_str().ok_or("Invalid filename encoding")?; - if name_str.contains("..") { - return Err("Invalid export filename: '..' not allowed".to_string()); - } - export_dir.join(filename) - } - None => { - let timestamp = Utc::now().format("%Y%m%d-%H%M%S"); - export_dir.join(format!("vestige-portable-{}.json", timestamp)) - } - }; - - let archive = storage - .export_portable_archive_to_path(&export_path) - .map_err(|e| e.to_string())?; - let file_size = std::fs::metadata(&export_path) - .map(|m| m.len()) - .unwrap_or(0); - - return Ok(serde_json::json!({ - "tool": "export", - "path": export_path.display().to_string(), - "format": "portable", - "archiveFormat": archive.archive_format, - "schemaVersion": archive.schema_version, - "tablesExported": archive.tables.len(), - "rowsExported": archive.total_rows(), - "sizeBytes": file_size, - })); - } - // Parse since date let since_date = match &args.since { Some(date_str) => { @@ -533,7 +412,13 @@ pub async fn execute_export(storage: &Arc, args: Option) -> Resu .collect(); // Determine export path — always constrained to vestige exports directory - let export_dir = storage.sidecar_dir("exports"); + let vestige_dir = directories::ProjectDirs::from("com", "vestige", "core") + .ok_or("Could not determine data directory")?; + let export_dir = vestige_dir + .data_dir() + .parent() + .unwrap_or(vestige_dir.data_dir()) + .join("exports"); std::fs::create_dir_all(&export_dir) .map_err(|e| format!("Failed to create export directory: {}", e))?; @@ -556,7 +441,7 @@ pub async fn execute_export(storage: &Arc, args: Option) -> Resu }; // Write export - let file = create_private_file(&export_path) + let file = std::fs::File::create(&export_path) .map_err(|e| format!("Failed to create export file: {}", e))?; let mut writer = std::io::BufWriter::new(file); @@ -778,7 +663,6 @@ mod tests { tags: vec![], valid_from: None, valid_until: None, - source_envelope: None, }) .unwrap(); } @@ -833,7 +717,6 @@ mod tests { tags: vec![], valid_from: None, valid_until: None, - source_envelope: None, }) .unwrap(); } @@ -846,200 +729,4 @@ mod tests { assert_eq!(triggers["savesSinceLastDream"], 3); assert!(triggers["lastDreamTimestamp"].is_null()); } - - // ======================================================================== - // SCHEMA INTROSPECTION TESTS (PR2) - // ======================================================================== - - #[test] - fn test_system_status_schema_has_schema_introspection_flag() { - let schema = system_status_schema(); - let props = &schema["properties"]; - let flag = &props["schema_introspection"]; - assert!(flag.is_object(), "schema_introspection property must exist"); - assert_eq!(flag["type"], "boolean"); - assert_eq!(flag["default"], false); - // Top-level required must NOT include this — flag is opt-in. - let required = schema.get("required"); - if let Some(req) = required { - let req_arr = req.as_array().unwrap(); - assert!(!req_arr.contains(&serde_json::json!("schema_introspection"))); - } - } - - #[tokio::test] - async fn test_system_status_without_schema_flag_omits_schema_block() { - // Backwards-compat: when the flag is not set (or false), the response - // shape is unchanged — no `schema` key. - let (storage, _dir) = test_storage().await; - let result = execute_system_status(&storage, &test_cognitive(), None).await; - assert!(result.is_ok()); - let value = result.unwrap(); - assert!( - value.get("schema").is_none(), - "schema block must NOT be present when flag is unset, got {:?}", - value.get("schema") - ); - - // Explicit false → still no schema block. - let result = execute_system_status( - &storage, - &test_cognitive(), - Some(serde_json::json!({ "schema_introspection": false })), - ) - .await; - assert!(result.is_ok()); - let value = result.unwrap(); - assert!(value.get("schema").is_none()); - } - - #[tokio::test] - async fn test_system_status_with_schema_flag_emits_schema_block() { - let (storage, _dir) = test_storage().await; - storage - .ingest(vestige_core::IngestInput { - content: "Schema introspection seed memory".to_string(), - node_type: "fact".to_string(), - source: None, - sentiment_score: 0.0, - sentiment_magnitude: 0.0, - tags: vec!["schema-test".to_string()], - valid_from: None, - valid_until: None, - source_envelope: None, - }) - .unwrap(); - - let result = execute_system_status( - &storage, - &test_cognitive(), - Some(serde_json::json!({ "schema_introspection": true })), - ) - .await; - assert!(result.is_ok(), "{:?}", result); - let value = result.unwrap(); - - // Shape assertions. - let schema_block = value - .get("schema") - .expect("schema block must be present when flag is true"); - assert!(schema_block.is_object()); - assert!( - schema_block["schemaVersion"].is_number(), - "schemaVersion must be a number, got {:?}", - schema_block["schemaVersion"] - ); - // Schema version should be >= 13 (V13 is the highest landed migration - // at the time this PR was authored). - let v = schema_block["schemaVersion"].as_u64().unwrap(); - assert!(v >= 13, "expected schema_version >= 13, got {}", v); - - // tables should be a non-empty array of {name, rows, columns}. - let tables = schema_block["tables"].as_array().unwrap(); - assert!(!tables.is_empty(), "expected at least one table"); - let kn = tables - .iter() - .find(|t| t["name"] == "knowledge_nodes") - .expect("knowledge_nodes table must be present"); - assert_eq!(kn["rows"], 1, "ingested exactly one memory"); - let cols = kn["columns"].as_array().unwrap(); - assert!(!cols.is_empty(), "knowledge_nodes must have columns"); - // The id column is universally present. - let col_names: Vec<&str> = cols.iter().filter_map(|c| c.as_str()).collect(); - assert!( - col_names.contains(&"id"), - "knowledge_nodes.id must be in columns list: {:?}", - col_names - ); - - // Convenience fields. - assert!(schema_block["embeddingNullCount"].is_number()); - // activeEmbeddingModel may be null if the `embeddings` feature is - // not enabled in the test build; just check the key exists. - assert!(schema_block.get("activeEmbeddingModel").is_some()); - assert!(schema_block.get("activeEmbeddingDimensions").is_some()); - } - - #[tokio::test] - async fn test_system_status_camelcase_alias() { - // Accept both `schema_introspection` (snake) and `schemaIntrospection` - // (camel) per the #[serde(rename_all = "camelCase")] + alias attr. - let (storage, _dir) = test_storage().await; - let result = execute_system_status( - &storage, - &test_cognitive(), - Some(serde_json::json!({ "schemaIntrospection": true })), - ) - .await; - assert!(result.is_ok(), "{:?}", result); - let value = result.unwrap(); - assert!( - value.get("schema").is_some(), - "camelCase form must also trigger schema block" - ); - } - - #[test] - fn test_storage_schema_introspection_method() { - // Direct test on the Storage method, independent of the MCP layer. - let dir = TempDir::new().unwrap(); - let storage = Storage::new(Some(dir.path().join("test.db"))).unwrap(); - let intro = storage - .schema_introspection() - .expect("schema_introspection must succeed on a fresh DB"); - - // Schema version pulled from the schema_version table. - assert!( - intro.schema_version >= 13, - "fresh DB should be at schema_version >= 13, got {}", - intro.schema_version - ); - // At least one walked table should exist. - assert!( - !intro.tables.is_empty(), - "expected at least one user-data table" - ); - // Empty DB → no embeddings → embedding_null_count == 0 (no rows to - // count). Once we ingest, it should be > 0 (no embeddings generated - // in tests by default). - assert_eq!(intro.embedding_null_count, 0); - } - - #[tokio::test] - async fn test_portable_export_writes_archive_to_storage_exports_dir() { - let (storage, _dir) = test_storage().await; - storage - .ingest(vestige_core::IngestInput { - content: "Portable MCP export test memory".to_string(), - node_type: "fact".to_string(), - source: None, - sentiment_score: 0.0, - sentiment_magnitude: 0.0, - tags: vec!["portable".to_string()], - valid_from: None, - valid_until: None, - source_envelope: None, - }) - .unwrap(); - - let result = execute_export( - &storage, - Some(serde_json::json!({ - "format": "portable", - "path": "portable-test.json" - })), - ) - .await - .unwrap(); - - let path = result["path"].as_str().unwrap(); - assert_eq!(result["format"], "portable"); - assert!(path.ends_with("exports/portable-test.json")); - assert!(std::path::Path::new(path).exists()); - assert_eq!( - result["archiveFormat"], - vestige_core::PORTABLE_ARCHIVE_FORMAT - ); - assert!(result["rowsExported"].as_u64().unwrap() > 0); - } } diff --git a/crates/vestige-mcp/src/tools/memory_status.rs b/crates/vestige-mcp/src/tools/memory_status.rs deleted file mode 100644 index 61a6c58..0000000 --- a/crates/vestige-mcp/src/tools/memory_status.rs +++ /dev/null @@ -1,141 +0,0 @@ -//! Unified `memory_status` Tool (v2.2 — Tool Consolidation) -//! -//! Folds four read-only status/health/temporal tools into one -//! view-dispatched surface: -//! -//! view = health (default) | retention | timeline | changelog -//! -//! - `health` → full system health + statistics (the former `system_status`). -//! Returns the byte-for-byte `system_status` shape (audit scripts parse it), -//! including `schema_introspection` passthrough. -//! - `retention` → the lightweight retention dashboard (former `memory_health`). -//! - `timeline` → chronological browse (former `memory_timeline`). -//! - `changelog` → audit trail of memory changes (former `memory_changelog`). -//! -//! This is a thin facade: each view forwards the *same* args envelope to the -//! existing handler. None of the underlying arg structs use -//! `deny_unknown_fields`, so the discriminator `view` is simply ignored by each -//! handler — no lossy re-scoping required, and per-view fields validate as -//! before. The `cognitive` lock is never held across a forwarded call. - -use serde_json::Value; -use std::sync::Arc; -use tokio::sync::Mutex; - -use vestige_core::{OutputConfig, Storage}; - -use crate::cognitive::CognitiveEngine; - -/// Discriminated-union schema for the unified `memory_status` tool. -pub fn schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "view": { - "type": "string", - "enum": ["health", "retention", "timeline", "changelog"], - "default": "health", - "description": "Which status view. 'health' (default): full system health + stats + FSRS preview + warnings + recommendations. 'retention': lightweight retention dashboard (avg/distribution/trend). 'timeline': browse memories chronologically. 'changelog': audit trail of memory state changes." - }, - // --- [health view] --- - "schema_introspection": { - "type": "boolean", - "description": "[health view] Include the response-schema description in the output." - }, - // --- [timeline view] --- - "start": { "type": "string", "description": "[timeline/changelog view] Start of range (ISO 8601 date or datetime)." }, - "end": { "type": "string", "description": "[timeline/changelog view] End of range (ISO 8601 date or datetime)." }, - "node_type": { "type": "string", "description": "[timeline view] Filter by node type (e.g. 'fact', 'decision')." }, - "tags": { "type": "array", "items": { "type": "string" }, "description": "[timeline view] Filter by tags (ANY match)." }, - "detail_level": { - "type": "string", "enum": ["brief", "summary", "full"], - "description": "[timeline view] Level of detail (default 'summary')." - }, - // --- [changelog view] --- - "memory_id": { "type": "string", "description": "[changelog view] Per-memory mode: state transitions for this memory id." }, - // --- shared: limit (per-view ranges differ; clamped internally) --- - "limit": { - "type": "integer", - "description": "Max results. [timeline] default 50, max 200. [changelog] default 20, clamped to 100. Ignored by health/retention.", - "minimum": 1, "maximum": 200 - } - } - }) -} - -/// Unified dispatcher for `memory_status`. Routes on `view` (default `health`). -pub async fn execute( - storage: &Arc, - cognitive: &Arc>, - output_config: &OutputConfig, - args: Option, -) -> Result { - let view = args - .as_ref() - .and_then(|a| a.get("view")) - .and_then(|v| v.as_str()) - .unwrap_or("health") - .to_string(); - - match view.as_str() { - // Byte-for-byte system_status shape (incl. schema_introspection passthrough). - "health" => super::maintenance::execute_system_status(storage, cognitive, args).await, - "retention" => super::health::execute(storage, args).await, - "timeline" => super::timeline::execute(storage, output_config, args).await, - "changelog" => super::changelog::execute(storage, args).await, - other => Err(format!( - "Unknown memory_status view '{other}'. Use health|retention|timeline|changelog." - )), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::cognitive::CognitiveEngine; - - fn test_storage() -> Arc { - let dir = tempfile::TempDir::new().unwrap(); - let storage = Storage::new(Some(dir.path().join("test.db"))).unwrap(); - // Keep the tempdir alive for the duration of the process by leaking it; - // these are short-lived unit tests. - std::mem::forget(dir); - Arc::new(storage) - } - - #[test] - fn test_schema_views() { - let s = schema(); - let views = s["properties"]["view"]["enum"].as_array().unwrap(); - assert_eq!(views.len(), 4); - assert_eq!(s["properties"]["view"]["default"], "health"); - } - - #[tokio::test] - async fn test_default_view_is_health() { - let storage = test_storage(); - let cognitive = Arc::new(Mutex::new(CognitiveEngine::new())); - let oc = OutputConfig::default(); - // No args → health view → must match system_status output exactly. - let unified = execute(&storage, &cognitive, &oc, None).await.unwrap(); - let direct = super::super::maintenance::execute_system_status(&storage, &cognitive, None) - .await - .unwrap(); - assert_eq!( - unified, direct, - "memory_status view=health must equal system_status byte-for-byte" - ); - } - - #[tokio::test] - async fn test_all_views_resolve() { - let storage = test_storage(); - let cognitive = Arc::new(Mutex::new(CognitiveEngine::new())); - let oc = OutputConfig::default(); - for view in ["health", "retention", "timeline", "changelog"] { - let args = Some(serde_json::json!({ "view": view })); - let r = execute(&storage, &cognitive, &oc, args).await; - assert!(r.is_ok(), "view={view} should resolve, got {r:?}"); - } - } -} diff --git a/crates/vestige-mcp/src/tools/memory_unified.rs b/crates/vestige-mcp/src/tools/memory_unified.rs index 1b32262..2d73b6b 100644 --- a/crates/vestige-mcp/src/tools/memory_unified.rs +++ b/crates/vestige-mcp/src/tools/memory_unified.rs @@ -43,8 +43,8 @@ pub fn schema() -> Value { "properties": { "action": { "type": "string", - "enum": ["get", "get_batch", "delete", "purge", "state", "promote", "demote", "edit"], - "description": "Action to perform: 'get' retrieves full memory node, 'get_batch' retrieves multiple memories by IDs (use 'ids' array), 'purge' permanently removes memory content and embeddings after confirm=true, 'delete' is a backwards-compatible alias for purge and also requires confirm=true, 'state' returns accessibility state, 'promote' increases retrieval strength (thumbs up), 'demote' decreases retrieval strength (thumbs down), 'edit' updates content in-place (preserves FSRS state)" + "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", @@ -57,12 +57,7 @@ pub fn schema() -> Value { }, "reason": { "type": "string", - "description": "Why this memory is being promoted/demoted/purged (optional, for logging)." - }, - "confirm": { - "type": "boolean", - "description": "Required for action='purge' and action='delete'. Purge/delete permanently removes memory content and embeddings; only a non-content tombstone remains.", - "default": false + "description": "Why this memory is being promoted/demoted (optional, for logging). Only used with promote/demote actions." }, "content": { "type": "string", @@ -80,7 +75,6 @@ struct MemoryArgs { id: Option, ids: Option>, reason: Option, - confirm: Option, content: Option, } @@ -116,32 +110,13 @@ pub async fn execute( match args.action.as_str() { "get" => execute_get(storage, &id).await, - "delete" => { - execute_purge( - storage, - &id, - args.reason, - args.confirm.unwrap_or(false), - "delete", - ) - .await - } - "purge" => { - execute_purge( - storage, - &id, - args.reason, - args.confirm.unwrap_or(false), - "purge", - ) - .await - } + "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, get_batch, delete, purge, state, promote, demote, edit", + "Invalid action '{}'. Must be one of: get, get_batch, delete, state, promote, demote, edit", args.action )), } @@ -230,39 +205,15 @@ async fn execute_get_batch(storage: &Arc, ids: &[String]) -> Result, - id: &str, - reason: Option, - confirm: bool, - action: &str, -) -> Result { - if !confirm { - return Err( - "Purge is irreversible. Pass confirm=true to permanently remove memory content and embeddings." - .to_string(), - ); - } - - let report = storage - .purge_node(id, reason.as_deref()) - .map_err(|e| e.to_string())?; +/// 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())?; Ok(serde_json::json!({ - "action": action, - "success": report.deleted, + "action": "delete", + "success": deleted, "nodeId": id, - "message": if report.deleted { - "Memory purged permanently; content and embeddings removed. Non-content tombstone retained for sync/audit." - } else { - "Memory not found" - }, - "deletedAt": report.deleted_at.to_rfc3339(), - "edgesPruned": report.edges_pruned, - "insightsRewritten": report.insights_rewritten, - "insightsDeleted": report.insights_deleted, - "childrenOrphaned": report.children_orphaned, + "message": if deleted { "Memory deleted successfully" } else { "Memory not found" }, })) } @@ -518,15 +469,13 @@ mod tests { assert!(schema["properties"]["reason"].is_object()); assert_eq!(schema["required"], serde_json::json!(["action"])); assert!(schema["properties"]["ids"].is_object()); // get_batch support - // Verify all 8 actions are in enum + // Verify all 7 actions are in enum let actions = schema["properties"]["action"]["enum"].as_array().unwrap(); - assert_eq!(actions.len(), 8); + assert_eq!(actions.len(), 7); assert!(actions.contains(&serde_json::json!("get_batch"))); - assert!(actions.contains(&serde_json::json!("purge"))); assert!(actions.contains(&serde_json::json!("edit"))); assert!(actions.contains(&serde_json::json!("promote"))); assert!(actions.contains(&serde_json::json!("demote"))); - assert!(schema["properties"]["confirm"].is_object()); } // === INTEGRATION TESTS === @@ -552,7 +501,6 @@ mod tests { tags: vec!["test-tag".to_string()], valid_from: None, valid_until: None, - source_envelope: None, }) .unwrap(); node.id @@ -614,21 +562,10 @@ mod tests { } #[tokio::test] - async fn test_delete_requires_confirm() { + async fn test_delete_existing_memory() { let (storage, _dir) = test_storage().await; let id = ingest_memory(&storage).await; - let args = serde_json::json!({ "action": "delete", "id": id.clone() }); - let result = execute(&storage, &test_cognitive(), Some(args)).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("confirm=true")); - assert!(storage.get_node(&id).unwrap().is_some()); - } - - #[tokio::test] - async fn test_delete_existing_memory_with_confirm() { - let (storage, _dir) = test_storage().await; - let id = ingest_memory(&storage).await; - let args = serde_json::json!({ "action": "delete", "id": id, "confirm": true }); + let args = serde_json::json!({ "action": "delete", "id": id }); let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_ok()); let value = result.unwrap(); @@ -649,11 +586,8 @@ mod tests { .unwrap() .id; let _ = storage.delete_node(&warmup_id); - let args = serde_json::json!({ - "action": "delete", - "id": "00000000-0000-0000-0000-000000000000", - "confirm": true - }); + 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(); @@ -665,7 +599,7 @@ mod tests { async fn test_delete_then_get_returns_not_found() { let (storage, _dir) = test_storage().await; let id = ingest_memory(&storage).await; - let del_args = serde_json::json!({ "action": "delete", "id": id, "confirm": true }); + let del_args = serde_json::json!({ "action": "delete", "id": id }); execute(&storage, &test_cognitive(), Some(del_args)) .await .unwrap(); @@ -675,42 +609,6 @@ mod tests { assert_eq!(value["found"], false); } - #[tokio::test] - async fn test_purge_requires_confirm() { - let (storage, _dir) = test_storage().await; - let id = ingest_memory(&storage).await; - let args = serde_json::json!({ "action": "purge", "id": id.clone() }); - let result = execute(&storage, &test_cognitive(), Some(args)).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("confirm=true")); - assert!(storage.get_node(&id).unwrap().is_some()); - } - - #[tokio::test] - async fn test_purge_existing_memory() { - let (storage, _dir) = test_storage().await; - let id = ingest_memory(&storage).await; - let args = serde_json::json!({ - "action": "purge", - "id": id, - "confirm": true, - "reason": "test cleanup" - }); - let result = execute(&storage, &test_cognitive(), Some(args)).await; - assert!(result.is_ok()); - let value = result.unwrap(); - assert_eq!(value["action"], "purge"); - assert_eq!(value["success"], true); - assert!( - value["message"] - .as_str() - .unwrap() - .contains("purged permanently") - ); - assert_eq!(value["edgesPruned"], 0); - assert!(storage.get_node(&id).unwrap().is_none()); - } - #[tokio::test] async fn test_state_existing_memory() { let (storage, _dir) = test_storage().await; diff --git a/crates/vestige-mcp/src/tools/merge.rs b/crates/vestige-mcp/src/tools/merge.rs deleted file mode 100644 index 70a2683..0000000 --- a/crates/vestige-mcp/src/tools/merge.rs +++ /dev/null @@ -1,541 +0,0 @@ -//! Merge / Supersede control tools (Phase 3 — v2.1.25) -//! -//! Diff-previewed, confidence-gated, reversible, self-explaining -//! combine/dedupe/supersede on a never-delete (bitemporal) store. The default -//! is always preview/review — these tools never silently mutate memory. -//! -//! Tool surface (each registered as its own MCP tool name, all routed here): -//! -//! - `merge_candidates` — surface likely duplicate clusters with confidence + -//! the signals behind each (Fellegi-Sunter match / possible / non-match). -//! - `plan_merge` — previewable merge PLAN (a diff) without applying it. -//! - `plan_supersede` — preview superseding A with B (bitemporal invalidation, -//! audit-preserving) without applying. -//! - `apply_plan` — execute a previously-generated plan id; recorded as a -//! reversible operation. -//! - `merge_undo` — reverse a prior merge/supersede operation (the reflog). -//! - `protect` — pin a memory so it can never be auto-merged/superseded/forgotten. -//! - `merge_policy` — get/set the two confidence thresholds + auto_apply. -//! -//! The actual logic lives in `vestige_core` (`storage::Storage` + -//! `advanced::merge_supersede`); this layer only validates arguments and shapes -//! JSON. - -use serde_json::{Value, json}; -use std::sync::Arc; -use vestige_core::Storage; - -// ============================================================================ -// SCHEMAS -// ============================================================================ - -/// `merge_candidates` input schema. -pub fn merge_candidates_schema() -> Value { - json!({ - "type": "object", - "properties": { - "limit": { - "type": "integer", - "description": "Max candidate clusters to return (default 20).", - "default": 20, "minimum": 1, "maximum": 100 - }, - "tags": { - "type": "array", - "items": { "type": "string" }, - "description": "Optional: only consider memories with these tags (ANY match)." - } - } - }) -} - -/// `plan_merge` input schema. -pub fn plan_merge_schema() -> Value { - json!({ - "type": "object", - "properties": { - "member_ids": { - "type": "array", - "items": { "type": "string" }, - "description": "IDs of the memories to merge (>= 2). The survivor is kept; the rest are bitemporally invalidated (kept for audit)." - }, - "survivor_id": { - "type": "string", - "description": "Optional: which member to keep. Defaults to the highest-retention member." - } - }, - "required": ["member_ids"] - }) -} - -/// `plan_supersede` input schema. -pub fn plan_supersede_schema() -> Value { - json!({ - "type": "object", - "properties": { - "old_id": { "type": "string", "description": "Memory being superseded (kept, marked invalid)." }, - "new_id": { "type": "string", "description": "Memory that supersedes the old one." } - }, - "required": ["old_id", "new_id"] - }) -} - -/// `apply_plan` input schema. -pub fn apply_plan_schema() -> Value { - json!({ - "type": "object", - "properties": { - "plan_id": { "type": "string", "description": "ID of a plan produced by plan_merge / plan_supersede." }, - "confirm": { - "type": "boolean", - "description": "Required true for 'possible'/'non_match' plans. 'match' plans apply only if the policy has auto_apply=true, else confirm is required too.", - "default": false - } - }, - "required": ["plan_id"] - }) -} - -/// `merge_undo` input schema. -pub fn merge_undo_schema() -> Value { - json!({ - "type": "object", - "properties": { - "operation_id": { - "type": "string", - "description": "ID of the merge/supersede operation to reverse. Omit to list recent operations (the reflog)." - } - } - }) -} - -/// `protect` input schema. -pub fn protect_schema() -> Value { - json!({ - "type": "object", - "properties": { - "id": { "type": "string", "description": "Memory id to protect/unprotect." }, - "protected": { - "type": "boolean", - "description": "true to pin (block auto-merge/supersede/forget), false to unpin. Default true.", - "default": true - } - }, - "required": ["id"] - }) -} - -/// `merge_policy` input schema. -pub fn merge_policy_schema() -> Value { - json!({ - "type": "object", - "properties": { - "match_threshold": { - "type": "number", - "description": "Score >= this => 'match' (auto-merge eligible). 0-1.", - "minimum": 0.0, "maximum": 1.0 - }, - "possible_threshold": { - "type": "number", - "description": "Score in [possible, match) => 'possible' (review). Below => not offered. 0-1.", - "minimum": 0.0, "maximum": 1.0 - }, - "auto_apply": { - "type": "boolean", - "description": "Allow 'match'-class plans to apply without confirm. Default false (review-first)." - } - } - }) -} - -// ============================================================================ -// DISPATCH -// ============================================================================ - -/// Route a merge/supersede tool call by tool name. -pub async fn execute( - storage: &Arc, - tool: &str, - args: Option, -) -> Result { - match tool { - "merge_candidates" => merge_candidates(storage, args), - "plan_merge" => plan_merge(storage, args), - "plan_supersede" => plan_supersede(storage, args), - "apply_plan" => apply_plan(storage, args), - "merge_undo" => merge_undo(storage, args), - "protect" => protect(storage, args), - "merge_policy" => merge_policy(storage, args), - other => Err(format!("unknown merge tool: {other}")), - } -} - -fn obj(args: &Option) -> serde_json::Map { - args.as_ref() - .and_then(|v| v.as_object().cloned()) - .unwrap_or_default() -} - -// ============================================================================ -// merge_candidates -// ============================================================================ - -fn merge_candidates(storage: &Arc, args: Option) -> Result { - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - { - let a = obj(&args); - let limit = a.get("limit").and_then(|v| v.as_u64()).unwrap_or(20) as usize; - let tags: Vec = a - .get("tags") - .and_then(|v| v.as_array()) - .map(|arr| { - arr.iter() - .filter_map(|t| t.as_str().map(|s| s.to_string())) - .collect() - }) - .unwrap_or_default(); - - let policy = storage.get_merge_policy().map_err(|e| e.to_string())?; - let candidates = storage - .merge_candidates(policy, limit, &tags) - .map_err(|e| e.to_string())?; - - let out: Vec = candidates - .iter() - .map(|c| { - json!({ - "memberIds": c.member_ids, - "previews": c.previews, - "survivorId": c.survivor_id, - "confidence": format!("{:.3}", c.confidence), - "classification": c.classification.as_str(), - "hasProtectedMember": c.has_protected_member, - "signals": { - "embeddingSimilarity": format!("{:.3}", c.signals.embedding_similarity), - "tagOverlap": format!("{:.3}", c.signals.tag_overlap), - "tokenOverlap": format!("{:.3}", c.signals.token_overlap), - "combinedScore": format!("{:.3}", c.signals.combined_score) - }, - "nextStep": if c.has_protected_member { - "A member is protected — unprotect it or pick it as survivor before plan_merge." - } else { - "Call plan_merge with these memberIds to preview the combined result." - } - }) - }) - .collect(); - - let policy = storage.get_merge_policy().map_err(|e| e.to_string())?; - Ok(json!({ - "candidates": out, - "totalCandidates": out.len(), - "policy": { - "matchThreshold": policy.match_threshold, - "possibleThreshold": policy.possible_threshold, - "autoApply": policy.auto_apply - }, - "note": "Nothing was changed. These are review candidates only." - })) - } - #[cfg(not(all(feature = "embeddings", feature = "vector-search")))] - { - let _ = (storage, args); - Ok(json!({ "error": "Embeddings feature not enabled.", "candidates": [] })) - } -} - -// ============================================================================ -// plan_merge -// ============================================================================ - -fn plan_merge(storage: &Arc, args: Option) -> Result { - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - { - let a = obj(&args); - let member_ids: Vec = a - .get("member_ids") - .and_then(|v| v.as_array()) - .map(|arr| { - arr.iter() - .filter_map(|t| t.as_str().map(|s| s.to_string())) - .collect() - }) - .unwrap_or_default(); - if member_ids.len() < 2 { - return Err("member_ids must contain at least 2 ids".into()); - } - let survivor = a.get("survivor_id").and_then(|v| v.as_str()); - let policy = storage.get_merge_policy().map_err(|e| e.to_string())?; - let plan = storage - .plan_merge(&member_ids, survivor, policy) - .map_err(|e| e.to_string())?; - Ok(plan_to_json(&plan, &policy)) - } - #[cfg(not(all(feature = "embeddings", feature = "vector-search")))] - { - let _ = (storage, args); - Err("Embeddings feature not enabled.".into()) - } -} - -// ============================================================================ -// plan_supersede -// ============================================================================ - -fn plan_supersede(storage: &Arc, args: Option) -> Result { - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - { - let a = obj(&args); - let old_id = a - .get("old_id") - .and_then(|v| v.as_str()) - .ok_or("old_id is required")?; - let new_id = a - .get("new_id") - .and_then(|v| v.as_str()) - .ok_or("new_id is required")?; - let policy = storage.get_merge_policy().map_err(|e| e.to_string())?; - let plan = storage - .plan_supersede(old_id, new_id, policy) - .map_err(|e| e.to_string())?; - Ok(plan_to_json(&plan, &policy)) - } - #[cfg(not(all(feature = "embeddings", feature = "vector-search")))] - { - let _ = (storage, args); - Err("Embeddings feature not enabled.".into()) - } -} - -#[cfg(all(feature = "embeddings", feature = "vector-search"))] -fn plan_to_json(plan: &vestige_core::MergePlan, policy: &vestige_core::MergePolicy) -> Value { - let requires_confirm = - plan.classification != vestige_core::MatchClass::Match || !policy.auto_apply; - json!({ - "planId": plan.id, - "kind": plan.kind.as_str(), - "survivorId": plan.survivor_id, - "memberIds": plan.member_ids, - "diff": { - "resultContent": plan.result_content, - "resultTags": plan.result_tags, - "resultSource": plan.result_source, - "invalidatedIds": plan.invalidated_ids - }, - "confidence": format!("{:.3}", plan.confidence), - "classification": plan.classification.as_str(), - "signals": { - "embeddingSimilarity": format!("{:.3}", plan.signals.embedding_similarity), - "tagOverlap": format!("{:.3}", plan.signals.tag_overlap), - "tokenOverlap": format!("{:.3}", plan.signals.token_overlap), - "combinedScore": format!("{:.3}", plan.signals.combined_score) - }, - "explanation": plan.explanation, - "requiresConfirm": requires_confirm, - "nextStep": format!( - "Review the diff. To execute: apply_plan with plan_id='{}'{}.", - plan.id, - if requires_confirm { " and confirm=true" } else { "" } - ), - "note": "Nothing was changed. This is a preview plan — apply_plan applies it; merge_undo reverses it." - }) -} - -// ============================================================================ -// apply_plan -// ============================================================================ - -fn apply_plan(storage: &Arc, args: Option) -> Result { - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - { - let a = obj(&args); - let plan_id = a - .get("plan_id") - .and_then(|v| v.as_str()) - .ok_or("plan_id is required")?; - let confirm = a.get("confirm").and_then(|v| v.as_bool()).unwrap_or(false); - let op = storage - .apply_plan(plan_id, confirm) - .map_err(|e| e.to_string())?; - Ok(json!({ - "operationId": op.id, - "opType": op.op_type, - "status": op.status, - "survivorId": op.survivor_id, - "affectedIds": op.affected_ids, - "reason": op.reason, - "appliedAt": op.created_at, - "reversible": true, - "nextStep": format!("To reverse this, call merge_undo with operation_id='{}'.", op.id), - "note": "Old memories were bitemporally invalidated (valid_until stamped), NOT deleted. They remain queryable for audit." - })) - } - #[cfg(not(all(feature = "embeddings", feature = "vector-search")))] - { - let _ = (storage, args); - Err("Embeddings feature not enabled.".into()) - } -} - -// ============================================================================ -// merge_undo (also lists the reflog when no id given) -// ============================================================================ - -fn merge_undo(storage: &Arc, args: Option) -> Result { - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - { - let a = obj(&args); - match a.get("operation_id").and_then(|v| v.as_str()) { - Some(op_id) => { - let op = storage.merge_undo(op_id).map_err(|e| e.to_string())?; - Ok(json!({ - "undoOperationId": op.id, - "revertedOperationId": op.reverts_op_id, - "status": "reverted", - "affectedIds": op.affected_ids, - "reason": op.reason, - "note": "The original operation was reversed: survivor content/tags restored and invalidation cleared. The plan is re-openable." - })) - } - None => { - // No id => return the reflog so the caller can pick one. - let ops = storage - .list_merge_operations(20) - .map_err(|e| e.to_string())?; - let log: Vec = ops - .iter() - .map(|op| { - json!({ - "operationId": op.id, - "opType": op.op_type, - "status": op.status, - "survivorId": op.survivor_id, - "affectedIds": op.affected_ids, - "confidence": op.confidence.map(|c| format!("{:.3}", c)), - "reason": op.reason, - "createdAt": op.created_at, - "revertedAt": op.reverted_at - }) - }) - .collect(); - Ok(json!({ - "operations": log, - "totalOperations": log.len(), - "note": "This is the reversible operation log (the memory reflog). Pass operation_id to reverse one." - })) - } - } - } - #[cfg(not(all(feature = "embeddings", feature = "vector-search")))] - { - let _ = (storage, args); - Err("Embeddings feature not enabled.".into()) - } -} - -// ============================================================================ -// protect -// ============================================================================ - -fn protect(storage: &Arc, args: Option) -> Result { - let a = obj(&args); - let id = a - .get("id") - .and_then(|v| v.as_str()) - .ok_or("id is required")?; - let protected = a.get("protected").and_then(|v| v.as_bool()).unwrap_or(true); - storage - .set_protected(id, protected) - .map_err(|e| e.to_string())?; - Ok(json!({ - "id": id, - "protected": protected, - "note": if protected { - "Memory pinned. It can never be auto-merged, superseded, or garbage-collected until unprotected." - } else { - "Memory unprotected. It is now eligible for merge/supersede/forget again." - } - })) -} - -// ============================================================================ -// merge_policy (get when no args, set otherwise) -// ============================================================================ - -fn merge_policy(storage: &Arc, args: Option) -> Result { - let a = obj(&args); - let current = storage.get_merge_policy().map_err(|e| e.to_string())?; - - let has_update = a.contains_key("match_threshold") - || a.contains_key("possible_threshold") - || a.contains_key("auto_apply"); - - if has_update { - let match_t = a - .get("match_threshold") - .and_then(|v| v.as_f64()) - .map(|v| v as f32) - .unwrap_or(current.match_threshold); - let possible_t = a - .get("possible_threshold") - .and_then(|v| v.as_f64()) - .map(|v| v as f32) - .unwrap_or(current.possible_threshold); - let auto = a - .get("auto_apply") - .and_then(|v| v.as_bool()) - .unwrap_or(current.auto_apply); - let policy = vestige_core::MergePolicy::new(match_t, possible_t, auto); - storage - .set_merge_policy(policy) - .map_err(|e| e.to_string())?; - Ok(json!({ - "updated": true, - "matchThreshold": policy.match_threshold, - "possibleThreshold": policy.possible_threshold, - "autoApply": policy.auto_apply, - "note": "Policy saved. Fellegi-Sunter: score>=match => auto-merge eligible; [possible,match) => review; below => not offered." - })) - } else { - Ok(json!({ - "matchThreshold": current.match_threshold, - "possibleThreshold": current.possible_threshold, - "autoApply": current.auto_apply, - "note": "Two-threshold merge policy. Pass match_threshold / possible_threshold / auto_apply to change it." - })) - } -} - -// ============================================================================ -// TESTS — see tests/merge_supersede_test.rs for full integration coverage. -// ============================================================================ - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn schemas_are_objects() { - for s in [ - merge_candidates_schema(), - plan_merge_schema(), - plan_supersede_schema(), - apply_plan_schema(), - merge_undo_schema(), - protect_schema(), - merge_policy_schema(), - ] { - assert_eq!(s["type"], "object"); - } - } - - #[test] - fn plan_merge_requires_two_ids() { - assert!( - plan_merge_schema()["required"] - .as_array() - .unwrap() - .iter() - .any(|v| v == "member_ids") - ); - } -} diff --git a/crates/vestige-mcp/src/tools/mod.rs b/crates/vestige-mcp/src/tools/mod.rs index 1efa955..b1331cf 100644 --- a/crates/vestige-mcp/src/tools/mod.rs +++ b/crates/vestige-mcp/src/tools/mod.rs @@ -2,28 +2,16 @@ //! //! Tool implementations for the Vestige MCP server. //! -//! v2.2 Tool Consolidation (Layer 1): the advertised surface is 12 tools — -//! recall, memory, codebase, intention, smart_ingest, source_sync, -//! memory_status, dedup, graph, maintain, session_start, suppress. The unified -//! facade modules (recall, dedup, memory_status, graph_unified, maintain, plus -//! the earlier *_unified) dispatch on an action/mode/view discriminator and -//! delegate to the granular handler modules below, which stay in the crate as -//! the implementation layer and as hidden back-compat aliases (see the redirect -//! arms in server.rs). See docs/launch/tool-consolidation-v2.2.0.md. +//! The unified tools (codebase_unified, intention_unified, memory_unified, search_unified) +//! are the primary API. The granular tools below are kept for backwards compatibility +//! but are not exposed in the MCP tool list. // Active unified tools pub mod codebase_unified; pub mod intention_unified; pub mod memory_unified; pub mod search_unified; - -// v2.2: Unified retrieval surface — folds search + deep_reference + -// cross_reference + contradictions into one mode-dispatched tool. -// mode=lookup (default) is a zero-overhead pass-through to search_unified. -pub mod recall; pub mod smart_ingest; -// #57: external-source connectors (GitHub Issues / Redmine retrieval layer) -pub mod source_sync; // v1.2: Temporal query tools pub mod changelog; @@ -32,21 +20,10 @@ pub mod timeline; // v1.2: Maintenance tools pub mod maintenance; -// v2.2: Unified maintenance surface — folds consolidate + dream + gc + -// importance_score + backup + export + restore into one action-dispatched tool. -pub mod maintain; - -// v2.2: Unified status surface — folds system_status + memory_health + -// memory_timeline + memory_changelog into one view-dispatched tool. -pub mod memory_status; - // v1.3: Auto-save and dedup tools pub mod dedup; pub mod importance; -// v2.1.25: Merge / Supersede controls (Phase 3) -pub mod merge; - // v1.5: Cognitive tools pub mod dream; pub mod explore; @@ -60,21 +37,12 @@ pub mod session_context; pub mod graph; pub mod health; -// v2.2: Unified graph surface — folds explore_connections + predict + -// memory_graph + composed_graph into one action-dispatched tool. -pub mod graph_unified; - // v2.1: Cross-reference (connect the dots) -pub mod composed_graph; -pub mod contradictions; pub mod cross_reference; // v2.0.5: Active Forgetting — Anderson 2025 + Davis Rac1 pub mod suppress; -// Retroactive Salience Backfill — Cai 2024 Nature (memory with hindsight) -pub mod backfill; - // 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)] diff --git a/crates/vestige-mcp/src/tools/recall.rs b/crates/vestige-mcp/src/tools/recall.rs deleted file mode 100644 index c899fd6..0000000 --- a/crates/vestige-mcp/src/tools/recall.rs +++ /dev/null @@ -1,163 +0,0 @@ -//! Unified `recall` Tool (v2.2 — Tool Consolidation, HOT PATH) -//! -//! Folds the four retrieval/reasoning tools into one mode-dispatched surface: -//! -//! mode = lookup (DEFAULT) | reason | contradictions -//! -//! - `lookup` (default) → hybrid search (the former `search`). This is the hot -//! path: with no `mode` set, `recall` is a ZERO-overhead pass-through to -//! `search_unified::execute` — it must never pay the cost of the reasoning -//! path. (`deep_reference`/`reason` runs spreading activation + FSRS trust -//! scoring + contradiction analysis and is 5–10× slower.) -//! - `reason` → deep cognitive reasoning across memories (former -//! `deep_reference` / `cross_reference`). -//! - `contradictions` → trust-weighted disagreement pairs (former -//! `contradictions`). -//! -//! The schema is derived from `search_unified::schema()` (so every lookup -//! parameter stays available and documented) plus the `mode` discriminator and -//! the reason/contradictions fields. `query` is NOT globally required because -//! the contradictions mode is scoped by `topic`; per-mode requirements are -//! validated at runtime. - -use serde_json::Value; -use std::sync::Arc; -use tokio::sync::Mutex; - -use vestige_core::{OutputConfig, Storage}; - -use crate::cognitive::CognitiveEngine; - -/// Discriminated-union schema for the unified `recall` tool. -/// -/// Built on top of `search_unified::schema()` so all lookup parameters carry -/// through verbatim; the `required: ["query"]` constraint is dropped (validated -/// per-mode at runtime) and the mode/reason/contradictions fields are added. -pub fn schema() -> Value { - let mut schema = super::search_unified::schema(); - - if let Some(obj) = schema.as_object_mut() { - // Drop the global `query` requirement — contradictions uses `topic`. - obj.remove("required"); - - if let Some(props) = obj.get_mut("properties").and_then(|p| p.as_object_mut()) { - props.insert( - "mode".to_string(), - serde_json::json!({ - "type": "string", - "enum": ["lookup", "reason", "contradictions"], - "default": "lookup", - "description": "Retrieval mode. 'lookup' (default): fast hybrid search — use for plain recall. 'reason': deep cognitive reasoning across memories (FSRS-6 trust scoring, spreading activation, supersession, contradiction analysis) — use when accuracy matters; needs 'query'. 'contradictions': surface trust-weighted disagreement pairs for a 'topic' (or recent memories)." - }), - ); - // reason (deep_reference) extra field. - props.insert( - "depth".to_string(), - serde_json::json!({ - "type": "integer", - "description": "[reason mode] How many memories to analyze (default 20, max 50).", - "minimum": 5, "maximum": 50 - }), - ); - // contradictions extra fields. - props.insert( - "topic".to_string(), - serde_json::json!({ - "type": "string", - "description": "[contradictions mode] Topic to scope contradiction detection. If omitted, scans recent memories." - }), - ); - props.insert( - "since".to_string(), - serde_json::json!({ - "type": "string", - "description": "[contradictions mode] RFC3339 timestamp; only memories updated after this are considered." - }), - ); - props.insert( - "min_trust".to_string(), - serde_json::json!({ - "type": "number", - "minimum": 0.0, "maximum": 1.0, - "description": "[contradictions mode] Minimum trust for both sides of a contradiction (default 0.3)." - }), - ); - } - } - - schema -} - -/// Unified dispatcher for `recall`. Routes on `mode` (default `lookup`). -/// -/// HOT-PATH INVARIANT: `mode` absent ⇒ `lookup` ⇒ direct pass-through to -/// `search_unified::execute`, no extra work. -pub async fn execute( - storage: &Arc, - cognitive: &Arc>, - output_config: &OutputConfig, - args: Option, -) -> Result { - let mode = args - .as_ref() - .and_then(|a| a.get("mode")) - .and_then(|v| v.as_str()) - .unwrap_or("lookup"); - - match mode { - // Zero-overhead default: straight to hybrid search. - "lookup" => super::search_unified::execute(storage, cognitive, output_config, args).await, - // Deep reasoning (deep_reference / cross_reference share this handler). - "reason" => super::cross_reference::execute(storage, cognitive, args).await, - // Trust-weighted contradiction pairs (storage-only). - "contradictions" => super::contradictions::execute(storage, args).await, - other => Err(format!( - "Unknown recall mode '{other}'. Use lookup|reason|contradictions." - )), - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_schema_has_mode_and_no_required() { - let s = schema(); - let modes = s["properties"]["mode"]["enum"].as_array().unwrap(); - assert_eq!(modes.len(), 3); - assert_eq!(s["properties"]["mode"]["default"], "lookup"); - // query must NOT be globally required (contradictions uses topic). - assert!( - s.get("required").is_none(), - "recall must not globally require 'query'" - ); - // lookup params carried over from search schema. - assert!(s["properties"]["limit"].is_object()); - assert!(s["properties"]["detail_level"].is_object()); - } - - #[tokio::test] - async fn test_lookup_is_default_and_resolves() { - let dir = tempfile::TempDir::new().unwrap(); - let storage = Arc::new(Storage::new(Some(dir.path().join("test.db"))).unwrap()); - let cognitive = Arc::new(Mutex::new(CognitiveEngine::new())); - let oc = OutputConfig::default(); - // No mode → lookup → behaves like search (query required by search). - let args = Some(serde_json::json!({ "query": "anything" })); - let r = execute(&storage, &cognitive, &oc, args).await; - assert!(r.is_ok(), "default lookup should resolve: {r:?}"); - } - - #[tokio::test] - async fn test_contradictions_mode_resolves_without_query() { - let dir = tempfile::TempDir::new().unwrap(); - let storage = Arc::new(Storage::new(Some(dir.path().join("test.db"))).unwrap()); - let cognitive = Arc::new(Mutex::new(CognitiveEngine::new())); - let oc = OutputConfig::default(); - // contradictions uses topic, not query — must resolve with no query. - let args = Some(serde_json::json!({ "mode": "contradictions" })); - let r = execute(&storage, &cognitive, &oc, args).await; - assert!(r.is_ok(), "contradictions mode should resolve: {r:?}"); - } -} diff --git a/crates/vestige-mcp/src/tools/restore.rs b/crates/vestige-mcp/src/tools/restore.rs index f000397..6f1bc04 100644 --- a/crates/vestige-mcp/src/tools/restore.rs +++ b/crates/vestige-mcp/src/tools/restore.rs @@ -6,10 +6,9 @@ use serde::Deserialize; use serde_json::Value; -use std::path::Path; use std::sync::Arc; -use vestige_core::{IngestInput, PortableArchive, PortableImportMode, Storage}; +use vestige_core::{IngestInput, Storage}; /// Input schema for restore tool pub fn schema() -> Value { @@ -19,16 +18,6 @@ pub fn schema() -> Value { "path": { "type": "string", "description": "Path to the backup JSON file to restore from" - }, - "allowAnyPath": { - "type": "boolean", - "description": "Allow restoring from a file outside the active Vestige backups/ or exports/ directories. Only set true for trusted local files.", - "default": false - }, - "merge": { - "type": "boolean", - "description": "For portable archives, merge into the current database instead of requiring an empty target. Applies sync tombstones and keeps newer local memory rows on conflict.", - "default": false } }, "required": ["path"] @@ -36,13 +25,8 @@ pub fn schema() -> Value { } #[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] struct RestoreArgs { path: String, - #[serde(default)] - allow_any_path: bool, - #[serde(default)] - merge: bool, } #[derive(Deserialize)] @@ -72,57 +56,14 @@ pub async fn execute(storage: &Arc, args: Option) -> Result return Err("Missing arguments".to_string()), }; - let path = Path::new(&args.path); + let path = std::path::Path::new(&args.path); if !path.exists() { return Err(format!("Backup file not found: {}", args.path)); } - if !args.allow_any_path { - ensure_restore_path_allowed(storage, path)?; - } - // Read and parse backup - let backup_bytes = std::fs::read(path).map_err(|e| format!("Failed to read backup: {}", e))?; - if backup_bytes.starts_with(b"SQLite format 3\0") { - return Err( - "Restore expected JSON, but this file is a raw SQLite database backup. Use portable export/import for cross-device transfer, or replace the database file manually while Vestige is stopped." - .to_string(), - ); - } - let backup_content = String::from_utf8(backup_bytes) - .map_err(|_| "Restore file is not UTF-8 JSON".to_string())?; - - if let Ok(archive) = serde_json::from_str::(&backup_content) - && archive.archive_format == vestige_core::PORTABLE_ARCHIVE_FORMAT - { - let rows = archive.total_rows(); - let tables = archive.tables.len(); - let mode = if args.merge { - PortableImportMode::Merge - } else { - PortableImportMode::EmptyOnly - }; - let report = storage - .import_portable_archive(&archive, mode) - .map_err(|e| e.to_string())?; - return Ok(serde_json::json!({ - "tool": "restore", - "success": true, - "mode": if args.merge { "portable-merge" } else { "portable" }, - "tables": tables, - "rows": rows, - "tablesImported": report.tables_imported, - "rowsImported": report.rows_imported, - "tablesSkipped": report.tables_skipped, - "rowsInserted": report.rows_inserted, - "rowsUpdated": report.rows_updated, - "rowsDeleted": report.rows_deleted, - "rowsSkipped": report.rows_skipped, - "conflictsKeptLocal": report.conflicts_kept_local, - "ftsRebuilt": report.fts_rebuilt, - "message": format!("Imported {} rows from portable archive.", report.rows_imported), - })); - } + let backup_content = + std::fs::read_to_string(path).map_err(|e| format!("Failed to read backup: {}", e))?; // Try parsing as wrapped format first (MCP response wrapper), // then fall back to direct RecallResult @@ -173,7 +114,6 @@ pub async fn execute(storage: &Arc, args: Option) -> Result, args: Option) -> Result Result<(), String> { - let canonical_path = path - .canonicalize() - .map_err(|e| format!("Failed to resolve restore path: {}", e))?; - - for dir in [ - storage.sidecar_dir("exports"), - storage.sidecar_dir("backups"), - ] { - if !dir.exists() { - continue; - } - let canonical_dir = dir - .canonicalize() - .map_err(|e| format!("Failed to resolve allowed restore directory: {}", e))?; - if canonical_path.starts_with(&canonical_dir) { - return Ok(()); - } - } - - Err(format!( - "MCP restore is restricted to {} and {} by default. Pass allowAnyPath=true only for trusted local files.", - storage.sidecar_dir("exports").display(), - storage.sidecar_dir("backups").display() - )) -} - #[cfg(test)] mod tests { use super::*; @@ -243,7 +156,6 @@ mod tests { let s = schema(); assert_eq!(s["type"], "object"); assert!(s["properties"]["path"].is_object()); - assert!(s["properties"]["allowAnyPath"].is_object()); assert!( s["required"] .as_array() @@ -281,20 +193,10 @@ mod tests { async fn test_malformed_json_fails() { let (storage, dir) = test_storage().await; let path = write_temp_file(&dir, "bad.json", "this is not json {{{"); - let args = serde_json::json!({ "path": path, "allowAnyPath": true }); - let result = execute(&storage, Some(args)).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Unrecognized backup format")); - } - - #[tokio::test] - async fn test_restore_rejects_arbitrary_path_by_default() { - let (storage, dir) = test_storage().await; - let path = write_temp_file(&dir, "outside_exports.json", "[]"); let args = serde_json::json!({ "path": path }); let result = execute(&storage, Some(args)).await; assert!(result.is_err()); - assert!(result.unwrap_err().contains("restricted")); + assert!(result.unwrap_err().contains("Unrecognized backup format")); } #[tokio::test] @@ -305,7 +207,7 @@ mod tests { { "content": "Memory two", "nodeType": "concept" } ]); let path = write_temp_file(&dir, "backup.json", &backup.to_string()); - let args = serde_json::json!({ "path": path, "allowAnyPath": true }); + let args = serde_json::json!({ "path": path }); let result = execute(&storage, Some(args)).await; assert!(result.is_ok()); let value = result.unwrap(); @@ -327,7 +229,7 @@ mod tests { ] }); let path = write_temp_file(&dir, "recall.json", &backup.to_string()); - let args = serde_json::json!({ "path": path, "allowAnyPath": true }); + let args = serde_json::json!({ "path": path }); let result = execute(&storage, Some(args)).await; assert!(result.is_ok()); let value = result.unwrap(); @@ -335,48 +237,12 @@ mod tests { assert_eq!(value["total"], 3); } - #[tokio::test] - async fn test_restore_portable_archive_from_allowed_exports_dir() { - let (source, _source_dir) = test_storage().await; - let (target, _target_dir) = test_storage().await; - - source - .ingest(vestige_core::IngestInput { - content: "Portable MCP restore test memory".to_string(), - node_type: "fact".to_string(), - source: None, - sentiment_score: 0.0, - sentiment_magnitude: 0.0, - tags: vec!["portable".to_string()], - valid_from: None, - valid_until: None, - source_envelope: None, - }) - .unwrap(); - - let export_dir = target.sidecar_dir("exports"); - std::fs::create_dir_all(&export_dir).unwrap(); - let archive_path = export_dir.join("portable-restore.json"); - source - .export_portable_archive_to_path(&archive_path) - .unwrap(); - - let args = serde_json::json!({ "path": archive_path }); - let result = execute(&target, Some(args)).await.unwrap(); - - assert_eq!(result["tool"], "restore"); - assert_eq!(result["success"], true); - assert_eq!(result["mode"], "portable"); - assert!(result["rowsImported"].as_u64().unwrap() > 0); - assert_eq!(target.get_stats().unwrap().total_nodes, 1); - } - #[tokio::test] async fn test_restore_empty_results_array() { let (storage, dir) = test_storage().await; let backup = serde_json::json!({ "results": [] }); let path = write_temp_file(&dir, "empty.json", &backup.to_string()); - let args = serde_json::json!({ "path": path, "allowAnyPath": true }); + let args = serde_json::json!({ "path": path }); let result = execute(&storage, Some(args)).await; assert!(result.is_ok()); let value = result.unwrap(); @@ -389,7 +255,7 @@ mod tests { // Empty [] parses as Vec first, which has no items → "Empty backup file" let (storage, dir) = test_storage().await; let path = write_temp_file(&dir, "empty_arr.json", "[]"); - let args = serde_json::json!({ "path": path, "allowAnyPath": true }); + let args = serde_json::json!({ "path": path }); let result = execute(&storage, Some(args)).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("Empty backup file")); @@ -400,7 +266,7 @@ mod tests { let (storage, dir) = test_storage().await; let backup = serde_json::json!([{ "content": "No type specified" }]); let path = write_temp_file(&dir, "notype.json", &backup.to_string()); - let args = serde_json::json!({ "path": path, "allowAnyPath": true }); + let args = serde_json::json!({ "path": path }); let result = execute(&storage, Some(args)).await; assert!(result.is_ok()); assert_eq!(result.unwrap()["restored"], 1); diff --git a/crates/vestige-mcp/src/tools/review.rs b/crates/vestige-mcp/src/tools/review.rs index bf88482..f5a0025 100644 --- a/crates/vestige-mcp/src/tools/review.rs +++ b/crates/vestige-mcp/src/tools/review.rs @@ -117,7 +117,6 @@ mod tests { tags: vec![], valid_from: None, valid_until: None, - source_envelope: None, }; let node = storage.ingest(input).unwrap(); node.id diff --git a/crates/vestige-mcp/src/tools/search_unified.rs b/crates/vestige-mcp/src/tools/search_unified.rs index 8c87add..bfe419e 100644 --- a/crates/vestige-mcp/src/tools/search_unified.rs +++ b/crates/vestige-mcp/src/tools/search_unified.rs @@ -21,8 +21,8 @@ use tokio::sync::Mutex; use crate::cognitive::CognitiveEngine; use vestige_core::{ - CompetitionCandidate, EncodingContext, MemoryLifecycle, MemorySnapshot, MemoryState, - OutputConfig, Storage, TopicalContext, + CompetitionCandidate, EncodingContext, MemoryLifecycle, MemorySnapshot, MemoryState, Storage, + TopicalContext, }; /// Input schema for unified search tool @@ -87,49 +87,6 @@ pub fn schema() -> Value { "description": "precise: top results only (fast, token-efficient, skips activation/competition). balanced: full 7-stage cognitive pipeline (default). exhaustive: maximum recall with 5x overfetch, deep graph traversal, no competition suppression.", "enum": ["precise", "balanced", "exhaustive"], "default": "balanced" - }, - "concrete": { - "type": "boolean", - "description": "Force literal/concrete search. Skips semantic expansion, FSRS reweighting, spreading activation, and cognitive side effects. Auto-enabled for quoted strings, env vars, UUIDs, paths, and code identifiers.", - "default": false - }, - "tag_prefix": { - "type": "string", - "description": "Optional tag-prefix filter. When set, only results carrying at least one tag whose value starts with this prefix are returned (case-sensitive). Example: tag_prefix=\"meeting:\" matches memories tagged 'meeting:standup', 'meeting:1-on-1', etc. Applied as a post-filter; combine with a larger 'limit' if you expect heavy thinning." - }, - "source_system": { - "type": "string", - "description": "Investigation filter (#57): only memories ingested from this external system, e.g. 'github' or 'redmine'. Post-filter — non-connector memories are excluded. Combine with a larger 'limit' if thinning is heavy." - }, - "source_project": { - "type": "string", - "description": "Investigation filter: only memories from this source project/repo, exact match (GitHub 'owner/repo', Redmine project id)." - }, - "source_id": { - "type": "string", - "description": "Investigation filter: a specific source record id (issue number / ticket id). Pair with source_system to disambiguate across systems." - }, - "source_type": { - "type": "string", - "description": "Investigation filter: source record type, e.g. 'issue', 'comment'." - }, - "source_author": { - "type": "string", - "description": "Investigation filter: the source author/reporter (not assignee)." - }, - "source_updated_after": { - "type": "string", - "description": "Investigation filter: only records whose source was updated at/after this RFC3339 timestamp (inclusive)." - }, - "source_updated_before": { - "type": "string", - "description": "Investigation filter: only records whose source was updated at/before this RFC3339 timestamp (inclusive)." - }, - "source_status": { - "type": "string", - "enum": ["any", "valid", "tombstoned"], - "description": "Investigation filter: 'any' (default), 'valid' (currently-valid records only), or 'tombstoned' (records no longer visible upstream, kept for audit).", - "default": "any" } }, "required": ["query"] @@ -157,26 +114,6 @@ struct SearchArgs { token_budget: Option, #[serde(alias = "retrieval_mode")] retrieval_mode: Option, - concrete: Option, - #[serde(alias = "tag_prefix")] - tag_prefix: Option, - // #57 Phase 4 — source-aware investigation filters (all post-filters). - #[serde(alias = "source_system")] - source_system: Option, - #[serde(alias = "source_project")] - source_project: Option, - #[serde(alias = "source_id")] - source_id: Option, - #[serde(alias = "source_type")] - source_type: Option, - #[serde(alias = "source_author")] - source_author: Option, - #[serde(alias = "source_updated_after")] - source_updated_after: Option, - #[serde(alias = "source_updated_before")] - source_updated_before: Option, - #[serde(alias = "source_status")] - source_status: Option, } /// Execute unified search with 7-stage cognitive pipeline. @@ -194,7 +131,6 @@ struct SearchArgs { pub async fn execute( storage: &Arc, cognitive: &Arc>, - output_config: &OutputConfig, args: Option, ) -> Result { let args: SearchArgs = match args { @@ -206,15 +142,12 @@ pub async fn execute( return Err("Query cannot be empty".to_string()); } - // Validate detail_level. Precedence: explicit MCP param > config file > - // built-in default. The explicit arg is validated; the config fallback is - // already validated at load time. - let detail_level_owned = output_config.resolve_detail_level(args.detail_level.as_deref()); - let detail_level = match detail_level_owned.as_str() { - "brief" => "brief", - "full" => "full", - "summary" => "summary", - invalid => { + // Validate detail_level + let detail_level = match args.detail_level.as_deref() { + Some("brief") => "brief", + Some("full") => "full", + Some("summary") | None => "summary", + Some(invalid) => { return Err(format!( "Invalid detail_level '{}'. Must be 'brief', 'summary', or 'full'.", invalid @@ -222,9 +155,8 @@ pub async fn execute( } }; - // Clamp all parameters to valid ranges. The default limit honors the - // config file (e.g. a `research` profile) when no explicit param is set. - let limit = output_config.resolve_limit(args.limit, 10).clamp(1, 100); + // Clamp all parameters to valid ranges + let limit = args.limit.unwrap_or(10).clamp(1, 100); 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); @@ -241,109 +173,6 @@ pub async fn execute( } }; - // #57 Phase 4 — parse the source-aware investigation filter once (shared by - // both the concrete and hybrid paths). Hard-errors on malformed input. - let source_filter = SourceFilter::from_args(&args)?; - - let concrete = args - .concrete - .unwrap_or_else(|| is_literal_query(&args.query)); - if concrete { - // When a tag_prefix OR a source filter is requested, fetch a larger - // pool so the post-filter has enough headroom to still return ~limit - // results after thinning. Cap at the same upper bound the underlying - // SQL path uses elsewhere (100). - let concrete_fetch_limit = if args.tag_prefix.is_some() || source_filter.is_active() { - (limit * 3).min(100) - } else { - limit - }; - let results = storage - .concrete_search_filtered( - &args.query, - concrete_fetch_limit, - args.include_types.as_deref(), - args.exclude_types.as_deref(), - ) - .map_err(|e| e.to_string())?; - - // Apply tag_prefix post-filter BEFORE strengthen-on-access so - // results the caller did not actually receive do not get a - // testing-effect boost. - let filtered_results: Vec<&vestige_core::SearchResult> = results - .iter() - .filter(|r| match args.tag_prefix.as_deref() { - Some(prefix) => tags_match_prefix(&r.node.tags, prefix), - None => true, - }) - .filter(|r| node_matches_source(&r.node, &source_filter)) - .take(limit as usize) - .collect(); - - let ids: Vec<&str> = filtered_results - .iter() - .map(|r| r.node.id.as_str()) - .collect(); - let _ = storage.strengthen_batch_on_access(&ids); - - let mut formatted: Vec = filtered_results - .iter() - .filter(|r| r.node.retention_strength >= min_retention) - .map(|r| format_search_result(r, detail_level)) - .collect(); - apply_output_masks(&mut formatted, output_config); - - let mut budget_expandable: Vec = Vec::new(); - let mut budget_tokens_used: Option = None; - if let Some(budget) = args.token_budget { - let budget = budget.clamp(100, 100000) as usize; - let budget_chars = budget * 4; - let mut used = 0; - let mut budgeted = Vec::new(); - - for result in &formatted { - let size = serde_json::to_string(result).unwrap_or_default().len(); - if used + size > budget_chars { - if let Some(id) = result.get("id").and_then(|v| v.as_str()) { - budget_expandable.push(id.to_string()); - } - continue; - } - used += size; - budgeted.push(result.clone()); - } - - budget_tokens_used = Some(used / 4); - formatted = budgeted; - } - - let mut response = serde_json::json!({ - "query": args.query, - "method": "concrete", - "retrievalMode": retrieval_mode, - "concrete": true, - "detailLevel": detail_level, - "profile": output_config.profile.as_str(), - "total": formatted.len(), - "results": formatted, - }); - - if formatted.is_empty() { - response["hint"] = serde_json::json!( - "No concrete matches found. Try concrete=false or a broader natural-language query." - ); - } - if !budget_expandable.is_empty() { - response["expandable"] = serde_json::json!(budget_expandable); - } - if let Some(tokens) = budget_tokens_used { - response["tokenBudgetUsed"] = serde_json::json!(tokens); - response["tokenBudgetLimit"] = serde_json::json!(args.token_budget.unwrap()); - } - - return Ok(response); - } - // Favor semantic search — research shows 0.3/0.7 outperforms equal weights let keyword_weight = 0.3_f32; let semantic_weight = 0.7_f32; @@ -390,15 +219,7 @@ pub async fn execute( "exhaustive" => 5, // Deep overfetch for maximum recall _ => 3, // Balanced default }; - // When a tag_prefix OR source filter is requested, double the overfetch - // (capped at the same 100 ceiling) so the post-filter has enough headroom - // to still return ~limit results after thinning. - let post_filter_multiplier = if args.tag_prefix.is_some() || source_filter.is_active() { - 2 - } else { - 1 - }; - let overfetch_limit = (limit * overfetch_multiplier * post_filter_multiplier).min(100); // Cap at 100 to avoid excessive DB load + let overfetch_limit = (limit * overfetch_multiplier).min(100); // Cap at 100 to avoid excessive DB load let results = storage .hybrid_search_filtered( @@ -427,34 +248,10 @@ pub async fn execute( }) .collect(); - // Apply tag_prefix post-filter BEFORE the reranker so the (expensive) - // cross-encoder does not waste cycles on memories the caller will not - // receive. The Stage 0 keyword-priority merge below also respects the - // filter when applied, since merged items must have survived this step - // OR be re-introduced from keyword_priority_results (which we re-filter). - if let Some(prefix) = args.tag_prefix.as_deref() { - filtered_results.retain(|r| tags_match_prefix(&r.node.tags, prefix)); - } - // #57 Phase 4 — source-aware investigation post-filter (same precedent). - if source_filter.is_active() { - filtered_results.retain(|r| node_matches_source(&r.node, &source_filter)); - } - // ==================================================================== // Dedup: merge Stage 0 keyword-priority results into Stage 1 results // ==================================================================== for kp in &keyword_priority_results { - // Respect tag_prefix here too — Stage 0 ran without it and can - // re-introduce filtered-out memories on the "new result" branch. - if let Some(prefix) = args.tag_prefix.as_deref() - && !tags_match_prefix(&kp.node.tags, prefix) - { - continue; - } - // Respect the source filter on re-inject for the same reason. - if source_filter.is_active() && !node_matches_source(&kp.node, &source_filter) { - continue; - } if let Some(existing) = filtered_results .iter_mut() .find(|r| r.node.id == kp.node.id) @@ -508,8 +305,7 @@ pub async fn execute( .unwrap_or(std::cmp::Ordering::Equal) }); - // Rerank the remaining candidates when the vector-search search stack is enabled. - #[cfg(feature = "vector-search")] + // Rerank the remaining candidates let reranked_results: Vec = if rerank_candidates.is_empty() { Vec::new() } else if let Ok(mut cog) = cognitive.try_lock() { @@ -534,11 +330,6 @@ pub async fn execute( .cloned() .collect() }; - #[cfg(not(feature = "vector-search"))] - let reranked_results: Vec = rerank_candidates - .into_iter() - .map(|(result, _)| result) - .collect(); // Merge: bypass first, then reranked, trim to limit filtered_results = bypass_results; @@ -549,7 +340,6 @@ pub async fn execute( // ==================================================================== // STAGE 3: Temporal boosting (recency + validity windows) // ==================================================================== - #[cfg(feature = "vector-search")] if let Ok(cog) = cognitive.try_lock() { for result in &mut filtered_results { let recency = cog.temporal_searcher.recency_boost(result.node.created_at); @@ -781,16 +571,6 @@ pub async fn execute( }; cog.reconsolidation.mark_labile(&result.node.id, snapshot); } - - // 7D. Feed the co-access channel: the memories returned together by one - // query WERE accessed together, so record_usage learns their co-access - // patterns (trigger -> predicted). Without this call the speculative - // retriever's co-access prediction channel stays permanently empty. - if filtered_results.len() >= 2 { - let accessed: Vec = - filtered_results.iter().map(|r| r.node.id.clone()).collect(); - cog.speculative_retriever.record_usage(&[], &accessed); - } } // ==================================================================== @@ -800,7 +580,6 @@ pub async fn execute( .iter() .map(|r| format_search_result(r, detail_level)) .collect(); - apply_output_masks(&mut formatted, output_config); // ==================================================================== // Token budget enforcement (v1.8.0) @@ -841,7 +620,6 @@ pub async fn execute( "method": "hybrid+cognitive", "retrievalMode": retrieval_mode, "detailLevel": detail_level, - "profile": output_config.profile.as_str(), "total": formatted.len(), "results": formatted, }); @@ -883,265 +661,7 @@ pub async fn execute( Ok(response) } -fn is_literal_query(query: &str) -> bool { - let trimmed = query.trim(); - if trimmed.len() >= 2 { - let bytes = trimmed.as_bytes(); - if (bytes[0] == b'"' && bytes[bytes.len() - 1] == b'"') - || (bytes[0] == b'\'' && bytes[bytes.len() - 1] == b'\'') - { - return true; - } - } - - if uuid::Uuid::parse_str(trimmed).is_ok() { - return true; - } - - let token_count = trimmed.split_whitespace().count(); - if token_count != 1 { - return false; - } - - let has_identifier_punctuation = trimmed - .chars() - .any(|c| matches!(c, '_' | '-' | '/' | '\\' | '.' | ':' | '=' | '@')); - if has_identifier_punctuation { - return true; - } - - let has_alpha = trimmed.chars().any(|c| c.is_ascii_alphabetic()); - has_alpha - && trimmed.contains('_') - && trimmed - .chars() - .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_') -} - -/// Returns `true` when the given tag list contains at least one tag whose -/// string value starts with `prefix`. Empty prefix matches every result with -/// at least one tag (and never matches a tagless result). -/// -/// Case-sensitive by design: the existing tag-match semantics in -/// `memory_timeline` / `export` / `gc` are exact-match (case-sensitive), so -/// keeping this consistent avoids surprise. Operators wanting case-insensitive -/// prefix-search should normalize tags at ingest time. -fn tags_match_prefix(tags: &[String], prefix: &str) -> bool { - tags.iter().any(|t| t.starts_with(prefix)) -} - -/// Validity filter for source-aware search (#57 Phase 4). -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -enum SourceStatus { - /// No validity constraint. - #[default] - Any, - /// Only currently-valid records. - Valid, - /// Only tombstoned records (no longer visible upstream, kept for audit). - Tombstoned, -} - -/// Parsed source-aware investigation filter (#57 Phase 4). -/// -/// All fields are optional; an all-empty filter matches every node (so search -/// behavior is byte-for-byte unchanged when no source filter is supplied). Any -/// source-scoped field being set excludes legacy/agent memories that have no -/// `source_envelope`. Applied as a post-filter on the recalled nodes, mirroring -/// the existing `tag_prefix` precedent (no SQL changes). -#[derive(Debug, Clone, Default)] -struct SourceFilter { - system: Option, - project: Option, - id: Option, - source_type: Option, - author: Option, - updated_after: Option>, - updated_before: Option>, - status: SourceStatus, -} - -impl SourceFilter { - /// Build from raw args, hard-erroring on malformed timestamps / status enum - /// (consistent with how `detail_level` / `retrieval_mode` reject bad input — - /// a silently-`None` bound would widen the filter and return wrong rows). - fn from_args(args: &SearchArgs) -> Result { - let parse_ts = |s: &Option, - field: &str| - -> Result>, String> { - match s { - None => Ok(None), - Some(v) => chrono::DateTime::parse_from_rfc3339(v) - .map(|dt| Some(dt.with_timezone(&chrono::Utc))) - .map_err(|_| format!("Invalid {field}: '{v}' is not an RFC3339 timestamp")), - } - }; - let status = match args.source_status.as_deref() { - None | Some("any") => SourceStatus::Any, - Some("valid") => SourceStatus::Valid, - Some("tombstoned") => SourceStatus::Tombstoned, - Some(other) => { - return Err(format!( - "Invalid source_status '{other}'. Must be 'any', 'valid', or 'tombstoned'." - )); - } - }; - Ok(Self { - system: args.source_system.clone(), - project: args.source_project.clone(), - id: args.source_id.clone(), - source_type: args.source_type.clone(), - author: args.source_author.clone(), - updated_after: parse_ts(&args.source_updated_after, "source_updated_after")?, - updated_before: parse_ts(&args.source_updated_before, "source_updated_before")?, - status, - }) - } - - /// True when at least one filter is set (used to size the over-fetch pool). - fn is_active(&self) -> bool { - self.system.is_some() - || self.project.is_some() - || self.id.is_some() - || self.source_type.is_some() - || self.author.is_some() - || self.updated_after.is_some() - || self.updated_before.is_some() - || self.status != SourceStatus::Any - } -} - -/// Predicate: does this node satisfy the source-aware investigation filter? -/// An all-empty filter returns `true` for every node. -fn node_matches_source(node: &vestige_core::KnowledgeNode, filter: &SourceFilter) -> bool { - // Validity check operates on the NODE (valid_until lives on the node). - match filter.status { - SourceStatus::Any => {} - SourceStatus::Valid if !node.is_currently_valid() => return false, - SourceStatus::Tombstoned if node.is_currently_valid() => return false, - _ => {} - } - - // Any source-scoped field requires an envelope; legacy memories are out. - // This includes `source_status=valid`: otherwise a source-scoped query for - // valid connector records would also return ordinary valid agent memories. - let envelope_scoped = filter.system.is_some() - || filter.project.is_some() - || filter.id.is_some() - || filter.source_type.is_some() - || filter.author.is_some() - || filter.updated_after.is_some() - || filter.updated_before.is_some() - || filter.status != SourceStatus::Any; - if !envelope_scoped { - return true; - } - let Some(env) = node.source_envelope.as_ref() else { - return false; - }; - - let exact = |want: &Option, have: &Option| -> bool { - match want { - None => true, - Some(w) => have.as_deref() == Some(w.as_str()), - } - }; - if !exact(&filter.system, &env.source_system) { - return false; - } - if !exact(&filter.project, &env.source_project) { - return false; - } - if !exact(&filter.id, &env.source_id) { - return false; - } - if !exact(&filter.source_type, &env.source_type) { - return false; - } - if !exact(&filter.author, &env.source_author) { - return false; - } - // Date bounds (inclusive) on the source-updated time. - if filter.updated_after.is_some() || filter.updated_before.is_some() { - let Some(ts) = env.source_updated_at else { - return false; - }; - if let Some(after) = filter.updated_after - && ts < after - { - return false; - } - if let Some(before) = filter.updated_before - && ts > before - { - return false; - } - } - true -} - /// Format a search result based on the requested detail level. -/// Score field keys dropped when an output profile suppresses scores. -const SCORE_FIELDS: &[&str] = &["combinedScore", "keywordScore", "semanticScore"]; -/// Timestamp field keys dropped when an output profile suppresses timestamps. -const TIMESTAMP_FIELDS: &[&str] = &[ - "createdAt", - "updatedAt", - "lastAccessed", - "nextReview", - "validFrom", - "validUntil", -]; - -/// Strip score/timestamp fields from already-formatted result objects according -/// to the active output profile (e.g. the `lean` profile drops both). Tools -/// call this after formatting so the field-mask behavior is centralized and the -/// per-detail-level formatters stay unchanged. -pub fn apply_output_masks(results: &mut [Value], output_config: &OutputConfig) { - if output_config.show_scores && output_config.show_timestamps { - return; - } - for result in results.iter_mut() { - if let Some(obj) = result.as_object_mut() { - if !output_config.show_scores { - for key in SCORE_FIELDS { - obj.remove(*key); - } - } - if !output_config.show_timestamps { - for key in TIMESTAMP_FIELDS { - obj.remove(*key); - } - } - } - } -} - -/// Build a compact `source` object from a node's connector provenance (#57), -/// or `Value::Null` when the memory has no external source envelope. -/// -/// Surfacing `url` in search results is the whole point of the connector layer: -/// the agent can follow the citation back to the canonical Redmine/GitHub record -/// for authoritative detail. `tombstoned` flags records no longer visible -/// upstream (kept for audit). -fn source_provenance(node: &vestige_core::KnowledgeNode) -> Value { - let Some(env) = node.source_envelope.as_ref() else { - return Value::Null; - }; - serde_json::json!({ - "system": env.source_system, - "id": env.source_id, - "url": env.source_url, - "project": env.source_project, - "type": env.source_type, - "author": env.source_author, - "sourceUpdatedAt": env.source_updated_at.map(|dt| dt.to_rfc3339()), - "syncedAt": env.synced_at.map(|dt| dt.to_rfc3339()), - // A tombstoned (no-longer-visible) record has valid_until set in the past. - "tombstoned": !node.is_currently_valid(), - }) -} - fn format_search_result(r: &vestige_core::SearchResult, detail_level: &str) -> Value { match detail_level { "brief" => serde_json::json!({ @@ -1151,65 +671,45 @@ fn format_search_result(r: &vestige_core::SearchResult, detail_level: &str) -> V "retentionStrength": r.node.retention_strength, "combinedScore": r.combined_score, }), - "full" => { - let mut v = serde_json::json!({ - "id": r.node.id, - "content": r.node.content, - "combinedScore": r.combined_score, - "keywordScore": r.keyword_score, - "semanticScore": r.semantic_score, - "nodeType": r.node.node_type, - "tags": r.node.tags, - "retentionStrength": r.node.retention_strength, - "storageStrength": r.node.storage_strength, - "retrievalStrength": r.node.retrieval_strength, - "source": r.node.source, - "sentimentScore": r.node.sentiment_score, - "sentimentMagnitude": r.node.sentiment_magnitude, - "createdAt": r.node.created_at.to_rfc3339(), - "updatedAt": r.node.updated_at.to_rfc3339(), - "lastAccessed": r.node.last_accessed.to_rfc3339(), - "nextReview": r.node.next_review.map(|dt| dt.to_rfc3339()), - "stability": r.node.stability, - "difficulty": r.node.difficulty, - "reps": r.node.reps, - "lapses": r.node.lapses, - "validFrom": r.node.valid_from.map(|dt| dt.to_rfc3339()), - "validUntil": r.node.valid_until.map(|dt| dt.to_rfc3339()), - "matchType": format!("{:?}", r.match_type), - }); - attach_source_record(&mut v, &r.node); - v - } + "full" => serde_json::json!({ + "id": r.node.id, + "content": r.node.content, + "combinedScore": r.combined_score, + "keywordScore": r.keyword_score, + "semanticScore": r.semantic_score, + "nodeType": r.node.node_type, + "tags": r.node.tags, + "retentionStrength": r.node.retention_strength, + "storageStrength": r.node.storage_strength, + "retrievalStrength": r.node.retrieval_strength, + "source": r.node.source, + "sentimentScore": r.node.sentiment_score, + "sentimentMagnitude": r.node.sentiment_magnitude, + "createdAt": r.node.created_at.to_rfc3339(), + "updatedAt": r.node.updated_at.to_rfc3339(), + "lastAccessed": r.node.last_accessed.to_rfc3339(), + "nextReview": r.node.next_review.map(|dt| dt.to_rfc3339()), + "stability": r.node.stability, + "difficulty": r.node.difficulty, + "reps": r.node.reps, + "lapses": r.node.lapses, + "validFrom": r.node.valid_from.map(|dt| dt.to_rfc3339()), + "validUntil": r.node.valid_until.map(|dt| dt.to_rfc3339()), + "matchType": format!("{:?}", r.match_type), + }), // "summary" (default) — includes dates so AI never has to guess when a memory is from - _ => { - let mut v = serde_json::json!({ - "id": r.node.id, - "content": r.node.content, - "combinedScore": r.combined_score, - "keywordScore": r.keyword_score, - "semanticScore": r.semantic_score, - "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(), - }); - attach_source_record(&mut v, &r.node); - v - } - } -} - -/// Inject a `sourceRecord` object into a result `Value` ONLY when the memory -/// has external provenance, so legacy (agent/user-authored) memories keep their -/// exact prior result shape. -fn attach_source_record(value: &mut Value, node: &vestige_core::KnowledgeNode) { - let provenance = source_provenance(node); - if !provenance.is_null() - && let Value::Object(map) = value - { - map.insert("sourceRecord".to_string(), provenance); + _ => serde_json::json!({ + "id": r.node.id, + "content": r.node.content, + "combinedScore": r.combined_score, + "keywordScore": r.keyword_score, + "semanticScore": r.semantic_score, + "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(), + }), } } @@ -1223,44 +723,36 @@ pub fn format_node(node: &vestige_core::KnowledgeNode, detail_level: &str) -> Va "tags": node.tags, "retentionStrength": node.retention_strength, }), - "full" => { - let mut v = serde_json::json!({ - "id": node.id, - "content": node.content, - "nodeType": node.node_type, - "tags": node.tags, - "retentionStrength": node.retention_strength, - "storageStrength": node.storage_strength, - "retrievalStrength": node.retrieval_strength, - "source": node.source, - "sentimentScore": node.sentiment_score, - "sentimentMagnitude": node.sentiment_magnitude, - "createdAt": node.created_at.to_rfc3339(), - "updatedAt": node.updated_at.to_rfc3339(), - "lastAccessed": node.last_accessed.to_rfc3339(), - "nextReview": node.next_review.map(|dt| dt.to_rfc3339()), - "stability": node.stability, - "difficulty": node.difficulty, - "reps": node.reps, - "lapses": node.lapses, - "validFrom": node.valid_from.map(|dt| dt.to_rfc3339()), - "validUntil": node.valid_until.map(|dt| dt.to_rfc3339()), - }); - attach_source_record(&mut v, node); - v - } + "full" => serde_json::json!({ + "id": node.id, + "content": node.content, + "nodeType": node.node_type, + "tags": node.tags, + "retentionStrength": node.retention_strength, + "storageStrength": node.storage_strength, + "retrievalStrength": node.retrieval_strength, + "source": node.source, + "sentimentScore": node.sentiment_score, + "sentimentMagnitude": node.sentiment_magnitude, + "createdAt": node.created_at.to_rfc3339(), + "updatedAt": node.updated_at.to_rfc3339(), + "lastAccessed": node.last_accessed.to_rfc3339(), + "nextReview": node.next_review.map(|dt| dt.to_rfc3339()), + "stability": node.stability, + "difficulty": node.difficulty, + "reps": node.reps, + "lapses": node.lapses, + "validFrom": node.valid_from.map(|dt| dt.to_rfc3339()), + "validUntil": node.valid_until.map(|dt| dt.to_rfc3339()), + }), // "summary" (default) - _ => { - let mut v = serde_json::json!({ - "id": node.id, - "content": node.content, - "nodeType": node.node_type, - "tags": node.tags, - "retentionStrength": node.retention_strength, - }); - attach_source_record(&mut v, node); - v - } + _ => serde_json::json!({ + "id": node.id, + "content": node.content, + "nodeType": node.node_type, + "tags": node.tags, + "retentionStrength": node.retention_strength, + }), } } @@ -1297,7 +789,6 @@ mod tests { tags: vec![], valid_from: None, valid_until: None, - source_envelope: None, }; let node = storage.ingest(input).unwrap(); node.id @@ -1311,13 +802,7 @@ mod tests { async fn test_search_empty_query_fails() { let (storage, _dir) = test_storage().await; let args = serde_json::json!({ "query": "" }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("empty")); } @@ -1326,13 +811,7 @@ mod tests { async fn test_search_whitespace_only_query_fails() { let (storage, _dir) = test_storage().await; let args = serde_json::json!({ "query": " \t\n " }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("empty")); } @@ -1340,7 +819,7 @@ mod tests { #[tokio::test] async fn test_search_missing_arguments_fails() { let (storage, _dir) = test_storage().await; - let result = execute(&storage, &test_cognitive(), &OutputConfig::default(), None).await; + let result = execute(&storage, &test_cognitive(), None).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("Missing arguments")); } @@ -1349,123 +828,11 @@ mod tests { async fn test_search_missing_query_field_fails() { let (storage, _dir) = test_storage().await; let args = serde_json::json!({ "limit": 10 }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("Invalid arguments")); } - #[test] - fn test_literal_query_detection() { - assert!(is_literal_query("\"exact phrase\"")); - assert!(is_literal_query("OPENAI_API_KEY")); - assert!(is_literal_query("mlx_lm.server")); - assert!(is_literal_query("src/main.rs")); - assert!(is_literal_query("4da778e2-1111-4444-8888-123456789abc")); - assert!(!is_literal_query("how should memory search work")); - } - - #[tokio::test] - async fn test_concrete_search_env_var_lands_first() { - let (storage, _dir) = test_storage().await; - ingest_test_content( - &storage, - "General OpenAI setup and API key rotation guidance", - ) - .await; - let target = ingest_test_content( - &storage, - "Release smoke test requires OPENAI_API_KEY to be set in the shell", - ) - .await; - ingest_test_content( - &storage, - "Credentials should be stored outside the repository", - ) - .await; - - let args = serde_json::json!({ - "query": "OPENAI_API_KEY", - "limit": 5 - }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await - .unwrap(); - - assert_eq!(result["method"], "concrete"); - assert_eq!(result["concrete"], true); - assert_eq!(result["results"][0]["id"], target); - } - - #[tokio::test] - async fn test_concrete_search_uuid_lands_first() { - let (storage, _dir) = test_storage().await; - let uuid = "4da778e2-1111-4444-8888-123456789abc"; - ingest_test_content(&storage, "Several memories mention release identifiers").await; - let target = ingest_test_content( - &storage, - &format!("The migration bug is tracked by exact id {}", uuid), - ) - .await; - - let args = serde_json::json!({ - "query": uuid, - "limit": 5 - }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await - .unwrap(); - - assert_eq!(result["method"], "concrete"); - assert_eq!(result["results"][0]["id"], target); - } - - #[tokio::test] - async fn test_concrete_search_process_name_lands_first() { - let (storage, _dir) = test_storage().await; - ingest_test_content( - &storage, - "The local MLX server can expose an OpenAI-compatible endpoint", - ) - .await; - let target = ingest_test_content( - &storage, - "If mlx_lm.server is already running, do not start a second Sanhedrin backend", - ) - .await; - - let args = serde_json::json!({ - "query": "mlx_lm.server", - "limit": 5 - }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await - .unwrap(); - - assert_eq!(result["method"], "concrete"); - assert_eq!(result["results"][0]["id"], target); - } - // ======================================================================== // LIMIT CLAMPING TESTS // ======================================================================== @@ -1480,13 +847,7 @@ mod tests { "query": "test", "limit": 0 }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_ok()); } @@ -1500,13 +861,7 @@ mod tests { "query": "test", "limit": 1000 }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_ok()); } @@ -1519,13 +874,7 @@ mod tests { "query": "test", "limit": -5 }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_ok()); } @@ -1542,13 +891,7 @@ mod tests { "query": "test", "min_retention": -0.5 }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_ok()); } @@ -1561,13 +904,7 @@ mod tests { "query": "test", "min_retention": 1.5 }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; // Should succeed but may return no results (retention > 1.0 clamped to 1.0) assert!(result.is_ok()); } @@ -1585,13 +922,7 @@ mod tests { "query": "test", "min_similarity": -0.5 }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_ok()); } @@ -1604,13 +935,7 @@ mod tests { "query": "test", "min_similarity": 1.5 }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; // Should succeed but may return no results assert!(result.is_ok()); } @@ -1625,13 +950,7 @@ mod tests { ingest_test_content(&storage, "The Rust programming language is memory safe.").await; let args = serde_json::json!({ "query": "rust" }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_ok()); let value = result.unwrap(); @@ -1651,13 +970,7 @@ mod tests { "query": "python", "min_similarity": 0.0 }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_ok()); let value = result.unwrap(); @@ -1679,13 +992,7 @@ mod tests { "limit": 2, "min_similarity": 0.0 }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_ok()); let value = result.unwrap(); @@ -1699,13 +1006,7 @@ mod tests { // Don't ingest anything - database is empty let args = serde_json::json!({ "query": "anything" }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_ok()); let value = result.unwrap(); @@ -1722,13 +1023,7 @@ mod tests { "query": "testing", "min_similarity": 0.0 }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_ok()); let value = result.unwrap(); @@ -1761,13 +1056,7 @@ mod tests { "query": "item", "min_similarity": 0.0 }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_ok()); let value = result.unwrap(); @@ -1853,13 +1142,7 @@ mod tests { "detail_level": "brief", "min_similarity": 0.0 }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_ok()); let value = result.unwrap(); @@ -1888,13 +1171,7 @@ mod tests { "detail_level": "full", "min_similarity": 0.0 }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_ok()); let value = result.unwrap(); @@ -1921,13 +1198,7 @@ mod tests { "query": "default", "min_similarity": 0.0 }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_ok()); let value = result.unwrap(); @@ -1956,13 +1227,7 @@ mod tests { "query": "test", "detail_level": "invalid_level" }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("Invalid detail_level")); } @@ -1991,13 +1256,7 @@ mod tests { "token_budget": 200, "min_similarity": 0.0 }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_ok()); let value = result.unwrap(); @@ -2024,13 +1283,7 @@ mod tests { "token_budget": 150, "min_similarity": 0.0 }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_ok()); let value = result.unwrap(); @@ -2049,13 +1302,7 @@ mod tests { "query": "no budget", "min_similarity": 0.0 }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_ok()); let value = result.unwrap(); @@ -2073,477 +1320,4 @@ mod tests { assert_eq!(tb["minimum"], 100); assert_eq!(tb["maximum"], 100000); } - - // ======================================================================== - // TAG_PREFIX TESTS (PR1) - // ======================================================================== - - #[test] - fn test_tags_match_prefix_unit() { - let with_meeting = vec!["meeting:standup".to_string(), "team".to_string()]; - let without_meeting = vec!["adhoc".to_string(), "team".to_string()]; - let tagless: Vec = vec![]; - - assert!(tags_match_prefix(&with_meeting, "meeting:")); - assert!(!tags_match_prefix(&without_meeting, "meeting:")); - // Empty prefix matches when any tag exists; never matches a tagless - // memory. This preserves the "tag_prefix is a filter, not a default - // wildcard" semantics — a tagless memory has no tag-prefix to satisfy. - assert!(tags_match_prefix(&with_meeting, "")); - assert!(!tags_match_prefix(&tagless, "")); - // Case-sensitive (consistent with existing exact-tag matching). - assert!(!tags_match_prefix(&with_meeting, "Meeting:")); - // Prefix must match from the start, not anywhere in the tag value. - assert!(!tags_match_prefix(&with_meeting, "standup")); - } - - #[test] - fn test_schema_has_tag_prefix() { - let schema_value = schema(); - let tp = &schema_value["properties"]["tag_prefix"]; - assert!(tp.is_object(), "tag_prefix property must be present"); - assert_eq!(tp["type"], "string"); - // tag_prefix is NOT required. - let required = schema_value["required"].as_array().unwrap(); - assert!(!required.contains(&serde_json::json!("tag_prefix"))); - } - - // ===================== #57 Phase 4 source filters ===================== - - /// Build a KnowledgeNode carrying a source envelope for filter tests. - fn node_with_source( - system: &str, - project: &str, - id: &str, - author: &str, - updated: &str, - ) -> vestige_core::KnowledgeNode { - let mut n = vestige_core::KnowledgeNode::default(); - n.id = format!("{system}-{id}"); - // SourceEnvelope is #[non_exhaustive]; build via Default + field set. - let mut env = vestige_core::SourceEnvelope::default(); - env.source_system = Some(system.to_string()); - env.source_id = Some(id.to_string()); - env.source_url = Some(format!("https://x/{id}")); - env.source_updated_at = chrono::DateTime::parse_from_rfc3339(updated) - .ok() - .map(|d| d.with_timezone(&chrono::Utc)); - env.content_hash = Some("h".to_string()); - env.source_project = Some(project.to_string()); - env.source_type = Some("issue".to_string()); - env.source_author = Some(author.to_string()); - n.source_envelope = Some(env); - n - } - - fn filter_from(json: serde_json::Value) -> SourceFilter { - let mut v = json; - v["query"] = serde_json::json!("q"); - let args: SearchArgs = serde_json::from_value(v).unwrap(); - SourceFilter::from_args(&args).unwrap() - } - - #[test] - fn source_filter_empty_matches_everything() { - let f = SourceFilter::default(); - assert!(!f.is_active()); - let gh = node_with_source("github", "o/r", "1", "octo", "2026-06-19T00:00:00Z"); - let legacy = vestige_core::KnowledgeNode::default(); // no envelope - assert!(node_matches_source(&gh, &f)); - assert!(node_matches_source(&legacy, &f), "no filter = unchanged"); - } - - #[test] - fn source_filter_exact_fields() { - let gh = node_with_source("github", "o/r", "57", "octo", "2026-06-19T00:00:00Z"); - let rm = node_with_source("redmine", "infra", "57", "jane", "2026-06-19T00:00:00Z"); - - let by_system = filter_from(serde_json::json!({"sourceSystem": "github"})); - assert!(node_matches_source(&gh, &by_system)); - assert!(!node_matches_source(&rm, &by_system)); - - let by_project = filter_from(serde_json::json!({"sourceProject": "infra"})); - assert!(node_matches_source(&rm, &by_project)); - assert!(!node_matches_source(&gh, &by_project)); - - let by_author = filter_from(serde_json::json!({"sourceAuthor": "octo"})); - assert!(node_matches_source(&gh, &by_author)); - assert!(!node_matches_source(&rm, &by_author)); - - // id + system together disambiguate across systems sharing an id. - let by_id_sys = - filter_from(serde_json::json!({"sourceSystem": "redmine", "sourceId": "57"})); - assert!(node_matches_source(&rm, &by_id_sys)); - assert!(!node_matches_source(&gh, &by_id_sys)); - } - - #[test] - fn source_filter_excludes_legacy_memories_when_envelope_scoped() { - let legacy = vestige_core::KnowledgeNode::default(); - let f = filter_from(serde_json::json!({"sourceSystem": "github"})); - assert!( - !node_matches_source(&legacy, &f), - "an envelope-scoped filter must exclude memories with no source" - ); - } - - #[test] - fn source_filter_date_bounds_inclusive() { - let n = node_with_source("github", "o/r", "1", "octo", "2026-06-15T12:00:00Z"); - // After bound: inclusive at the exact instant, excludes earlier. - assert!(node_matches_source( - &n, - &filter_from(serde_json::json!({"sourceUpdatedAfter": "2026-06-15T12:00:00Z"})) - )); - assert!(!node_matches_source( - &n, - &filter_from(serde_json::json!({"sourceUpdatedAfter": "2026-06-16T00:00:00Z"})) - )); - // Before bound: inclusive, excludes later. - assert!(node_matches_source( - &n, - &filter_from(serde_json::json!({"sourceUpdatedBefore": "2026-06-15T12:00:00Z"})) - )); - assert!(!node_matches_source( - &n, - &filter_from(serde_json::json!({"sourceUpdatedBefore": "2026-06-15T00:00:00Z"})) - )); - } - - #[test] - fn source_filter_status_valid_vs_tombstoned() { - let mut live = node_with_source("github", "o/r", "1", "octo", "2026-06-19T00:00:00Z"); - let mut dead = node_with_source("github", "o/r", "2", "octo", "2026-06-19T00:00:00Z"); - let legacy = vestige_core::KnowledgeNode::default(); - // Tombstone `dead` by setting valid_until in the past. - dead.valid_until = Some(chrono::Utc::now() - chrono::Duration::days(1)); - live.valid_until = None; - - let valid = filter_from(serde_json::json!({"sourceStatus": "valid"})); - assert!(node_matches_source(&live, &valid)); - assert!(!node_matches_source(&dead, &valid)); - assert!( - !node_matches_source(&legacy, &valid), - "source_status is source-scoped and must not include legacy memories" - ); - - let tomb = filter_from(serde_json::json!({"sourceStatus": "tombstoned"})); - assert!(!node_matches_source(&live, &tomb)); - assert!(node_matches_source(&dead, &tomb)); - assert!(!node_matches_source(&legacy, &tomb)); - } - - #[test] - fn source_filter_rejects_bad_timestamp_and_status() { - let mut v = serde_json::json!({"query": "q", "sourceUpdatedAfter": "not-a-date"}); - let args: SearchArgs = serde_json::from_value(v.take()).unwrap(); - assert!(SourceFilter::from_args(&args).is_err()); - - let mut v2 = serde_json::json!({"query": "q", "sourceStatus": "bogus"}); - let args2: SearchArgs = serde_json::from_value(v2.take()).unwrap(); - assert!(SourceFilter::from_args(&args2).is_err()); - } - - #[test] - fn test_schema_has_source_filters() { - let s = schema(); - for prop in [ - "source_system", - "source_project", - "source_id", - "source_type", - "source_author", - "source_updated_after", - "source_updated_before", - "source_status", - ] { - assert!( - s["properties"][prop].is_object(), - "schema must expose {prop}" - ); - } - // None of the source filters are required. - let required = s["required"].as_array().unwrap(); - for prop in ["source_system", "source_status"] { - assert!(!required.contains(&serde_json::json!(prop))); - } - } - - /// Helper that ingests a memory with specific tags. The base - /// `ingest_test_content` helper passes `tags: vec![]`, which is fine - /// for legacy tests but not for tag_prefix coverage. - async fn ingest_with_tags(storage: &Arc, content: &str, tags: Vec<&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: tags.into_iter().map(String::from).collect(), - valid_from: None, - valid_until: None, - source_envelope: None, - }; - let node = storage.ingest(input).unwrap(); - node.id - } - - #[tokio::test] - async fn test_search_tag_prefix_filters_results() { - let (storage, _dir) = test_storage().await; - // Three memories matching the query semantically, only two carry - // the meeting:* tag-class. - ingest_with_tags( - &storage, - "Standup discussion about Q3 roadmap blockers", - vec!["meeting:standup", "roadmap"], - ) - .await; - ingest_with_tags( - &storage, - "1-on-1 sync on roadmap clarity and ownership", - vec!["meeting:1-on-1", "roadmap"], - ) - .await; - ingest_with_tags( - &storage, - "Solo note: roadmap dependency graph audit", - vec!["adhoc", "roadmap"], - ) - .await; - - let args = serde_json::json!({ - "query": "roadmap", - "tag_prefix": "meeting:", - "min_similarity": 0.0 - }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; - assert!(result.is_ok(), "{:?}", result); - let value = result.unwrap(); - let results = value["results"].as_array().unwrap(); - // Both meeting:* memories should land; the adhoc one should not. - for r in results { - let tags = r["tags"].as_array().expect("tags must be present"); - let has_meeting = tags - .iter() - .any(|t| t.as_str().is_some_and(|s| s.starts_with("meeting:"))); - assert!(has_meeting, "result lacks meeting:* tag: {}", r); - } - // We expect 2 matches given the corpus above. The exact count - // depends on the cognitive pipeline's competition/suppression - // dynamics, so assert a lower bound. - assert!( - !results.is_empty(), - "tag_prefix should leave at least one meeting:* result, got {}", - results.len() - ); - } - - #[tokio::test] - async fn test_search_tag_prefix_excludes_tagless_memories() { - let (storage, _dir) = test_storage().await; - ingest_with_tags( - &storage, - "Notebook entry about consolidation cycles", - vec![], // tagless - ) - .await; - ingest_with_tags( - &storage, - "Project note about consolidation cycles", - vec!["project:vestige"], - ) - .await; - - let args = serde_json::json!({ - "query": "consolidation", - "tag_prefix": "project:", - "min_similarity": 0.0 - }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; - assert!(result.is_ok()); - let value = result.unwrap(); - let results = value["results"].as_array().unwrap(); - for r in results { - let tags = r["tags"].as_array().expect("tags must be present"); - let has_project = tags - .iter() - .any(|t| t.as_str().is_some_and(|s| s.starts_with("project:"))); - assert!(has_project, "tagless or non-project result leaked: {}", r); - } - } - - #[tokio::test] - async fn test_search_without_tag_prefix_unchanged() { - // Backwards-compat: same corpus, same query, no tag_prefix → all - // results pass through regardless of tag composition. This is the - // load-bearing test for additive-only behavior. - let (storage, _dir) = test_storage().await; - ingest_with_tags(&storage, "Notebook entry about audit cycles", vec![]).await; - ingest_with_tags( - &storage, - "Project note about audit cycles", - vec!["project:audit"], - ) - .await; - - let args = serde_json::json!({ - "query": "audit", - "min_similarity": 0.0 - }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; - assert!(result.is_ok()); - let value = result.unwrap(); - let results = value["results"].as_array().unwrap(); - // Both should be retrievable since no tag_prefix is set. - assert!( - !results.is_empty(), - "expected at least one result with no tag_prefix" - ); - } - - #[tokio::test] - async fn test_search_tag_prefix_concrete_path() { - // Concrete-search path (literal query) must also honor tag_prefix. - let (storage, _dir) = test_storage().await; - ingest_with_tags( - &storage, - "OPENAI_API_KEY rotation playbook for meetings", - vec!["meeting:ops"], - ) - .await; - ingest_with_tags( - &storage, - "OPENAI_API_KEY rotation playbook for solo audits", - vec!["adhoc"], - ) - .await; - - let args = serde_json::json!({ - "query": "OPENAI_API_KEY", - "concrete": true, - "tag_prefix": "meeting:" - }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; - assert!(result.is_ok(), "{:?}", result); - let value = result.unwrap(); - assert_eq!(value["method"], "concrete"); - let results = value["results"].as_array().unwrap(); - for r in results { - let tags = r["tags"].as_array().expect("tags must be present"); - let has_meeting = tags - .iter() - .any(|t| t.as_str().is_some_and(|s| s.starts_with("meeting:"))); - assert!(has_meeting, "concrete result lacks meeting:* tag: {}", r); - } - } - - // ======================================================================== - // Phase 2: Configurable Output — precedence tests - // ======================================================================== - - /// Config-file detail_level applies when no explicit MCP param is given. - #[tokio::test] - async fn test_config_detail_level_applies_without_param() { - let (storage, _dir) = test_storage().await; - ingest_test_content(&storage, "Config detail level fallback content.").await; - - // Config selects `full`; the call passes no detail_level. - let cfg = vestige_core::VestigeConfig::parse("[defaults]\ndetail_level=\"full\"").output(); - let args = serde_json::json!({ "query": "config detail", "min_similarity": 0.0 }); - let value = execute(&storage, &test_cognitive(), &cfg, Some(args)) - .await - .unwrap(); - assert_eq!(value["detailLevel"], "full"); - } - - /// Explicit MCP param beats the config file (precedence layer 1 > 2). - #[tokio::test] - async fn test_explicit_param_overrides_config() { - let (storage, _dir) = test_storage().await; - ingest_test_content(&storage, "Explicit overrides config content.").await; - - // Config says `full`, but the call explicitly requests `brief`. - let cfg = vestige_core::VestigeConfig::parse("[defaults]\ndetail_level=\"full\"").output(); - let args = serde_json::json!({ - "query": "explicit override", - "detail_level": "brief", - "min_similarity": 0.0 - }); - let value = execute(&storage, &test_cognitive(), &cfg, Some(args)) - .await - .unwrap(); - assert_eq!(value["detailLevel"], "brief"); - } - - /// The `lean` profile masks scores and timestamps from results. - #[tokio::test] - async fn test_lean_profile_masks_scores_and_timestamps() { - let (storage, _dir) = test_storage().await; - ingest_test_content(&storage, "Lean profile masking content.").await; - - let cfg = vestige_core::VestigeConfig::parse("[defaults]\nprofile=lean").output(); - let args = serde_json::json!({ "query": "lean masking", "min_similarity": 0.0 }); - let value = execute(&storage, &test_cognitive(), &cfg, Some(args)) - .await - .unwrap(); - assert_eq!(value["profile"], "lean"); - if let Some(first) = value["results"].as_array().and_then(|a| a.first()) { - assert!( - first.get("combinedScore").is_none(), - "lean must drop scores" - ); - assert!( - first.get("createdAt").is_none(), - "lean must drop timestamps" - ); - } - } - - /// The default profile is byte-for-byte the historical behavior: summary - /// detail with scores and timestamps present. - #[tokio::test] - async fn test_default_profile_preserves_behavior() { - let (storage, _dir) = test_storage().await; - ingest_test_content(&storage, "Default profile preserved content.").await; - - let args = serde_json::json!({ "query": "default preserved", "min_similarity": 0.0 }); - let value = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await - .unwrap(); - assert_eq!(value["detailLevel"], "summary"); - assert_eq!(value["profile"], "default"); - if let Some(first) = value["results"].as_array().and_then(|a| a.first()) { - assert!(first.get("createdAt").is_some(), "default keeps timestamps"); - } - } } diff --git a/crates/vestige-mcp/src/tools/session_context.rs b/crates/vestige-mcp/src/tools/session_context.rs index 3f17994..e7414eb 100644 --- a/crates/vestige-mcp/src/tools/session_context.rs +++ b/crates/vestige-mcp/src/tools/session_context.rs @@ -13,7 +13,7 @@ use serde::Deserialize; use serde_json::Value; use crate::cognitive::CognitiveEngine; -use vestige_core::{OutputConfig, Storage}; +use vestige_core::Storage; /// Input schema for session_context tool pub fn schema() -> Value { @@ -98,7 +98,6 @@ fn first_sentence(content: &str) -> String { pub async fn execute( storage: &Arc, cognitive: &Arc>, - output_config: &OutputConfig, args: Option, ) -> Result { let args: SessionContextArgs = match args { @@ -106,14 +105,6 @@ pub async fn execute( None => SessionContextArgs::default(), }; - // Per-query search width honors the active profile (e.g. `research` widens - // it, `lean` narrows it). No explicit MCP param exists here, so the config - // limit (or built-in default of 5) applies. Capped to keep the budgeted - // session response compact. - let per_query_limit = output_config.resolve_limit(None, 5).clamp(1, 25); - // The `lean` profile suppresses the inline memory date to save tokens. - let show_dates = output_config.show_timestamps; - 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); @@ -135,7 +126,7 @@ pub async fn execute( for query in &queries { let results = storage - .hybrid_search(query, per_query_limit, 0.3, 0.7) + .hybrid_search(query, 5, 0.3, 0.7) .map_err(|e| e.to_string())?; for r in results { @@ -143,12 +134,8 @@ pub async fn execute( continue; } let summary = first_sentence(&r.node.content); - let line = if show_dates { - let date_str = r.node.updated_at.format("%b %d, %Y").to_string(); - format!("- ({}) {}", date_str, summary) - } else { - 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 { @@ -236,7 +223,7 @@ pub async fn execute( Some(dt) => storage.count_memories_since(*dt).unwrap_or(0), None => stats.total_nodes, }; - let last_backup = storage.last_backup_timestamp(); + let last_backup = Storage::get_last_backup_timestamp(); let now = Utc::now(); let needs_dream = last_dream @@ -249,7 +236,7 @@ pub async fn execute( if include_status { let embedding_pct = if stats.total_nodes > 0 { - (stats.nodes_with_active_embeddings as f64 / stats.total_nodes as f64) * 100.0 + (stats.nodes_with_embeddings as f64 / stats.total_nodes as f64) * 100.0 } else { 0.0 }; @@ -397,7 +384,6 @@ pub async fn execute( Ok(serde_json::json!({ "context": context_text, - "profile": output_config.profile.as_str(), "tokensUsed": tokens_used, "tokenBudget": token_budget, "expandable": expandable_ids, @@ -506,7 +492,6 @@ mod tests { tags: tags.into_iter().map(|s| s.to_string()).collect(), valid_from: None, valid_until: None, - source_envelope: None, }; let node = storage.ingest(input).unwrap(); node.id @@ -544,7 +529,7 @@ mod tests { #[tokio::test] async fn test_default_no_args() { let (storage, _dir) = test_storage().await; - let result = execute(&storage, &test_cognitive(), &OutputConfig::default(), None).await; + let result = execute(&storage, &test_cognitive(), None).await; assert!(result.is_ok()); let value = result.unwrap(); @@ -561,21 +546,15 @@ mod tests { let (storage, _dir) = test_storage().await; ingest_test_content( &storage, - "The user prefers Rust and TypeScript for all projects.", + "Sam prefers Rust and TypeScript for all projects.", vec![], ) .await; let args = serde_json::json!({ - "queries": ["user preferences", "project context"] + "queries": ["Sam preferences", "project context"] }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_ok()); let value = result.unwrap(); @@ -603,13 +582,7 @@ mod tests { "queries": ["memory"], "token_budget": 200 }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_ok()); let value = result.unwrap(); @@ -645,13 +618,7 @@ mod tests { "queries": ["expandable test memory"], "token_budget": 150 }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_ok()); let value = result.unwrap(); @@ -662,7 +629,7 @@ mod tests { #[tokio::test] async fn test_automation_triggers_booleans() { let (storage, _dir) = test_storage().await; - let result = execute(&storage, &test_cognitive(), &OutputConfig::default(), None).await; + let result = execute(&storage, &test_cognitive(), None).await; assert!(result.is_ok()); let value = result.unwrap(); @@ -682,13 +649,7 @@ mod tests { "include_intentions": false, "include_predictions": false }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_ok()); let value = result.unwrap(); @@ -713,7 +674,6 @@ mod tests { tags: vec!["pattern".to_string(), "codebase:vestige".to_string()], valid_from: None, valid_until: None, - source_envelope: None, }; storage.ingest(input).unwrap(); @@ -723,13 +683,7 @@ mod tests { "topics": ["performance"] } }); - let result = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await; + let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_ok()); let value = result.unwrap(); @@ -738,42 +692,6 @@ mod tests { assert!(ctx.contains("vestige")); } - /// Phase 2: the response echoes the active profile, and the `lean` profile - /// suppresses inline memory dates to save tokens. - #[tokio::test] - async fn test_session_context_profile_echo_and_lean_dates() { - let (storage, _dir) = test_storage().await; - ingest_test_content(&storage, "Session profile content sentence.", vec![]).await; - - // Default profile -> profile echoed, dates present. - let args = serde_json::json!({ "queries": ["profile content"] }); - let value = execute( - &storage, - &test_cognitive(), - &OutputConfig::default(), - Some(args), - ) - .await - .unwrap(); - assert_eq!(value["profile"], "default"); - - // Lean profile -> profile echoed as lean. The memory line must not carry - // the "(Mon DD, YYYY)" inline date prefix. - let cfg = vestige_core::VestigeConfig::parse("[defaults]\nprofile=lean").output(); - let args = serde_json::json!({ "queries": ["profile content"] }); - let value = execute(&storage, &test_cognitive(), &cfg, Some(args)) - .await - .unwrap(); - assert_eq!(value["profile"], "lean"); - let ctx = value["context"].as_str().unwrap(); - if ctx.contains("**Memories:**") { - assert!( - !ctx.contains(", 20"), - "lean profile should omit the inline year in memory dates" - ); - } - } - // ======================================================================== // HELPER TESTS // ======================================================================== diff --git a/crates/vestige-mcp/src/tools/smart_ingest.rs b/crates/vestige-mcp/src/tools/smart_ingest.rs index 77c1afe..72dc3a6 100644 --- a/crates/vestige-mcp/src/tools/smart_ingest.rs +++ b/crates/vestige-mcp/src/tools/smart_ingest.rs @@ -57,15 +57,9 @@ pub fn schema() -> Value { "description": "Force creation of a new memory even if similar content exists", "default": false }, - "batchMergePolicy": { - "type": "string", - "enum": ["force_create", "smart"], - "description": "Batch mode only. Defaults to 'force_create' so caller-separated items stay separate. Use 'smart' to allow Prediction Error Gating against existing memories.", - "default": "force_create" - }, "items": { "type": "array", - "description": "Batch mode: array of items to save (max 20). Defaults to force-creating each caller-separated item; set batchMergePolicy='smart' to allow Prediction Error Gating against existing memories. Use at session end or before context compaction.", + "description": "Batch mode: array of items to save (max 20). Each runs through full cognitive pipeline with Prediction Error Gating. Use at session end or before context compaction.", "maxItems": 20, "items": { "type": "object", @@ -110,7 +104,6 @@ struct SmartIngestArgs { tags: Option>, source: Option, force_create: Option, - batch_merge_policy: Option, items: Option>, } @@ -138,28 +131,8 @@ pub async fn execute( // Detect mode: batch (items present) vs single (content present) if let Some(items) = args.items { - let batch_merge_policy = args - .batch_merge_policy - .unwrap_or_else(|| "force_create".to_string()); - let default_force_create = match batch_merge_policy.as_str() { - "force_create" => true, - "smart" => false, - other => { - return Err(format!( - "Invalid batchMergePolicy '{}'. Must be 'force_create' or 'smart'.", - other - )); - } - }; - let global_force = match args.force_create { - // An EXPLICIT forceCreate is authoritative and must be honored in both - // policies. Previously `Some(false)` under the default 'force_create' - // policy fell through to `default_force_create` (= true), silently - // inverting the caller's explicit false into a force-create. - Some(explicit) => explicit, - None => default_force_create, - }; - return execute_batch(storage, cognitive, items, global_force, &batch_merge_policy).await; + let global_force = args.force_create.unwrap_or(false); + return execute_batch(storage, cognitive, items, global_force).await; } // Single mode: content is required @@ -217,7 +190,6 @@ pub async fn execute( tags, valid_from: None, valid_until: None, - source_envelope: None, }; // ==================================================================== @@ -280,9 +252,6 @@ pub async fn execute( "similarity": result.similarity, "predictionError": result.prediction_error, "supersededId": result.superseded_id, - "previousContent": result.previous_content, - "mergedFrom": result.merged_from, - "mergePreview": result.merge_preview, "importanceScore": importance_composite, "reason": result.reason, "explanation": match result.decision.as_str() { @@ -336,7 +305,6 @@ async fn execute_batch( cognitive: &Arc>, items: Vec, global_force_create: bool, - batch_merge_policy: &str, ) -> Result { if items.is_empty() { return Err("Items array cannot be empty".to_string()); @@ -347,13 +315,9 @@ async fn execute_batch( let mut results = Vec::new(); let mut created = 0u32; - #[cfg(all(feature = "embeddings", feature = "vector-search"))] let mut updated = 0u32; - #[cfg(not(all(feature = "embeddings", feature = "vector-search")))] - let updated = 0u32; let mut skipped = 0u32; let mut errors = 0u32; - let mut batch_created_node_ids: Vec = Vec::new(); for (i, item) in items.into_iter().enumerate() { // Skip empty content @@ -417,7 +381,6 @@ async fn execute_batch( tags, valid_from: None, valid_until: None, - source_envelope: None, }; // ================================================================ @@ -434,7 +397,6 @@ async fn execute_batch( let node_type = node.node_type.clone(); created += 1; - batch_created_node_ids.push(node_id.clone()); run_post_ingest( cognitive, &node_id, @@ -466,18 +428,15 @@ async fn execute_batch( #[cfg(all(feature = "embeddings", feature = "vector-search"))] { - match storage.smart_ingest_excluding(input, &batch_created_node_ids) { + 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(); match result.decision.as_str() { - "create" | "supersede" | "merge" => { - created += 1; - batch_created_node_ids.push(node_id.clone()); - } - "update" | "reinforce" | "replace" | "add_context" => updated += 1, + "create" | "supersede" | "replace" => created += 1, + "update" | "reinforce" | "merge" | "add_context" => updated += 1, _ => created += 1, } @@ -496,11 +455,6 @@ async fn execute_batch( "decision": result.decision, "nodeId": node_id, "similarity": result.similarity, - "predictionError": result.prediction_error, - "supersededId": result.superseded_id, - "previousContent": result.previous_content, - "mergedFrom": result.merged_from, - "mergePreview": result.merge_preview, "importanceScore": importance_composite, "reason": result.reason })); @@ -525,7 +479,6 @@ async fn execute_batch( let node_type = node.node_type.clone(); created += 1; - batch_created_node_ids.push(node_id.clone()); run_post_ingest( cognitive, &node_id, @@ -558,7 +511,6 @@ async fn execute_batch( Ok(serde_json::json!({ "success": errors == 0, "mode": "batch", - "batchMergePolicy": batch_merge_policy, "summary": { "total": results.len(), "created": created, @@ -682,7 +634,6 @@ mod tests { assert_eq!(schema_value["type"], "object"); assert!(schema_value["properties"]["content"].is_object()); assert!(schema_value["properties"]["forceCreate"].is_object()); - assert!(schema_value["properties"]["batchMergePolicy"].is_object()); assert!(schema_value["properties"]["items"].is_object()); // v1.7: no top-level required — content for single mode, items for batch mode assert!(schema_value.get("required").is_none() || schema_value["required"].is_null()); @@ -853,84 +804,9 @@ mod tests { assert!(result.is_ok()); let value = result.unwrap(); assert_eq!(value["mode"], "batch"); - assert_eq!(value["batchMergePolicy"], "force_create"); assert_eq!(value["summary"]["total"], 2); } - #[tokio::test] - async fn test_batch_defaults_to_force_create_for_caller_separated_items() { - // Default policy (no explicit forceCreate) force-creates each - // caller-separated item so they stay separate. - let (storage, _dir) = test_storage().await; - let result = execute( - &storage, - &test_cognitive(), - Some(serde_json::json!({ - "items": [ - { "content": "Jira tickets should not auto-assign sprint fields." }, - { "content": "Sprint planning summaries should not append Jira status labels." } - ] - })), - ) - .await; - - let value = result.unwrap(); - assert_eq!(value["batchMergePolicy"], "force_create"); - assert_eq!(value["summary"]["created"], 2); - assert_eq!(value["summary"]["updated"], 0); - for item in value["results"].as_array().unwrap() { - assert_eq!(item["decision"], "create"); - assert!(item["reason"].as_str().unwrap().contains("Forced creation")); - } - } - - #[tokio::test] - async fn test_batch_explicit_force_create_false_is_honored() { - // Regression (#130): an EXPLICIT forceCreate:false must NOT be silently - // inverted to force-create by the default policy. Distinct/novel items are - // still created (PE gating creates novel content), but NOT via the - // "Forced creation" path. - let (storage, _dir) = test_storage().await; - let result = execute( - &storage, - &test_cognitive(), - Some(serde_json::json!({ - "forceCreate": false, - "items": [ - { "content": "Jira tickets should not auto-assign sprint fields." }, - { "content": "Sprint planning summaries should not append Jira status labels." } - ] - })), - ) - .await; - - let value = result.unwrap(); - assert_eq!(value["summary"]["created"], 2, "novel items still created"); - for item in value["results"].as_array().unwrap() { - assert!( - !item["reason"].as_str().unwrap().contains("Forced creation"), - "explicit forceCreate:false must not force-create" - ); - } - } - - #[tokio::test] - async fn test_batch_rejects_invalid_merge_policy() { - let (storage, _dir) = test_storage().await; - let result = execute( - &storage, - &test_cognitive(), - Some(serde_json::json!({ - "batchMergePolicy": "merge_everything", - "items": [{ "content": "Invalid policy should fail." }] - })), - ) - .await; - - assert!(result.is_err()); - assert!(result.unwrap_err().contains("batchMergePolicy")); - } - #[tokio::test] async fn test_batch_skips_empty_content() { let (storage, _dir) = test_storage().await; diff --git a/crates/vestige-mcp/src/tools/source_sync.rs b/crates/vestige-mcp/src/tools/source_sync.rs deleted file mode 100644 index ec9f284..0000000 --- a/crates/vestige-mcp/src/tools/source_sync.rs +++ /dev/null @@ -1,294 +0,0 @@ -//! `source_sync` MCP tool (#57) — index an external system into Vestige. -//! -//! Turns Vestige into a durable, offline, provenance-linked retrieval layer -//! over a long-lived external system. GitHub Issues and Redmine are the first -//! reference connectors: Vestige indexes issues, comments/journals, and source -//! metadata as source-aware memories you can search semantically and cite back -//! to the canonical issue URL — re-runnable idempotently (no duplicates) and -//! able to tombstone issues that vanish upstream. -//! -//! Unlike the official GitHub MCP server (a stateless live API proxy), this -//! keeps a local index: searchable offline, embedded for semantic recall, -//! joinable with the rest of your memory, and temporally versioned. -//! -//! ## Auth (security) -//! -//! Tokens are read from environment variables (`GITHUB_TOKEN` / -//! `VESTIGE_GITHUB_TOKEN`, `REDMINE_API_KEY` / `VESTIGE_REDMINE_API_KEY`) and -//! never from tool arguments, so credentials are not logged in the conversation. -//! Public GitHub repositories and anonymous Redmine instances can work without a -//! token/key at lower capability. - -use std::sync::Arc; - -use serde::Deserialize; -use serde_json::{Value, json}; - -use vestige_core::storage::Storage; - -/// JSON schema for the `source_sync` tool. -pub fn schema() -> Value { - json!({ - "type": "object", - "properties": { - "source": { - "type": "string", - "enum": ["github", "redmine"], - "description": "External system to sync: 'github' (GitHub Issues) or 'redmine' (a Redmine project).", - "default": "github" - }, - "repo": { - "type": "string", - "description": "GitHub only: repository as 'owner/name', e.g. 'samvallad33/vestige'." - }, - "project": { - "type": "string", - "description": "Redmine only: project identifier (slug or numeric id) to sync. The Redmine host comes from the REDMINE_URL env var." - }, - "reconcile": { - "type": "boolean", - "description": "Also tombstone local memories for issues no longer visible upstream (an extra full enumeration pass). Default false on incremental syncs.", - "default": false - }, - "max_pages": { - "type": "integer", - "description": "Max API pages to fetch this run (each page is up to 100 issues). Lets a first sync of a large project be resumed across calls. Default 10.", - "default": 10, - "minimum": 1, - "maximum": 1000 - } - }, - "required": [] - }) -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct SourceSyncArgs { - #[serde(default = "default_source")] - source: String, - #[serde(default)] - repo: Option, - #[serde(default)] - project: Option, - #[serde(default)] - reconcile: bool, - #[serde(default, alias = "max_pages")] - max_pages: Option, -} - -fn default_source() -> String { - "github".to_string() -} - -/// Read the GitHub token from the environment (never from tool args). -fn github_token() -> Option { - std::env::var("GITHUB_TOKEN") - .or_else(|_| std::env::var("VESTIGE_GITHUB_TOKEN")) - .ok() - .filter(|s| !s.trim().is_empty()) -} - -/// Read the Redmine API key from the environment (never from tool args). -fn redmine_api_key() -> Option { - std::env::var("REDMINE_API_KEY") - .or_else(|_| std::env::var("VESTIGE_REDMINE_API_KEY")) - .ok() - .filter(|s| !s.trim().is_empty()) -} - -/// Read the Redmine base URL from the environment. -fn redmine_url() -> Option { - std::env::var("REDMINE_URL") - .or_else(|_| std::env::var("VESTIGE_REDMINE_URL")) - .ok() - .filter(|s| !s.trim().is_empty()) -} - -pub async fn execute(storage: &Arc, args: Option) -> Result { - let args: SourceSyncArgs = match args { - Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {e}"))?, - None => return Err("Missing arguments".to_string()), - }; - - // Clamp to the schema's advertised maximum (1000). The JSON-schema `maximum` - // is advisory only — a client can still send a larger value — so enforce it - // here to bound the paginated fetch loop. - let max_pages = args.max_pages.unwrap_or(10).clamp(1, 1000); - - match args.source.as_str() { - "github" => { - let repo = args - .repo - .as_deref() - .ok_or_else(|| "github requires a 'repo' ('owner/name')".to_string())?; - let (owner, repo) = repo - .split_once('/') - .filter(|(o, r)| !o.is_empty() && !r.is_empty()) - .ok_or_else(|| { - "repo must be in 'owner/name' form, e.g. 'samvallad33/vestige'".to_string() - })?; - execute_github(storage, owner, repo, args.reconcile, max_pages).await - } - "redmine" => { - let project = args - .project - .as_deref() - .filter(|p| !p.trim().is_empty()) - .ok_or_else(|| "redmine requires a 'project' identifier".to_string())?; - let base_url = redmine_url().ok_or_else(|| { - "set the REDMINE_URL env var to the Redmine host (e.g. https://redmine.example.com)" - .to_string() - })?; - execute_redmine(storage, &base_url, project, args.reconcile, max_pages).await - } - other => Err(format!( - "Unsupported source '{other}'. Supported: 'github', 'redmine'." - )), - } -} - -/// Connectors are feature-gated; surface a clear message when the build omits -/// them rather than failing obscurely. -#[cfg(not(feature = "connectors"))] -async fn execute_github( - _storage: &Arc, - _owner: &str, - _repo: &str, - _reconcile: bool, - _max_pages: usize, -) -> Result { - Err(NO_CONNECTORS_MSG.to_string()) -} - -#[cfg(not(feature = "connectors"))] -async fn execute_redmine( - _storage: &Arc, - _base_url: &str, - _project: &str, - _reconcile: bool, - _max_pages: usize, -) -> Result { - Err(NO_CONNECTORS_MSG.to_string()) -} - -#[cfg(not(feature = "connectors"))] -const NO_CONNECTORS_MSG: &str = "This Vestige build was compiled without the 'connectors' feature. \ - Rebuild with --features connectors to enable source_sync."; - -#[cfg(feature = "connectors")] -async fn execute_github( - storage: &Arc, - owner: &str, - repo: &str, - reconcile: bool, - max_pages: usize, -) -> Result { - use vestige_core::connectors::github::{GithubConfig, GithubConnector}; - use vestige_core::connectors::run_sync; - - let config = GithubConfig::new(owner, repo).with_token(github_token()); - let connector = - GithubConnector::new(config).map_err(|e| format!("connector init failed: {e}"))?; - - let report = run_sync(storage.as_ref(), &connector, reconcile, max_pages) - .await - .map_err(|e| format!("sync failed: {e}"))?; - - let scope = format!("{owner}/{repo}"); - let total = report.created + report.updated + report.unchanged; - let authed = github_token().is_some(); - - let summary = format!( - "Synced {scope}: {} created, {} updated, {} unchanged{} ({total} records seen{}).", - report.created, - report.updated, - report.unchanged, - if report.reconciled { - format!(", {} tombstoned", report.tombstoned) - } else { - String::new() - }, - if authed { "" } else { ", unauthenticated" }, - ); - - Ok(json!({ - "ok": true, - "summary": summary, - "source": "github", - "scope": scope, - "created": report.created, - "updated": report.updated, - "unchanged": report.unchanged, - "tombstoned": report.tombstoned, - "reconciled": report.reconciled, - "cursor": report.new_cursor.map(|d| d.to_rfc3339()), - "authenticated": authed, - "warnings": report.warnings, - "hint": if total == 0 && !authed { - "No records returned. For private repos or higher rate limits, set GITHUB_TOKEN in the server environment." - } else if report.new_cursor.is_some() && total >= 100 { - "More may remain — run source_sync again to continue from the saved cursor." - } else { - "Search these with the normal search tools; results cite the GitHub issue URL." - } - })) -} - -#[cfg(feature = "connectors")] -async fn execute_redmine( - storage: &Arc, - base_url: &str, - project: &str, - reconcile: bool, - max_pages: usize, -) -> Result { - use vestige_core::connectors::redmine::{RedmineConfig, RedmineConnector}; - use vestige_core::connectors::run_sync; - - let config = RedmineConfig::new(base_url, project).with_api_key(redmine_api_key()); - let connector = - RedmineConnector::new(config).map_err(|e| format!("connector init failed: {e}"))?; - - let report = run_sync(storage.as_ref(), &connector, reconcile, max_pages) - .await - .map_err(|e| format!("sync failed: {e}"))?; - - let total = report.created + report.updated + report.unchanged; - let authed = redmine_api_key().is_some(); - - let summary = format!( - "Synced redmine project '{project}': {} created, {} updated, {} unchanged{} ({total} records seen{}).", - report.created, - report.updated, - report.unchanged, - if report.reconciled { - format!(", {} tombstoned", report.tombstoned) - } else { - String::new() - }, - if authed { "" } else { ", anonymous" }, - ); - - Ok(json!({ - "ok": true, - "summary": summary, - "source": "redmine", - "scope": project, - "created": report.created, - "updated": report.updated, - "unchanged": report.unchanged, - "tombstoned": report.tombstoned, - "reconciled": report.reconciled, - "cursor": report.new_cursor.map(|d| d.to_rfc3339()), - "authenticated": authed, - "warnings": report.warnings, - "hint": if total == 0 && !authed { - "No records returned. Set REDMINE_API_KEY (and confirm the REST API is enabled on the instance) for private projects." - } else if report.new_cursor.is_some() && total >= 100 { - "More may remain — run source_sync again to continue from the saved cursor." - } else { - "Search these with the normal search tools; results cite the Redmine issue URL." - } - })) -} diff --git a/crates/vestige-mcp/src/tools/suppress.rs b/crates/vestige-mcp/src/tools/suppress.rs index 342793e..f06debc 100644 --- a/crates/vestige-mcp/src/tools/suppress.rs +++ b/crates/vestige-mcp/src/tools/suppress.rs @@ -177,7 +177,6 @@ mod tests { tags: vec!["test".to_string()], valid_from: None, valid_until: None, - source_envelope: None, }) .unwrap() .id diff --git a/crates/vestige-mcp/src/tools/timeline.rs b/crates/vestige-mcp/src/tools/timeline.rs index 937ace7..14e58bf 100644 --- a/crates/vestige-mcp/src/tools/timeline.rs +++ b/crates/vestige-mcp/src/tools/timeline.rs @@ -9,9 +9,9 @@ use serde_json::Value; use std::collections::BTreeMap; use std::sync::Arc; -use vestige_core::{OutputConfig, Storage}; +use vestige_core::Storage; -use super::search_unified::{apply_output_masks, format_node}; +use super::search_unified::format_node; /// Input schema for memory_timeline tool pub fn schema() -> Value { @@ -87,11 +87,7 @@ fn parse_datetime(s: &str) -> Result, String> { } /// Execute memory_timeline tool -pub async fn execute( - storage: &Arc, - output_config: &OutputConfig, - 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 { @@ -104,13 +100,12 @@ pub async fn execute( }, }; - // Validate detail_level. Precedence: explicit MCP param > config > default. - let detail_level_owned = output_config.resolve_detail_level(args.detail_level.as_deref()); - let detail_level = match detail_level_owned.as_str() { - "brief" => "brief", - "full" => "full", - "summary" => "summary", - invalid => { + // Validate detail_level + let detail_level = match args.detail_level.as_deref() { + Some("brief") => "brief", + Some("full") => "full", + Some("summary") | None => "summary", + Some(invalid) => { return Err(format!( "Invalid detail_level '{}'. Must be 'brief', 'summary', or 'full'.", invalid @@ -129,8 +124,7 @@ pub async fn execute( None => Some(now), }; - // Precedence: explicit MCP param > config limit > built-in default (50). - let limit = output_config.resolve_limit(args.limit, 50).clamp(1, 200); + let limit = args.limit.unwrap_or(50).clamp(1, 200); // Query memories in time range with filters pushed into SQL. Rust-side // `retain` after `LIMIT` was unsafe for sparse types/tags — a dominant @@ -146,15 +140,14 @@ pub async fn execute( ) .map_err(|e| e.to_string())?; - // Group by day, applying the active profile's field masks (e.g. `lean` - // drops timestamps) to each formatted node. + // Group by day let mut by_day: BTreeMap> = BTreeMap::new(); for node in &results { let date = node.created_at.date_naive(); - let mut formatted = [format_node(node, detail_level)]; - apply_output_masks(&mut formatted, output_config); - let [formatted] = formatted; - by_day.entry(date).or_default().push(formatted); + by_day + .entry(date) + .or_default() + .push(format_node(node, detail_level)); } // Build timeline (newest first) @@ -180,7 +173,6 @@ pub async fn execute( "end": end.map(|dt| dt.to_rfc3339()), }, "detailLevel": detail_level, - "profile": output_config.profile.as_str(), "totalMemories": total, "days": days, "timeline": timeline, @@ -209,7 +201,6 @@ mod tests { tags: vec!["timeline-test".to_string()], valid_from: None, valid_until: None, - source_envelope: None, }) .unwrap(); } @@ -227,7 +218,6 @@ mod tests { tags: tags.iter().map(|t| t.to_string()).collect(), valid_from: None, valid_until: None, - source_envelope: None, }) .unwrap(); } @@ -272,7 +262,7 @@ mod tests { #[tokio::test] async fn test_timeline_no_args_defaults() { let (storage, _dir) = test_storage().await; - let result = execute(&storage, &OutputConfig::default(), None).await; + let result = execute(&storage, None).await; assert!(result.is_ok()); let value = result.unwrap(); assert_eq!(value["tool"], "memory_timeline"); @@ -284,7 +274,7 @@ mod tests { #[tokio::test] async fn test_timeline_empty_database() { let (storage, _dir) = test_storage().await; - let result = execute(&storage, &OutputConfig::default(), None).await; + let result = execute(&storage, None).await; let value = result.unwrap(); assert_eq!(value["totalMemories"], 0); assert_eq!(value["days"], 0); @@ -296,7 +286,7 @@ mod tests { let (storage, _dir) = test_storage().await; ingest_test_memory(&storage, "Timeline test memory 1").await; ingest_test_memory(&storage, "Timeline test memory 2").await; - let result = execute(&storage, &OutputConfig::default(), None).await; + let result = execute(&storage, None).await; let value = result.unwrap(); assert_eq!(value["totalMemories"], 2); assert!(value["days"].as_u64().unwrap() >= 1); @@ -306,7 +296,7 @@ mod tests { async fn test_timeline_invalid_detail_level() { let (storage, _dir) = test_storage().await; let args = serde_json::json!({ "detail_level": "invalid" }); - let result = execute(&storage, &OutputConfig::default(), Some(args)).await; + let result = execute(&storage, Some(args)).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("Invalid detail_level")); } @@ -316,7 +306,7 @@ mod tests { let (storage, _dir) = test_storage().await; ingest_test_memory(&storage, "Brief test memory").await; let args = serde_json::json!({ "detail_level": "brief" }); - let result = execute(&storage, &OutputConfig::default(), Some(args)).await; + let result = execute(&storage, Some(args)).await; assert!(result.is_ok()); let value = result.unwrap(); assert_eq!(value["detailLevel"], "brief"); @@ -327,7 +317,7 @@ mod tests { let (storage, _dir) = test_storage().await; ingest_test_memory(&storage, "Full test memory").await; let args = serde_json::json!({ "detail_level": "full" }); - let result = execute(&storage, &OutputConfig::default(), Some(args)).await; + let result = execute(&storage, Some(args)).await; assert!(result.is_ok()); let value = result.unwrap(); assert_eq!(value["detailLevel"], "full"); @@ -337,7 +327,7 @@ mod tests { async fn test_timeline_limit_clamped() { let (storage, _dir) = test_storage().await; let args = serde_json::json!({ "limit": 0 }); - let result = execute(&storage, &OutputConfig::default(), Some(args)).await; + let result = execute(&storage, Some(args)).await; assert!(result.is_ok()); // limit clamped to 1, no error } @@ -349,7 +339,7 @@ mod tests { "start": "2020-01-01", "end": "2030-12-31" }); - let result = execute(&storage, &OutputConfig::default(), Some(args)).await; + let result = execute(&storage, Some(args)).await; assert!(result.is_ok()); let value = result.unwrap(); assert!(value["totalMemories"].as_u64().unwrap() >= 1); @@ -360,7 +350,7 @@ mod tests { let (storage, _dir) = test_storage().await; ingest_test_memory(&storage, "A fact memory").await; let args = serde_json::json!({ "node_type": "concept" }); - let result = execute(&storage, &OutputConfig::default(), Some(args)).await; + let result = execute(&storage, Some(args)).await; let value = result.unwrap(); // Ingested as "fact", filtering for "concept" should yield 0 assert_eq!(value["totalMemories"], 0); @@ -371,7 +361,7 @@ mod tests { let (storage, _dir) = test_storage().await; ingest_test_memory(&storage, "Tagged memory").await; let args = serde_json::json!({ "tags": ["timeline-test"] }); - let result = execute(&storage, &OutputConfig::default(), Some(args)).await; + let result = execute(&storage, Some(args)).await; let value = result.unwrap(); assert!(value["totalMemories"].as_u64().unwrap() >= 1); } @@ -381,7 +371,7 @@ mod tests { let (storage, _dir) = test_storage().await; ingest_test_memory(&storage, "Tagged memory").await; let args = serde_json::json!({ "tags": ["nonexistent-tag"] }); - let result = execute(&storage, &OutputConfig::default(), Some(args)).await; + let result = execute(&storage, Some(args)).await; let value = result.unwrap(); assert_eq!(value["totalMemories"], 0); } @@ -419,9 +409,7 @@ mod tests { // 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, &OutputConfig::default(), Some(args)) - .await - .unwrap(); + 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" @@ -459,9 +447,7 @@ mod tests { } let args = serde_json::json!({ "tags": ["rare"], "limit": 5 }); - let value = execute(&storage, &OutputConfig::default(), Some(args)) - .await - .unwrap(); + 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" @@ -493,23 +479,4 @@ mod tests { assert_eq!(nodes.len(), 1, "Only the exact-tag match should return"); assert_eq!(nodes[0].content, "Exact tag hit"); } - - /// Phase 2: config-file detail_level applies when no explicit param is set, - /// and an explicit param overrides it. - #[tokio::test] - async fn test_timeline_config_detail_precedence() { - let (storage, _dir) = test_storage().await; - ingest_test_memory(&storage, "Timeline config precedence content.").await; - - let cfg = vestige_core::VestigeConfig::parse("[defaults]\ndetail_level=\"full\"").output(); - - // No explicit param -> config wins. - let value = execute(&storage, &cfg, None).await.unwrap(); - assert_eq!(value["detailLevel"], "full"); - - // Explicit param -> overrides config. - let args = serde_json::json!({ "detail_level": "brief" }); - let value = execute(&storage, &cfg, Some(args)).await.unwrap(); - assert_eq!(value["detailLevel"], "brief"); - } } diff --git a/crates/vestige-mcp/src/trace_recorder.rs b/crates/vestige-mcp/src/trace_recorder.rs deleted file mode 100644 index 0f7fec0..0000000 --- a/crates/vestige-mcp/src/trace_recorder.rs +++ /dev/null @@ -1,1369 +0,0 @@ -//! # Trace Recorder — the live black-box wiring -//! -//! Bridges an MCP `tools/call` to the persisted black box. For each call the -//! recorder: -//! -//! 1. derives a stable `runId` (client-supplied `runId`/`run_id` arg if present, -//! else a fresh `run_` UUID), -//! 2. records an `mcp.call` event with a **hash** of the args (never the raw -//! args, so traces can't leak prompt contents or secrets), -//! 3. after the tool returns, inspects the result JSON and records the -//! downstream events the agent experienced — `memory.retrieve` (with -//! per-id activation), `memory.suppress` (with reason), `sanhedrin.veto`, -//! `dream.patch`, -//! 4. persists every event to `agent_traces` and broadcasts it over the -//! dashboard event channel so the Black Box tab updates live. -//! -//! The recorder is best-effort: a persistence error never fails the tool call. - -use std::collections::BTreeMap; -use std::sync::Arc; - -use chrono::Utc; -use serde_json::Value; -use tokio::sync::broadcast; - -use crate::dashboard::events::VestigeEvent; -use vestige_core::{ - MemoryTraceEvent, Receipt, Storage, SuppressReason, SuppressedReceiptEntry, WriteSource, -}; - -/// Tools that write to memory and are therefore subject to risk-gated review. -/// -/// Includes `codebase` (its `remember_pattern` / `remember_decision` actions -/// write durable architectural-decision memories) so those brain mutations are -/// traced and gated like any other write (B2). Read-only actions on these tools -/// are filtered out downstream by [`is_write_decision`]. -fn is_write_tool(tool: &str) -> bool { - matches!( - tool, - "smart_ingest" | "ingest" | "session_checkpoint" | "memory" | "codebase" - ) -} - -/// Whether a tool's `decision`/`action` label denotes an actual memory write -/// (vs. a read like `get`/`state`). Used to keep reads out of the write trace. -fn is_write_decision(label: &str) -> bool { - matches!( - label, - "create" - | "created" - | "update" - | "updated" - | "supersede" - | "superseded" - | "reinforce" - | "reinforced" - | "merge" - | "merged" - | "replace" - | "replaced" - | "add_context" - | "edit" - | "edited" - | "promote" - | "promoted" - | "demote" - | "demoted" - | "remember_pattern" - | "remember_decision" - | "remembered" - // C2: destructive removals are brain mutations too — they must - // trace as memory.write and be gateable, not bypass review. - | "purge" - | "purged" - | "delete" - | "deleted" - | "forget" - | "forgotten" - ) -} - -/// Risk-gate the writes in a tool result. For each write the tool just made, -/// build a [`vestige_core::WriteContext`], classify it under the active -/// [`vestige_core::ReviewMode`], and — if risky — quarantine the just-written -/// node (suppress it so it is not used for retrieval until reviewed) and open a -/// [`vestige_core::MemoryPr`]. Normal writes are left untouched: they auto-land, -/// and they already got a receipt. -/// -/// Returns the list of opened-PR summaries (id, kind, title, signals) so the -/// caller can annotate the tool response and emit `MemoryPrOpened` events. -pub fn gate_writes( - storage: &Arc, - event_tx: Option<&broadcast::Sender>, - run_id: &str, - tool: &str, - result: &serde_json::Value, - mode: vestige_core::ReviewMode, -) -> Vec { - use vestige_core::{ - MemoryPr, MemoryPrKind, MemoryPrStatus, RiskClass, WriteContext, classify_write, - }; - - if !is_write_tool(tool) { - return Vec::new(); - } - - let mut opened = Vec::new(); - - // Collect each (id, decision) write the tool reported. - let writes = extract_writes(result); - for (id, decision) in writes { - let destructive = is_destructive_decision(&decision); - - // Pull the just-written node to inspect its real content/type/tags. - // C2: a destructive write (purge/delete/forget) has ALREADY removed the - // row, so get_node returns None — we must NOT skip it (that's how - // destructive removals were bypassing review). For those, build the - // context from the decision alone; for normal writes a missing node - // genuinely means nothing to gate, so skip. - let node = match storage.get_node(&id) { - Ok(Some(n)) => Some(n), - _ if destructive => None, - _ => continue, - }; - - let (supersedes, merges) = match decision.as_str() { - "supersede" | "replace" | "superseded" => (true, false), - "merge" | "merged" => (false, true), - _ => (false, false), - }; - // If this superseded something, treat the contradiction as against a - // high-trust memory when the *new* node's own retention is high (the - // pipeline only supersedes when confident). This keeps the gate honest - // without a second DB round-trip per write. - let contradicts_trust = match (&node, supersedes) { - (Some(n), true) => Some(n.retention_strength.max(0.7)), - _ => None, - }; - - let ctx = WriteContext { - source: Some(WriteSource::Agent), - node_type: node - .as_ref() - .map(|n| n.node_type.clone()) - .unwrap_or_default(), - content: node.as_ref().map(|n| n.content.clone()).unwrap_or_default(), - tags: node.as_ref().map(|n| n.tags.clone()).unwrap_or_default(), - contradicts_trust, - supersedes, - merges, - forgets: destructive, - ..Default::default() - }; - - // A destructive write ALWAYS warrants review (erasing brain state) even - // in Fast mode is debatable, but we respect the mode: the `forgets` - // signal in WriteContext makes classify_write gate it in Risk-Gated. - let (class, signals) = classify_write(&ctx, mode); - if class != RiskClass::Review { - continue; - } - - // Quarantine the just-written node so it's held out of retrieval until - // the PR is decided. For a destructive write there's no live node to - // suppress — the PR records the action for review/audit instead. - if node.is_some() { - let _ = storage.suppress_memory(&id); - } - - let kind = match decision.as_str() { - "supersede" | "replace" | "superseded" => MemoryPrKind::MemorySuperseded, - "merge" | "merged" => MemoryPrKind::DreamConsolidation, - _ if destructive => MemoryPrKind::NodeDecayed, - _ if contradicts_trust.is_some() => MemoryPrKind::ContradictionDetected, - _ => MemoryPrKind::NewFact, - }; - - // PRIV: never copy full memory content into the PR (it can hold a - // secret, and the PR row is read by the dashboard and may be exported). - // Store a short, redacted preview + a content hash instead. The preview - // is dropped entirely when the write was gated for a sensitive topic. - let sensitive = signals - .iter() - .any(|s| s.code == "sensitive_topic" || s.code == "sensitive_node_type"); - let raw_content = node.as_ref().map(|n| n.content.as_str()).unwrap_or(""); - let preview = content_preview(raw_content, sensitive); - let content_hash = hash_content(raw_content); - - let title = format!("{}: {}", pr_kind_phrase(kind), preview); - let pr = MemoryPr { - id: format!("pr_{}", uuid::Uuid::new_v4().simple()), - kind, - status: MemoryPrStatus::Pending, - title: title.clone(), - diff: serde_json::json!({ - "decision": decision, - "node": { - "id": id, - "nodeType": node.as_ref().map(|n| n.node_type.clone()).unwrap_or_default(), - // Redacted: preview (or "[redacted — sensitive]") + hash, - // never the full content. - "contentPreview": preview, - "contentHash": content_hash, - "tags": node.as_ref().map(|n| n.tags.clone()).unwrap_or_default(), - "deleted": node.is_none(), - }, - }), - signals: signals.clone(), - subject_id: Some(id.clone()), - run_id: Some(run_id.to_string()), - created_at: Utc::now().to_rfc3339(), - decided_at: None, - decision: None, - }; - - if let Err(e) = storage.save_memory_pr(&pr) { - tracing::warn!("memory PR save failed: {e}"); - continue; - } - - if let Some(tx) = event_tx { - let _ = tx.send(VestigeEvent::MemoryPrOpened { - id: pr.id.clone(), - kind: kind.as_str().to_string(), - title, - signal_count: signals.len(), - run_id: Some(run_id.to_string()), - timestamp: Utc::now(), - }); - } - - opened.push(serde_json::json!({ - "id": pr.id, - "kind": kind.as_str(), - "title": pr.title, - "signals": signals, - "subjectId": id, - })); - } - - opened -} - -struct PendingMemoryMutation { - action: String, - id: String, - reason: Option, -} - -/// Pre-gate memory mutations that would otherwise be irreversible or directly -/// inhibitory before the reviewer sees them. -/// -/// Normal risky writes are still handled post-commit by [`gate_writes`]. Purge, -/// delete, and suppress are different: executing the tool first either removes -/// the row or mutates retrieval influence before review. Under Risk-Gated and -/// Paranoid modes this function opens a pending Memory PR and returns a tool -/// response without performing the mutation. Fast mode keeps the historical -/// direct-execution behavior. -pub fn gate_pending_memory_mutation( - storage: &Arc, - event_tx: Option<&broadcast::Sender>, - run_id: &str, - tool: &str, - args: &Option, - mode: vestige_core::ReviewMode, -) -> Result, String> { - use vestige_core::{ - MemoryPr, MemoryPrKind, MemoryPrStatus, RiskClass, WriteContext, classify_write, - }; - - if matches!(mode, vestige_core::ReviewMode::Fast) { - return Ok(None); - } - - let Some(pending) = pending_memory_mutation(tool, args) else { - return Ok(None); - }; - let node = match storage.get_node(&pending.id) { - Ok(Some(node)) => node, - Ok(None) => return Ok(None), - Err(e) => return Err(format!("failed to inspect memory before review gate: {e}")), - }; - - let ctx = WriteContext { - source: Some(WriteSource::Agent), - node_type: node.node_type.clone(), - content: node.content.clone(), - tags: node.tags.clone(), - forgets: true, - ..Default::default() - }; - let (class, signals) = classify_write(&ctx, mode); - if class != RiskClass::Review { - return Ok(None); - } - - let sensitive = signals - .iter() - .any(|s| s.code == "sensitive_topic" || s.code == "sensitive_node_type"); - let preview = content_preview(&node.content, sensitive); - let content_hash = hash_content(&node.content); - let kind = MemoryPrKind::NodeDecayed; - let title = format!("{}: {}", pr_kind_phrase(kind), preview); - let pr = MemoryPr { - id: format!("pr_{}", uuid::Uuid::new_v4().simple()), - kind, - status: MemoryPrStatus::Pending, - title: title.clone(), - diff: serde_json::json!({ - "decision": pending.action, - "pendingAction": pending.action, - "requiresApproval": true, - "reason": pending.reason, - "node": { - "id": pending.id, - "nodeType": node.node_type, - "contentPreview": preview, - "contentHash": content_hash, - "tags": node.tags, - "deleted": false, - }, - }), - signals: signals.clone(), - subject_id: Some(pending.id.clone()), - run_id: Some(run_id.to_string()), - created_at: Utc::now().to_rfc3339(), - decided_at: None, - decision: None, - }; - - if let Err(e) = storage.save_memory_pr(&pr) { - tracing::warn!("pending destructive Memory PR save failed: {e}"); - return Err(format!( - "review gate failed closed: could not open Memory PR for pending mutation: {e}" - )); - } - - if let Some(tx) = event_tx { - let _ = tx.send(VestigeEvent::MemoryPrOpened { - id: pr.id.clone(), - kind: kind.as_str().to_string(), - title: title.clone(), - signal_count: signals.len(), - run_id: Some(run_id.to_string()), - timestamp: Utc::now(), - }); - } - - let opened = serde_json::json!({ - "id": pr.id, - "kind": kind.as_str(), - "title": pr.title, - "signals": signals, - "subjectId": pending.id, - }); - - Ok(Some(serde_json::json!({ - "action": format!("{}_pending_review", pending.action), - "success": false, - "pendingReview": true, - "nodeId": pending.id, - "message": "Mutation was not executed. Vestige opened a Memory PR and is waiting for review.", - "memoryPrsOpened": [opened], - "memoryPrNotice": "Vestige opened a Memory PR before applying this destructive or suppressive memory mutation. Approve with `forget`; keep the memory with `promote`; hold it suppressed with `quarantine`.", - }))) -} - -fn pending_memory_mutation( - tool: &str, - args: &Option, -) -> Option { - let args = args.as_ref()?; - match tool { - "memory" => { - let action = args.get("action")?.as_str()?.to_ascii_lowercase(); - if !matches!(action.as_str(), "purge" | "delete") { - return None; - } - if !args - .get("confirm") - .and_then(|v| v.as_bool()) - .unwrap_or(false) - { - return None; - } - Some(PendingMemoryMutation { - action, - id: args.get("id")?.as_str()?.to_string(), - reason: args - .get("reason") - .and_then(|v| v.as_str()) - .map(str::to_string), - }) - } - "delete_knowledge" => { - if !args - .get("confirm") - .and_then(|v| v.as_bool()) - .unwrap_or(false) - { - return None; - } - Some(PendingMemoryMutation { - action: "delete".to_string(), - id: args.get("id")?.as_str()?.to_string(), - reason: args - .get("reason") - .and_then(|v| v.as_str()) - .map(str::to_string), - }) - } - "suppress" => { - if args - .get("reverse") - .and_then(|v| v.as_bool()) - .unwrap_or(false) - { - return None; - } - Some(PendingMemoryMutation { - action: "suppress".to_string(), - id: args.get("id")?.as_str()?.to_string(), - reason: args - .get("reason") - .or_else(|| args.get("note")) - .and_then(|v| v.as_str()) - .map(str::to_string), - }) - } - _ => None, - } -} - -/// Whether a write decision permanently removes / forgets memory (so the live -/// row may already be gone when the gate runs). -fn is_destructive_decision(label: &str) -> bool { - matches!( - label, - "purge" | "purged" | "delete" | "deleted" | "forget" | "forgotten" - ) -} - -/// A short, privacy-preserving preview of memory content for a Memory PR. -/// When the write was flagged for a sensitive topic, the content is redacted -/// entirely — the reviewer sees the risk signals + hash, never the secret. -fn content_preview(content: &str, sensitive: bool) -> String { - if content.is_empty() { - return "(no content)".to_string(); - } - if sensitive { - return "[redacted — sensitive content; review via risk signals]".to_string(); - } - let trimmed: String = content.chars().take(80).collect(); - if content.chars().count() > 80 { - format!("{trimmed}…") - } else { - trimmed - } -} - -/// FNV-1a hex fingerprint of memory content — lets a reviewer correlate / -/// dedupe without the PR row carrying the raw (possibly secret) text. -fn hash_content(content: &str) -> String { - const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325; - const FNV_PRIME: u64 = 0x0000_0100_0000_01b3; - let mut hash = FNV_OFFSET; - for b in content.as_bytes() { - hash ^= u64::from(*b); - hash = hash.wrapping_mul(FNV_PRIME); - } - format!("{:016x}", hash) -} - -fn pr_kind_phrase(kind: vestige_core::MemoryPrKind) -> &'static str { - use vestige_core::MemoryPrKind::*; - match kind { - NewFact => "New fact pending review", - StrengthenedFact => "Strengthened fact", - ContradictionDetected => "Contradiction with existing memory", - MemorySuperseded => "Supersede existing memory", - EdgeAdded => "New edge", - NodeDecayed => "Decayed node", - DreamConsolidation => "Consolidation proposal", - } -} - -/// Tools whose output warrants a retrieval receipt. -fn is_retrieval_tool(tool: &str) -> bool { - matches!( - tool, - "deep_reference" | "cross_reference" | "search" | "explore_connections" - ) -} - -/// Build a [`Receipt`] from a retrieval tool's response JSON, persist it, and -/// return it as JSON ready to attach to that response. Reuses exactly the data -/// the tool already computed (retrieved ids + trust, suppressed ids + reason, -/// the activation path) — so the receipt is the auditable "nutrition label" for -/// the answer and costs nothing extra to produce. -/// -/// Returns `None` for non-retrieval tools or empty results. Best-effort -/// persistence: a storage error is logged, the receipt is still returned. -pub fn build_and_save_receipt( - storage: &Arc, - run_id: &str, - tool: &str, - result: &serde_json::Value, -) -> Option { - if !is_retrieval_tool(tool) { - return None; - } - - let (retrieved, activation) = extract_retrieved(result); - if retrieved.is_empty() { - return None; - } - let trust_scores: Vec = retrieved - .iter() - .map(|id| activation.get(id).copied().unwrap_or(0.0)) - .collect(); - - let suppressed: Vec = extract_suppressed(result) - .into_iter() - .map(|(id, reason)| SuppressedReceiptEntry::new(id, reason)) - .collect(); - - // The activation path: the run's reasoning chain if present, else a simple - // best-first chain of the retrieved ids. - let activation_path = result - .get("reasoning") - .and_then(|v| v.as_str()) - .map(|s| vec![s.to_string()]) - .unwrap_or_else(|| { - if retrieved.len() > 1 { - vec![retrieved.join(" -> ")] - } else { - Vec::new() - } - }); - - let query = result.get("query").and_then(|v| v.as_str()); - - let receipt = Receipt::build( - Utc::now(), - run_id, - retrieved, - suppressed, - activation_path, - &trust_scores, - Vec::new(), - ); - if let Err(e) = storage.save_receipt(&receipt, Some(run_id), Some(tool), query) { - tracing::warn!("receipt save failed: {e}"); - } - Some(serde_json::to_value(&receipt).unwrap_or(serde_json::Value::Null)) -} - -/// Derive the run id for a tool call. Honours a client-supplied `runId` / -/// `run_id` argument (so an agent can correlate a whole session's calls); -/// otherwise mints a fresh one. -pub fn run_id_for(args: &Option) -> String { - if let Some(a) = args { - for key in ["runId", "run_id"] { - if let Some(s) = a.get(key).and_then(|v| v.as_str()) - && !s.is_empty() - { - return s.to_string(); - } - } - } - format!("run_{}", uuid::Uuid::new_v4().simple()) -} - -/// A 64-bit FNV-1a hex fingerprint of the tool arguments — the -/// privacy-preserving stand-in stored on `mcp.call` events. We only need a -/// stable, collision-resistant-enough identifier for "same args → same hash" -/// in the trace, not a cryptographic digest, so a dependency-free FNV-1a keeps -/// the crate lean. -pub fn hash_args(args: &Option) -> String { - let bytes = match args { - Some(v) => serde_json::to_vec(v).unwrap_or_default(), - None => Vec::new(), - }; - const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325; - const FNV_PRIME: u64 = 0x0000_0100_0000_01b3; - let mut hash = FNV_OFFSET; - for b in &bytes { - hash ^= u64::from(*b); - hash = hash.wrapping_mul(FNV_PRIME); - } - format!("{:016x}", hash) -} - -/// Persist one trace event and broadcast it to the dashboard. Best-effort: -/// storage failures are logged, never propagated. -pub fn record( - storage: &Arc, - event_tx: Option<&broadcast::Sender>, - event: MemoryTraceEvent, -) { - let event = event.with_at(Utc::now().timestamp_millis()); - let seq = match storage.append_trace_event(&event) { - Ok(seq) => seq, - Err(e) => { - tracing::warn!("trace append failed: {e}"); - return; - } - }; - if let Some(tx) = event_tx { - let _ = tx.send(VestigeEvent::TraceEvent { - run_id: event.run_id().to_string(), - seq, - event, - timestamp: Utc::now(), - }); - } -} - -/// Record the opening `mcp.call` event for a tool invocation. -pub fn record_call( - storage: &Arc, - event_tx: Option<&broadcast::Sender>, - run_id: &str, - tool: &str, - args: &Option, -) { - record( - storage, - event_tx, - MemoryTraceEvent::McpCall { - run_id: run_id.to_string(), - tool: tool.to_string(), - args_hash: hash_args(args), - at: 0, - }, - ); -} - -/// Inspect a successful tool result and record the downstream memory events the -/// agent experienced (retrieve / suppress / veto / dream). Tool-output shapes -/// are matched leniently so this stays robust as tools evolve. -pub fn record_result( - storage: &Arc, - event_tx: Option<&broadcast::Sender>, - run_id: &str, - tool: &str, - result: &Value, -) { - // --- memory.retrieve: ids + per-id activation --- - let (ids, activation) = extract_retrieved(result); - if !ids.is_empty() { - record( - storage, - event_tx, - MemoryTraceEvent::MemoryRetrieve { - run_id: run_id.to_string(), - ids, - activation, - at: 0, - }, - ); - } - - // --- memory.suppress: each suppressed id + reason --- - for (id, reason) in extract_suppressed(result) { - record( - storage, - event_tx, - MemoryTraceEvent::MemorySuppress { - run_id: run_id.to_string(), - id, - reason, - at: 0, - }, - ); - } - - // --- memory.write: writes performed by ingest-like tools --- - for (id, decision) in extract_writes(result) { - record( - storage, - event_tx, - MemoryTraceEvent::MemoryWrite { - run_id: run_id.to_string(), - id, - diff: serde_json::json!({ "decision": decision }), - source: WriteSource::Agent, - at: 0, - }, - ); - } - - // --- contradiction.detected: each contradiction pair the agent faced --- - for (ids, winner_id, detail) in extract_contradictions(result) { - record( - storage, - event_tx, - MemoryTraceEvent::ContradictionDetected { - run_id: run_id.to_string(), - ids, - winner_id, - detail, - at: 0, - }, - ); - } - - // --- sanhedrin.veto: a blocked claim --- - if let Some((claim, evidence_ids, confidence)) = extract_veto(result) { - record( - storage, - event_tx, - MemoryTraceEvent::SanhedrinVeto { - run_id: run_id.to_string(), - claim, - evidence_ids, - confidence, - at: 0, - }, - ); - } - - // --- dream.patch: consolidation proposals --- - let proposal_ids = extract_dream_proposals(result, tool); - if !proposal_ids.is_empty() { - record( - storage, - event_tx, - MemoryTraceEvent::DreamPatch { - run_id: run_id.to_string(), - proposal_ids, - at: 0, - }, - ); - } -} - -/// Pull retrieved memory ids + their activation/score from a search-like or -/// deep_reference-like result. -fn extract_retrieved(result: &Value) -> (Vec, BTreeMap) { - let mut ids = Vec::new(); - let mut activation = BTreeMap::new(); - - // search_unified: { results: [{ id, score|activation, ... }] } - if let Some(arr) = result.get("results").and_then(|r| r.as_array()) { - for item in arr { - if let Some(id) = item.get("id").and_then(|v| v.as_str()) { - ids.push(id.to_string()); - let act = item - .get("activation") - .or_else(|| item.get("score")) - .and_then(|v| v.as_f64()); - if let Some(a) = act { - activation.insert(id.to_string(), a); - } - } - } - } - - // deep_reference: { evidence: [{ id, trust, ... }], recommended: { memory_id } } - if ids.is_empty() - && let Some(arr) = result.get("evidence").and_then(|r| r.as_array()) - { - for item in arr { - if let Some(id) = item.get("id").and_then(|v| v.as_str()) { - ids.push(id.to_string()); - if let Some(t) = item.get("trust").and_then(|v| v.as_f64()) { - activation.insert(id.to_string(), t); - } - } - } - } - - (ids, activation) -} - -/// Pull suppressed entries from a result. Recognises both the deep_reference -/// `superseded`/`contradictions` shapes and the explicit receipt `suppressed` -/// list `[{ id, reason }]`. -fn extract_suppressed(result: &Value) -> Vec<(String, SuppressReason)> { - let mut out = Vec::new(); - - if let Some(arr) = result - .get("receipt") - .and_then(|r| r.get("suppressed")) - .and_then(|s| s.as_array()) - { - for item in arr { - if let Some(id) = item.get("id").and_then(|v| v.as_str()) { - let reason = item - .get("reason") - .and_then(|v| v.as_str()) - .map(parse_suppress_reason) - .unwrap_or(SuppressReason::LowTrust); - out.push((id.to_string(), reason)); - } - } - } - - // deep_reference surfaces superseded ids directly. - if let Some(arr) = result.get("superseded").and_then(|s| s.as_array()) { - for item in arr { - let id = item - .get("id") - .and_then(|v| v.as_str()) - .or_else(|| item.as_str()); - if let Some(id) = id { - out.push((id.to_string(), SuppressReason::Contradicted)); - } - } - } - - out -} - -fn parse_suppress_reason(s: &str) -> SuppressReason { - match s { - "low_trust" => SuppressReason::LowTrust, - "decayed" => SuppressReason::Decayed, - "contradicted" => SuppressReason::Contradicted, - "privacy" => SuppressReason::Privacy, - "competition" => SuppressReason::Competition, - _ => SuppressReason::LowTrust, - } -} - -/// Pull writes from an ingest-like result (single `decision`+`nodeId` or a -/// `results` batch). -fn extract_writes(result: &Value) -> Vec<(String, String)> { - let mut out = Vec::new(); - let push = |out: &mut Vec<(String, String)>, item: &Value| { - // B2: accept either `decision` (smart_ingest) or `action` - // (memory promote/demote/edit, codebase remember_*). Read-only labels - // (get/state/...) are filtered out so reads never trace as writes. - let label = item - .get("decision") - .or_else(|| item.get("action")) - .and_then(|v| v.as_str()); - let id = item - .get("nodeId") - .or_else(|| item.get("id")) - .and_then(|v| v.as_str()); - if let (Some(label), Some(id)) = (label, id) - && is_write_decision(label) - { - out.push((id.to_string(), label.to_string())); - } - }; - push(&mut out, result); - if let Some(arr) = result.get("results").and_then(|r| r.as_array()) { - for item in arr { - push(&mut out, item); - } - } - out -} - -/// Pull contradiction pairs from a deep_reference result. Each entry is -/// `{ stronger: {id, ...}, weaker: {id, ...}, topic_overlap }`; the `stronger` -/// memory is the winner the agent trusted. -fn extract_contradictions(result: &Value) -> Vec<(Vec, Option, String)> { - let mut out = Vec::new(); - let Some(arr) = result.get("contradictions").and_then(|c| c.as_array()) else { - return out; - }; - for item in arr { - let stronger = item - .get("stronger") - .and_then(|s| s.get("id")) - .and_then(|v| v.as_str()); - let weaker = item - .get("weaker") - .and_then(|s| s.get("id")) - .and_then(|v| v.as_str()); - let (Some(s), Some(w)) = (stronger, weaker) else { - continue; - }; - let detail = format!( - "Contradiction: kept {s} over {w}{}", - item.get("topic_overlap") - .and_then(|v| v.as_f64()) - .map(|o| format!(" (topic overlap {:.0}%)", o * 100.0)) - .unwrap_or_default() - ); - out.push(( - vec![s.to_string(), w.to_string()], - Some(s.to_string()), - detail, - )); - } - out -} - -/// Pull a Sanhedrin-style veto, if the result carries one. -fn extract_veto(result: &Value) -> Option<(String, Vec, f64)> { - let veto = result.get("veto").or_else(|| result.get("sanhedrin"))?; - let claim = veto - .get("claim") - .and_then(|v| v.as_str()) - .unwrap_or("") - .to_string(); - if claim.is_empty() { - return None; - } - let evidence_ids = veto - .get("evidenceIds") - .or_else(|| veto.get("evidence_ids")) - .and_then(|v| v.as_array()) - .map(|arr| { - arr.iter() - .filter_map(|v| v.as_str().map(String::from)) - .collect() - }) - .unwrap_or_default(); - let confidence = veto - .get("confidence") - .and_then(|v| v.as_f64()) - .unwrap_or(0.0); - Some((claim, evidence_ids, confidence)) -} - -/// Pull dream consolidation proposal ids from a dream/consolidate tool result. -/// -/// Proposals are identified by an explicit `id` / proposal id when present. -/// The `dream` tool emits an `insights` array whose items carry no id (they are -/// `{insight_type, insight, source_memories, confidence, …}`), so we derive a -/// stable proposal id from each insight's real content — its type plus the -/// memories it consolidated. The dream genuinely ran; this just gives each real -/// proposal a deterministic handle for the trace. -fn extract_dream_proposals(result: &Value, tool: &str) -> Vec { - if tool != "dream" && tool != "consolidate" { - return Vec::new(); - } - let mut out = Vec::new(); - - // Explicit id arrays first (consolidate / future producers). - for key in ["proposalIds", "proposals", "connections"] { - if let Some(arr) = result.get(key).and_then(|v| v.as_array()) { - for item in arr { - if let Some(id) = item - .get("id") - .and_then(|v| v.as_str()) - .or_else(|| item.as_str()) - { - out.push(id.to_string()); - } - } - } - } - - // Dream insights: derive a stable id from real content. - if let Some(arr) = result.get("insights").and_then(|v| v.as_array()) { - for (i, item) in arr.iter().enumerate() { - if let Some(id) = item.get("id").and_then(|v| v.as_str()) { - out.push(id.to_string()); - continue; - } - let kind = item - .get("insight_type") - .and_then(|v| v.as_str()) - .unwrap_or("insight"); - // Prefer the consolidated source memories for a meaningful handle; - // fall back to the index so every real insight is still counted. - let src = item - .get("source_memories") - .and_then(|v| v.as_array()) - .map(|a| { - a.iter() - .filter_map(|m| m.as_str()) - // char-boundary-safe: byte-slicing &s[..8] panics when a - // multi-byte UTF-8 char straddles byte 8. - .map(|s| s.chars().take(8).collect::()) - .collect::>() - .join("+") - }) - .filter(|s| !s.is_empty()) - .unwrap_or_else(|| format!("idx{i}")); - out.push(format!("dream:{kind}:{src}")); - } - } - - out -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn run_id_honours_client_supplied() { - let args = Some(serde_json::json!({ "runId": "run_session_7" })); - assert_eq!(run_id_for(&args), "run_session_7"); - } - - #[test] - fn run_id_mints_when_absent() { - let id = run_id_for(&None); - assert!(id.starts_with("run_")); - assert!(id.len() > 10); - } - - #[test] - fn hash_is_stable_and_hides_content() { - let args = Some(serde_json::json!({ "query": "my secret prompt" })); - let h1 = hash_args(&args); - let h2 = hash_args(&args); - assert_eq!(h1, h2); - assert!(!h1.contains("secret")); - assert_eq!(h1.len(), 16); - } - - #[test] - fn extract_retrieved_from_search_shape() { - let r = serde_json::json!({ - "results": [ - { "id": "m1", "score": 0.9 }, - { "id": "m2", "activation": 0.4 } - ] - }); - let (ids, act) = extract_retrieved(&r); - assert_eq!(ids, vec!["m1", "m2"]); - assert_eq!(act["m1"], 0.9); - assert_eq!(act["m2"], 0.4); - } - - #[test] - fn extract_retrieved_from_deep_reference_shape() { - let r = serde_json::json!({ - "evidence": [ { "id": "e1", "trust": 0.7 } ] - }); - let (ids, act) = extract_retrieved(&r); - assert_eq!(ids, vec!["e1"]); - assert_eq!(act["e1"], 0.7); - } - - #[test] - fn extract_suppressed_from_receipt_and_superseded() { - let r = serde_json::json!({ - "receipt": { "suppressed": [ { "id": "s1", "reason": "contradicted" } ] }, - "superseded": [ { "id": "s2" } ] - }); - let out = extract_suppressed(&r); - assert!(out.contains(&("s1".to_string(), SuppressReason::Contradicted))); - assert!(out.contains(&("s2".to_string(), SuppressReason::Contradicted))); - } - - #[test] - fn extract_dream_proposals_from_real_insights_shape() { - // The exact shape the `dream` tool emits — insights without an id. - let r = serde_json::json!({ - "status": "dreamed", - "insights": [ - { - "insight_type": "Bridge", - "insight": "These two notes describe the same subsystem.", - "source_memories": ["aaaaaaaa1111", "bbbbbbbb2222"], - "confidence": 0.8, - "novelty_score": 0.6 - } - ] - }); - let ids = extract_dream_proposals(&r, "dream"); - assert_eq!(ids.len(), 1, "one real insight -> one proposal id"); - assert_eq!(ids[0], "dream:Bridge:aaaaaaaa+bbbbbbbb"); - } - - #[test] - fn extract_dream_proposals_empty_when_not_dream_tool() { - let r = serde_json::json!({ "insights": [{ "insight_type": "x" }] }); - assert!(extract_dream_proposals(&r, "search").is_empty()); - } - - #[test] - fn extract_writes_single_and_batch() { - let single = serde_json::json!({ "decision": "create", "nodeId": "n1" }); - assert_eq!( - extract_writes(&single), - vec![("n1".into(), "create".into())] - ); - let batch = serde_json::json!({ - "results": [ { "decision": "update", "id": "n2" } ] - }); - assert_eq!(extract_writes(&batch), vec![("n2".into(), "update".into())]); - } - - #[test] - fn extract_writes_recognizes_action_shape_b2() { - // B2: memory promote/demote return `action` + `nodeId`, not `decision`. - let promoted = serde_json::json!({ "action": "promoted", "nodeId": "m1" }); - assert_eq!( - extract_writes(&promoted), - vec![("m1".into(), "promoted".into())] - ); - let demoted = serde_json::json!({ "action": "demoted", "nodeId": "m2" }); - assert_eq!( - extract_writes(&demoted), - vec![("m2".into(), "demoted".into())] - ); - // codebase remember_decision returns action + nodeId. - let decision = serde_json::json!({ "action": "remember_decision", "nodeId": "c1" }); - assert_eq!( - extract_writes(&decision), - vec![("c1".into(), "remember_decision".into())] - ); - } - - #[test] - fn extract_writes_ignores_read_actions_b2() { - // A read (memory get / get_batch / state) carries nodeId but is NOT a write. - let read = serde_json::json!({ "action": "get", "nodeId": "m1" }); - assert!(extract_writes(&read).is_empty(), "get is not a write"); - let state = serde_json::json!({ "action": "state", "nodeId": "m2" }); - assert!(extract_writes(&state).is_empty(), "state is not a write"); - } - - #[test] - fn destructive_decision_classification_c2() { - for d in [ - "purge", - "delete", - "forget", - "purged", - "deleted", - "forgotten", - ] { - assert!(is_destructive_decision(d), "{d} is destructive"); - } - for d in ["create", "update", "promote", "reinforce"] { - assert!(!is_destructive_decision(d), "{d} is not destructive"); - } - } - - #[test] - fn content_preview_redacts_sensitive_and_truncates() { - // PRIV: sensitive content is fully redacted, never previewed. - assert_eq!( - content_preview("the production auth token is sk-abc123", true), - "[redacted — sensitive content; review via risk signals]" - ); - // Ordinary content is truncated, not redacted. - let long = "a".repeat(200); - let prev = content_preview(&long, false); - assert!(prev.ends_with('…')); - assert!(prev.chars().count() <= 81); - // Empty content. - assert_eq!(content_preview("", false), "(no content)"); - } - - #[test] - fn content_hash_is_stable_and_hides_text() { - let h = hash_content("my secret memory"); - assert_eq!(h, hash_content("my secret memory"), "stable"); - assert!(!h.contains("secret")); - assert_eq!(h.len(), 16); - } - - #[test] - fn extract_writes_recognizes_destructive_actions_c2() { - // C2: purge/delete are brain mutations and must trace + be gateable. - for act in ["purge", "delete"] { - let r = serde_json::json!({ "action": act, "nodeId": "m1", "success": true }); - assert_eq!( - extract_writes(&r), - vec![("m1".into(), act.to_string())], - "{act} must be traced as a write" - ); - } - } - - fn store() -> std::sync::Arc { - let dir = tempfile::tempdir().unwrap(); - std::sync::Arc::new( - vestige_core::Storage::new(Some(dir.path().join("gate_test.db"))).unwrap(), - ) - } - - #[test] - fn gate_opens_pr_for_destructive_write_after_node_deleted_c2() { - // C2-deep: the row is GONE by the time the gate runs (purge deleted it), - // but a destructive write must STILL open a Memory PR — not be skipped. - let s = store(); - let node = s - .ingest(vestige_core::IngestInput { - content: "A memory the agent is about to purge.".to_string(), - node_type: "fact".to_string(), - ..Default::default() - }) - .unwrap(); - // Actually delete the row, like purge does. - let _ = s.delete_node(&node.id); - assert!(s.get_node(&node.id).unwrap().is_none(), "row is gone"); - - // The tool result the recorder sees for the purge. - let result = serde_json::json!({ "action": "purge", "nodeId": node.id, "success": true }); - let opened = gate_writes( - &s, - None, - "run_c2", - "memory", - &result, - vestige_core::ReviewMode::RiskGated, - ); - - assert_eq!( - opened.len(), - 1, - "destructive write must open a PR even with the node gone" - ); - let pr = s - .list_memory_prs(Some(vestige_core::MemoryPrStatus::Pending), 10) - .unwrap(); - assert_eq!(pr.len(), 1); - assert_eq!(pr[0].subject_id.as_deref(), Some(node.id.as_str())); - // The diff marks the node as deleted and carries no resurrected content. - assert_eq!(pr[0].diff["node"]["deleted"], serde_json::json!(true)); - } - - #[test] - fn gate_redacts_sensitive_content_in_pr_priv() { - // PRIV: a write gated for a sensitive topic must NOT carry the raw - // content into the PR diff/title — only a redaction + hash. - let s = store(); - let secret = "the production auth token is sk-live-SECRET-XYZ"; - let node = s - .ingest(vestige_core::IngestInput { - content: secret.to_string(), - node_type: "fact".to_string(), - ..Default::default() - }) - .unwrap(); - let result = serde_json::json!({ "decision": "create", "nodeId": node.id }); - let opened = gate_writes( - &s, - None, - "run_priv", - "smart_ingest", - &result, - vestige_core::ReviewMode::RiskGated, - ); - assert_eq!(opened.len(), 1, "sensitive write opens a PR"); - - let pr = &s - .list_memory_prs(Some(vestige_core::MemoryPrStatus::Pending), 10) - .unwrap()[0]; - let serialized = serde_json::to_string(pr).unwrap(); - assert!( - !serialized.contains("SECRET-XYZ") && !serialized.contains("sk-live"), - "PR must not contain the raw secret content; got: {serialized}" - ); - assert!( - serialized.contains("redacted"), - "PR must mark the content redacted" - ); - // A content hash is present so reviewers can still correlate. - assert!(pr.diff["node"]["contentHash"].as_str().is_some()); - } - - #[test] - fn pre_gate_blocks_purge_before_deleting_c2() { - let s = store(); - let node = s - .ingest(vestige_core::IngestInput { - content: "A memory awaiting destructive review.".to_string(), - node_type: "fact".to_string(), - ..Default::default() - }) - .unwrap(); - let args = Some(serde_json::json!({ - "action": "purge", - "id": node.id, - "confirm": true, - "reason": "test purge" - })); - - let response = gate_pending_memory_mutation( - &s, - None, - "run_pre_gate", - "memory", - &args, - vestige_core::ReviewMode::RiskGated, - ) - .unwrap() - .expect("purge should be pre-gated"); - - assert_eq!(response["pendingReview"], serde_json::json!(true)); - assert!( - s.get_node(&node.id).unwrap().is_some(), - "pre-gating must not delete before review" - ); - let pr = s - .list_memory_prs(Some(vestige_core::MemoryPrStatus::Pending), 10) - .unwrap(); - assert_eq!(pr.len(), 1); - assert_eq!(pr[0].diff["pendingAction"], serde_json::json!("purge")); - assert_eq!(pr[0].diff["node"]["deleted"], serde_json::json!(false)); - } - - #[test] - fn pre_gate_leaves_fast_mode_direct() { - let s = store(); - let node = s - .ingest(vestige_core::IngestInput { - content: "Fast mode purge target.".to_string(), - node_type: "fact".to_string(), - ..Default::default() - }) - .unwrap(); - let args = Some(serde_json::json!({ - "action": "purge", - "id": node.id, - "confirm": true - })); - - assert!( - gate_pending_memory_mutation( - &s, - None, - "run_fast", - "memory", - &args, - vestige_core::ReviewMode::Fast, - ) - .unwrap() - .is_none(), - "Fast mode should preserve direct execution" - ); - } - - #[test] - fn pre_gate_blocks_direct_suppress_before_mutating() { - let s = store(); - let node = s - .ingest(vestige_core::IngestInput { - content: "A memory awaiting suppress review.".to_string(), - node_type: "fact".to_string(), - ..Default::default() - }) - .unwrap(); - let args = Some(serde_json::json!({ "id": node.id, "reason": "test suppress" })); - - let response = gate_pending_memory_mutation( - &s, - None, - "run_suppress", - "suppress", - &args, - vestige_core::ReviewMode::RiskGated, - ) - .unwrap() - .expect("suppress should be pre-gated"); - - assert_eq!(response["pendingReview"], serde_json::json!(true)); - let kept = s.get_node(&node.id).unwrap().unwrap(); - assert_eq!(kept.suppression_count, 0, "pre-gate must not suppress yet"); - let pr = s - .list_memory_prs(Some(vestige_core::MemoryPrStatus::Pending), 10) - .unwrap(); - assert_eq!(pr[0].diff["pendingAction"], serde_json::json!("suppress")); - } - - #[test] - fn write_tool_set_includes_codebase_b2() { - assert!(is_write_tool("codebase")); - assert!(is_write_tool("memory")); - assert!(!is_write_tool("search")); - assert!(!is_write_tool("deep_reference")); - } -} diff --git a/demo/PITCH-FINAL-spoken.md b/demo/PITCH-FINAL-spoken.md deleted file mode 100644 index 70173cc..0000000 --- a/demo/PITCH-FINAL-spoken.md +++ /dev/null @@ -1,77 +0,0 @@ -# Vestige — THE PITCH THAT CHANGED EVERYTHING (final, condensed cut) - -Sam's stage pitch — ~45-50s spoken, for a multi-pitch event with a separate -networking hour afterward. Solo founder, first person, no slides/terminal on -stage (the demo happens 1:1 at networking on Sam's pre-loaded laptop). Two seams -closed from the draft: opener "hours"->"days" (matches "days ago"), and "AI -agents that touch" (plural). NAME = Vestige. - ---- - -## THE PITCH - -> Your bug was born days before it crashed — but you just can't remember where. -> -> When production breaks, you lose days hunting a bug. The cause is usually a -> quiet change you made days ago that looks nothing like the error it created. -> -> Today's memory systems fail here, because they look for what *looks like* the -> problem. But a root cause never looks like the bug it creates. -> -> **[beat]** -> -> It's like blaming a goalie for a 90th-minute World Cup goal — when the real -> failure was a silent midfield turnover fifteen minutes earlier. They're -> searching the goal line. Vestige traces the match backward. -> -> A 2024 Nature paper proved the human brain rewinds time to link a sudden shock -> to the quiet memory that caused it. Vestige does the exact same thing for -> software. -> -> Everyone else built a memory that remembers. I built the first one that -> *realizes.* Millions of AI agents that touch production will hit this wall. -> That's not a feature — that's the next layer of the dev stack. -> -> I'm Sam. I created Vestige from my tiny Chicago apartment — the first memory -> that finds the cause, not the lookalike. -> -> Come find me after. I'll show you in thirty seconds. - ---- - -## THE LINES THAT WIN THE ROOM -- **Opener (pattern interrupt):** "Your bug was born days before it crashed — but you just can't remember where." -- **The detonation (THE line):** "A root cause never looks like the bug it creates." → beat of silence after. -- **The soccer proof (the secret weapon — makes ANYONE get it):** "...blaming the goalie for a 90th-minute goal when the real failure was a midfield turnover fifteen minutes earlier. They're searching the goal line. Vestige traces the match backward." -- **The category (the quotable):** "Everyone else built a memory that remembers. I built the first one that realizes." -- **The TAM (the money line):** "That's not a feature — that's the next layer of the dev stack." -- **The closer (fits the networking format):** "Come find me after. I'll show you in thirty seconds." - -## STAGE RULES -1. NEVER open with "Hi, I'm Sam, thanks." Walk out, hold the room one beat, THEN - the opener. Your name comes near the END. -2. The detonation line — say it, then a real beat of silence before the soccer line. -3. The soccer analogy is your weapon — deliver it conversationally, like you're - explaining it to a friend. It's what makes a NON-engineer get it instantly. -4. "Built it alone, from a Chicago apartment" is a flex. Own it. -5. End on "I'll show you in thirty seconds." Don't say thank you. Let it hang. - -## NETWORKING (the demo happens HERE, not on stage) -- The stage pitch's only job: be the MOST memorable founder so they seek you out - in the networking hour, out of all the other pitches. -- Have the seeded demo (`./demo/postdict-demo.sh`) ALREADY RUNNING / pre-warmed on - your laptop so when someone walks up you show it in ~10 seconds, no boot fumble. -- Demo from STRENGTH: show YOUR seeded scenario (instant, reproducible). Do NOT - promise to debug their codebase live — Vestige has to be recording their history - first. If asked "does it work on mine?": "it has to record your history first — - give me your repo for a day and I'll show you it catch a real one." (That's the - pilot hook, said 1:1, never from stage.) - -## DELIVERY MAP -- "born days before it crashed" — land "born" with weight. -- "a root cause never looks like the bug it creates" — then a real beat of silence. -- soccer line — conversational, build to "Vestige traces the match backward." -- "It runs locally" energy on "Vestige does the exact same thing for software." -- "the first one that realizes" — let it land. -- "the next layer of the dev stack" — slow, final. Mic-drop. -- "I'll show you in thirty seconds." — quiet, confident, then stop. diff --git a/demo/README.md b/demo/README.md deleted file mode 100644 index fd9469b..0000000 --- a/demo/README.md +++ /dev/null @@ -1,159 +0,0 @@ -# Postdict — memory with hindsight - -> Every other AI memory finds what your bug **looks like**. -> This one finds what **caused** it — even when the cause was days ago -> and looks nothing like the crash. - -When your agent hits a failure **today**, Postdict reaches **backward in time** -and surfaces the quiet earlier change that caused it — the root cause a vector / -semantic search **structurally cannot** find, because it isn't *similar* to the -error, only *causally upstream* of it. - -It's a faithful port of a 2024 *Nature* neuroscience result: the brain links a -later salient event to an earlier quiet memory **backward in time** ("fear links -retrospectively, not prospectively"). Root cause is always in the past — so the -backward-only direction isn't a metaphor, it's the correct behavior. - ---- - -## ▶️ Run the demo (30 seconds, one command) - -```sh -# from the repo root -cargo build -p vestige-mcp --bin vestige --release # first time only (~1 min) -./demo/postdict-demo.sh -``` - -That's it. It uses a **fresh throwaway database** (a temp dir, deleted on exit) — -it touches nothing else on your machine. No API keys, no network, fully local. - -**Pacing:** the script pauses ~1.4s between beats for a clean screen-recording. -- `PAUSE=0 ./demo/postdict-demo.sh` — instant (no pauses) -- `PAUSE=2 ./demo/postdict-demo.sh` — slower, more dramatic - ---- - -## What you'll see - -The demo plants three memories into an empty store, then asks one question: - -| When | Memory | Note | -|---|---|---| -| **3 days ago** | `Set API_TIMEOUT=2 in the deploy env to speed up cold starts` | the quiet cause. boring. forgotten. | -| **20 days ago** | `A 500 Internal Server Error happened in the billing service` | a lookalike — *resembles* today's crash | -| **today** 💥 | `Service crashed: 500 Internal Server Error on the auth endpoint` | the failure | - -Then it runs the same question through both engines, side by side: - -``` -── 1. SIMILARITY SEARCH · keyword (BM25) ── - 1. A 500 Internal Server Error happened in the billing service ← top match - → ranked by RESEMBLANCE. its top hit is a lookalike, not the cause. - -── 2. POSTDICT (reach backward for the CAUSE) ── - #1 Set API_TIMEOUT=2 in the deploy env to speed up cold starts - ↩ reached back 3.0 days before the failure - 🔗 causal join: api_timeout - ✅ promoted — it will resurface next time -``` - -**Similarity search confidently returns the lookalike.** It's wrong. -**Postdict reaches back 3 days and finds the real cause** — by the shared -`API_TIMEOUT` entity, backward in time. Then it promotes that memory so it -stops decaying and surfaces next time. - -> The label says exactly which engine ran (`keyword (BM25)` here; it becomes -> `semantic (vector + BM25 hybrid)` once embeddings are generated). No -> sleight of hand — it's the real search every other memory tool does. - ---- - -## Try your own scenario (the "it's not staged" proof) - -Nothing here is hardcoded. Build any history and ask: - -```sh -DB=$(mktemp -d)/db -BIN=./target/release/vestige - -# plant a quiet cause N days ago (--ago-days backdates it) -$BIN --data-dir "$DB" ingest "Disabled the checkout cache while debugging" \ - --tags checkout --node-type decision --ago-days 4 - -# record a failure today that shares an entity (here: 'checkout') -$BIN --data-dir "$DB" ingest "Checkout latency spiked to 9s after deploy" \ - --tags checkout,latency,regression --node-type event - -# reach backward for the cause, with the similarity contrast -$BIN --data-dir "$DB" backfill --contrast -``` - -The cause and the failure share the `checkout` entity but are **not textually -similar** — so semantic search misses it and Postdict finds it. - -`vestige backfill --help` for all flags (`--manual`, `--lookback-days`, -`--failure-id`, `--no-promote`). - ---- - -## The honest boundary (read this — it's the point) - -- **If the upstream change was never recorded, nothing can reach it.** Postdict - reaches back through *memory*, not magic. No memory of the cause → no backfill. -- **It links by shared entities** (same file / env var / service / symbol), - backward in time — *not* by semantic similarity. That's deliberate: similarity - is exactly the blind spot every other memory already has. -- **It won't invent a cause.** No shared entity between the failure and an - earlier memory → no link. It would rather say nothing than fabricate. -- The "promote" step boosts the cause's retention so it resurfaces; it never - deletes or rewrites anything (bi-temporal — old memories stay queryable). - ---- - -## How it works (for the skeptics) - -1. **Trigger.** A memory lands that reads like a failure (high surprise + - failure markers like `error`/`crash`/`500`), or you mark one manually. -2. **Backward reach.** Postdict scans memories *older* than the failure that - share an entity with it (the causal join), within a lookback window. -3. **Rank by cause, not resemblance.** Candidates are scored by shared-entity - strength and *dissimilarity* — the less a candidate resembles the failure, - the more valuable it is, because that's precisely what a vector search can't - surface. -4. **Promote.** The surfaced cause's FSRS retention is boosted so it stops - decaying and is there next time. - -The mechanism, tests, and the *Nature* citation live in -[`crates/vestige-core/src/advanced/retroactive_backfill.rs`](../crates/vestige-core/src/advanced/retroactive_backfill.rs). -The field itself admits this is unsolved: causal + temporal retrieval is -"largely unexplored" (mem0, *State of AI Agent Memory 2026*), and frontier -models fail at cloud root-cause analysis ([arXiv:2602.09937](https://arxiv.org/abs/2602.09937)). -This is the first memory that does it. - ---- - -## Recording the demo (for a clean clip) - -1. Make your terminal large, dark theme, ~16pt mono font. Clear scrollback. -2. macOS: `Cmd-Shift-5` → record a tight region around the terminal (or the - whole window). QuickTime works too. -3. Run `./demo/postdict-demo.sh` (default pacing) — or `PAUSE=2` for a slower, - more cinematic take. -4. The single "hold here" frame: when **POSTDICT** resolves the `#1` cause with - `↩ reached back 3.0 days` — that's the money shot. Let it sit. -5. Trim to ~30s. Muted + captions plays best in feeds. - -**Caption / hook (ready to paste):** - -> Your crash is today. The cause was an env-var edit 3 days ago — it looks -> *nothing* like the error, so vector search will never surface it. Same query, -> split screen: similarity search returns the lookalike, Postdict reaches back -> and finds the cause. Seed's in the repo — run it yourself. - -**Pinned honest-boundary reply:** *"Where it doesn't work: if the upstream change -was never recorded, nothing can reach it. Everything in the clip is in the -seeded repo."* - ---- - -*Local-first. No API keys. No data leaves your machine.* diff --git a/demo/funding-demo-script.md b/demo/funding-demo-script.md deleted file mode 100644 index 0b52bc3..0000000 --- a/demo/funding-demo-script.md +++ /dev/null @@ -1,129 +0,0 @@ -# Postdict — the 60-second funding demo - -**Audience:** investors. **Goal:** they see a category, a moat, and a market — not a feature. -**The thesis, in four words:** *relevance does not equal resemblance.* - -Format: live terminal, one take, you talking over it. ~60s. Punchy. No slides. -**Rule:** the monologue earns ~20 seconds. The rest is the terminal *doing the impossible* on screen. - ---- - -## THE SCRIPT (what's on screen · what you say) - -### [0:00–0:15] — THE FLAWED AXIOM (your cold open, verbatim) - -**On screen:** a clean dark terminal, one line: -`relevance ≠ resemblance` - -**You say:** -> "Every major AI memory framework on Earth — every VC-backed startup, every -> native platform layer — is built on one flawed assumption: that **relevance -> equals resemblance.** They turn your text into vector embeddings, and when -> something breaks, they search for memories that *look similar* to the problem. -> -> Here's what blows that foundation to pieces: **a root cause never looks like -> the bug it creates.** So the entire industry is searching in the one place the -> answer can never be." - -*(Stop. Let it sit one beat. "Relevance equals resemblance" is the line they repeat to their partners.)* - ---- - -### [0:15–0:28] — MAKE IT CONCRETE (type it live) - -**On screen:** -``` -$ vestige ingest "Set API_TIMEOUT=2 in the deploy env" --ago-days 3 # the quiet cause -$ vestige ingest "500 error in the billing service" --ago-days 20 # an old lookalike -$ vestige ingest "Service crashed: 500 on the auth endpoint" # today's crash -``` - -**You say:** -> "Watch. A one-line config change three days ago — forgotten. An old, unrelated -> 500 error weeks back. And today, the auth service crashes. Now ask: which past -> memory *caused* today's crash? A vector database ranks by resemblance — so to -> it, today's crash looks most like that old billing 500. **The thing that looks -> similar is never the thing that caused it.**" - ---- - -### [0:28–0:45] — THE PROOF (the money shot · slow down here) - -**On screen:** `$ vestige backfill --contrast` → -``` -── 1. SIMILARITY SEARCH · keyword (BM25) ── - 1. 500 error in the billing service ← top match (WRONG) - → ranked by RESEMBLANCE. its top hit is a lookalike, not the cause. - -── 2. POSTDICT (reach backward for the CAUSE) ── - #1 Set API_TIMEOUT=2 in the deploy env - ↩ reached back 3.0 days before the failure - 🔗 causal join: api_timeout (RIGHT) -``` - -**You say (let the `↩ reached back 3.0 days` line hold in silence for a full beat):** -> "Same database. Same query. Similarity search returns the lookalike — confident, -> and wrong. Postdict reaches **backward three days** and finds the actual cause. -> Not because it's similar — because it's **causally upstream.** This is memory -> with hindsight: the 'ohhh, *that's* why' moment, automatic." - ---- - -### [0:45–0:55] — THE MOAT (kill the "can't they just add this?" objection) - -**You say:** -> "Two reasons this is defensible. One: it's a faithful port of a 2024 *Nature* -> result — the brain reaches *backward* in time to find causes, and it's -> backward-*only*, which is exactly right, because a root cause is always in the -> past. We didn't invent this; we ported the algorithm evolution already -> perfected. Two: the incumbents can't bolt this on. Their entire architecture -> **is** the flawed axiom. To do this you rebuild memory from the cognitive -> science up — which we already did, and it's running locally, today." - ---- - -### [0:55–1:00] — THE MARKET + THE ASK - -**On screen:** `the first memory that finds the cause, not the lookalike.` - -**You say:** -> "Every AI agent that writes code, runs infrastructure, or touches production -> hits root causes it can't explain. That's the entire agentic market, and it's -> on fire. We're not a better memory — we're the first memory that **reasons -> backward.** Local-first, reproducible, running now. We're raising [X] to make -> every agent debug like a senior engineer. The seed's in the repo — run it -> yourself." - ---- - -## WHY THIS OPENING IS STRONGER (and how to deliver it) - -Your framing beats "category error" because it names the **mechanism** of the -error, not just that one exists: - -- **"Relevance equals resemblance"** is a *diagnosis* — it tells the investor - precisely what's broken (the axiom) in four words. "Category error" only says - *that* something's broken. -- **The one-two punch:** state the flawed axiom → detonate it with the fact - ("a root cause never looks like the bug it creates"). The investor *feels* the - foundation crack. That's the moment they lean in. - -**Delivery rules:** -1. **The monologue is 20 seconds, max.** Investors fund what they *see* work, not - what they hear claimed. Get to the terminal fast; let the contrast carry the - weight. -2. **Memorize two sentences.** The axiom: *"They believe relevance equals - resemblance."* The detonation: *"A root cause never looks like the bug it - creates."* Everything else can be loose. -3. **Silence is the tool at 0:28–0:45.** When `↩ reached back 3.0 days` hits the - screen, say nothing for a full second. The image does the selling. -4. **The moat answer is non-optional.** Every investor thinks "can't Mem0 add - this?" Answer it *before* they ask — their architecture is the axiom. That's - what converts "neat" into "fundable." -5. **End on the market, not the demo.** "Every agent that touches production" is - the TAM. The demo earns the right to say it; don't bury it under the feature. - -## THE THREE LINES THAT DO THE WORK -- **The axiom (the hook):** "Every AI memory framework believes relevance equals resemblance." -- **The detonation (the thesis):** "A root cause never looks like the bug it creates." -- **The category (the close):** "We're not a better memory. We're the first memory that reasons backward." diff --git a/demo/funding-demo-v2-blow-it-open.md b/demo/funding-demo-v2-blow-it-open.md deleted file mode 100644 index cfb421f..0000000 --- a/demo/funding-demo-v2-blow-it-open.md +++ /dev/null @@ -1,117 +0,0 @@ -# Postdict — the 60-second SPOKEN pitch (solo founder, first person) - -**This is a SPEECH.** You, on stage, looking at 50-100 engineers / founders / -investors. No terminal. No slides. No screen. Just your voice and the room. - -**First person. "I built this." One person.** For a solo founder that's a -*strength* — it tells investors you can ship alone, which is exactly what they -bet on. Never say "we." - -Built for the *ear*: short sentences, hard stops, one line they'll repeat in the -hallway. Read the **[beats]** as pauses — silence is your loudest move on stage. - ---- - -## THE PITCH (≈60 seconds, spoken) - -> Every engineer in this room has lost a day to the same thing. -> -> Production breaks. You burn hours hunting. And the cause turns out to be one -> line you changed three days ago — and forgot. -> -> **[pause]** -> -> The bug looked nothing like that change. So you never connected them. You just -> suffered until you stumbled onto it. -> -> **[pause]** -> -> Now we're handing that exact job — debugging — to AI agents. And every AI -> memory system on Earth is about to fail at it. Mem0, Zep, all of them. Because -> they're built on one assumption: that *relevance equals resemblance.* They -> search your memory for whatever *looks like* the problem. -> -> **[slow down — this is the line]** -> -> But a root cause never looks like the bug it creates. -> -> **[full stop. let it hang.]** -> -> So the entire industry is searching in the one place the answer can never be. -> -> **[pause]** -> -> So I built the opposite. When your agent hits a failure, my memory reaches -> *backward in time* — and finds the quiet change, days earlier, that actually -> caused it. The one no similarity search will ever surface, because it isn't -> *similar* — it's *upstream.* -> -> I didn't invent this. I ported it from your brain. There's a 2024 *Nature* -> paper: when something goes wrong, the brain reaches backward to find the cause -> — backward only, because a cause is always in the past. I turned that into -> software. It runs locally. Today. -> -> **[pause]** -> -> And the incumbents can't copy it — their whole architecture *is* the wrong -> assumption. To do this, you rebuild memory from the brain up. I already did. -> -> **[land it]** -> -> Everyone else built a memory that *remembers.* I built the first one that -> *realizes.* Every AI agent that touches production needs it — that's the whole -> market, and it's on fire. -> -> I'm Sam. This is Postdict. The first memory that finds the *cause*, not the -> lookalike. Come find me — I'll prove it on a laptop in thirty seconds. - ---- - -## HOW TO DELIVER IT (this is 80% of the win on a stage) - -1. **Memorize three sentences. Improvise the rest.** If you only nail three, - nail these: - - **The wound:** *"The cause turns out to be one line you changed three days ago — and forgot."* - - **The detonation:** *"A root cause never looks like the bug it creates."* - - **The category:** *"Everyone else built a memory that remembers. I built the first one that realizes."* - -2. **The detonation line is the whole pitch.** Walk *toward* the audience as you - say it. Then STOP. Say nothing for two full seconds. That silence is you - letting 100 people independently realize you're right. Do not rush it. - -3. **Open about THEM, not you.** The first 15 seconds is their pain, not your - product. When heads nod because they've lived it, the room is yours — you - haven't even said what you do yet. - -4. **"I built this" is a flex, not a weakness.** Solo means you can ship without - a team — investors fund that. Say "I built," "I ported," "I already did it." - Own it. - -5. **Short sentences. Hard stops.** On a stage, a long sentence loses the back - row. Every sentence above is built to be said in one breath. Trust the - periods. - -6. **Don't demo on stage — *promise* the demo.** "Come find me, I'll prove it on - a laptop in 30 seconds" is stronger than fumbling a terminal in front of 100 - people. It pulls the serious ones to you afterward, one-on-one, where deals - actually start. - -7. **End on your name + the one-liner.** "I'm Sam. This is Postdict. The first - memory that finds the cause, not the lookalike." That's what they Google in - the parking lot. - ---- - -## THE 30-SECOND VERSION (if you only get a hallway / elevator) - -> "Every AI memory tool searches for what your bug *looks like.* But a root cause -> never looks like the bug it creates — it's a config change from three days ago -> that looks nothing like the crash. So they all miss it. I built the first -> memory that reaches *backward in time* to find the actual cause. Ported it -> straight from a *Nature* paper on how the brain does it. Every agent that -> touches production needs it. I'll show you on a laptop right now." - -## THE ONE LINE (if you get five seconds) - -> "I built the first AI memory that finds the *cause* of a bug, not the lookalike -> — because a cause never looks like the bug it creates." diff --git a/demo/postdict-demo.sh b/demo/postdict-demo.sh deleted file mode 100755 index 9477d4d..0000000 --- a/demo/postdict-demo.sh +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env bash -# ============================================================================ -# POSTDICT — Memory with Hindsight -# The demo: your service crashes TODAY. The cause was a quiet env-var change -# 3 DAYS AGO that vector search will never find. Watch Postdict reach back. -# -# Run it yourself: ./demo/postdict-demo.sh -# (uses a fresh throwaway DB — touches nothing else on your machine) -# ============================================================================ -set -euo pipefail - -# --- config ----------------------------------------------------------------- -BIN="${VESTIGE_BIN:-./target/release/vestige}" -DB="$(mktemp -d)/postdict-demo" -# pacing: how long to pause between beats (override with PAUSE=0 for instant) -PAUSE="${PAUSE:-1.4}" - -# --- colors ----------------------------------------------------------------- -B=$'\033[1m'; DIM=$'\033[2m'; R=$'\033[31m'; G=$'\033[32m'; Y=$'\033[33m' -M=$'\033[35m'; C=$'\033[36m'; W=$'\033[97m'; X=$'\033[0m' - -beat() { sleep "$PAUSE"; } -say() { printf '%s\n' "$1"; } -type_cmd() { # echo a command like it was typed - printf '%s$ %s%s\n' "$DIM" "$1" "$X"; beat -} - -clear 2>/dev/null || printf '\n\n' -say "${M}${B} ██████ POSTDICT — memory with hindsight ██████${X}" -say "${DIM} every other memory finds what your bug LOOKS like.${X}" -say "${DIM} this one finds what CAUSED it.${X}" -echo; beat - -# ── DAY -3 ────────────────────────────────────────────────────────────────── -say "${C}${B}┌─ 3 DAYS AGO ──────────────────────────────────────────────┐${X}" -say "${C}${B}│${X} a tiny, boring config change. nobody thinks twice. ${C}${B}│${X}" -say "${C}${B}└───────────────────────────────────────────────────────────┘${X}" -type_cmd "vestige ingest \"Set API_TIMEOUT=2 in the deploy env to speed up cold starts\" --tags API_TIMEOUT,deploy-env --ago-days 3" -"$BIN" --data-dir "$DB" ingest "Set API_TIMEOUT=2 in the deploy env to speed up cold starts" \ - --tags "API_TIMEOUT,deploy-env" --node-type decision --ago-days 3 2>/dev/null | grep -E "Node ID|Backdated" | sed "s/^/ ${DIM}/;s/$/${X}/" -echo; beat - -# ── DAY -20 (a noisy lookalike) ────────────────────────────────────────────── -say "${DIM} (also in history: an unrelated 500 error that LOOKS like today's crash)${X}" -"$BIN" --data-dir "$DB" ingest "A 500 Internal Server Error happened in the billing service last month" \ - --tags "billing-service" --node-type event --ago-days 20 2>/dev/null >/dev/null -beat - -# ── TODAY ──────────────────────────────────────────────────────────────────── -say "${R}${B}┌─ TODAY ────────────────────────────────────────────────────┐${X}" -say "${R}${B}│${X} 💥 your service just crashed. ${R}${B}│${X}" -say "${R}${B}└────────────────────────────────────────────────────────────┘${X}" -type_cmd "vestige ingest \"Service crashed: 500 Internal Server Error on the auth endpoint\" --tags auth-service,API_TIMEOUT,crash" -"$BIN" --data-dir "$DB" ingest "Service crashed: 500 Internal Server Error on the auth endpoint" \ - --tags "auth-service,API_TIMEOUT,crash" --node-type event 2>/dev/null >/dev/null -say " ${R}recorded.${X} now: ${W}${B}why did it crash?${X}" -echo; beat; beat - -# ── THE TURN ───────────────────────────────────────────────────────────────── -type_cmd "vestige backfill --contrast" -"$BIN" --data-dir "$DB" backfill --contrast 2>/dev/null -echo; beat - -# ── THE PRESTIGE ───────────────────────────────────────────────────────────── -say "${G}${B} ┌──────────────────────────────────────────────────────────┐${X}" -say "${G}${B} │${X} semantic search returned the lookalike. ${G}${B}│${X}" -say "${G}${B} │${X} Postdict reached back 3 days to the real cause. ${G}${B}│${X}" -say "${G}${B} │${X} ${W}not similar to the bug. causally upstream.${X} ${G}${B}│${X}" -say "${G}${B} └──────────────────────────────────────────────────────────┘${X}" -echo -say "${DIM} run it yourself: ./demo/postdict-demo.sh (seed is right here)${X}" -say "${DIM} honest limit: if the cause was never recorded, nothing can reach it.${X}" - -rm -rf "$(dirname "$DB")" diff --git a/docs/AGENT-MEMORY-PROTOCOL.md b/docs/AGENT-MEMORY-PROTOCOL.md deleted file mode 100644 index 6096982..0000000 --- a/docs/AGENT-MEMORY-PROTOCOL.md +++ /dev/null @@ -1,81 +0,0 @@ -# Agent Memory Protocol - -> Minimal instructions for any MCP-compatible agent using Vestige. - -Vestige is an MCP server, not a Claude-specific workflow. Register `vestige-mcp` -with your client, then give the agent a short instruction that makes memory part -of its normal reasoning loop. - -## Register Vestige - -Use your client's MCP server configuration format. The command is the same: - -```json -{ - "mcpServers": { - "vestige": { - "command": "vestige-mcp" - } - } -} -``` - -Examples: - -```bash -claude mcp add vestige vestige-mcp -s user -codex mcp add vestige -- vestige-mcp -``` - -## Agent Instruction - -Add this to the agent's global or project instruction file: - -```text -Use Vestige as durable local memory. - -At the start of a new session, call `session_context` with the current user, -project, and task context. If `session_context` is unavailable or too broad, call -`search` with a concrete query matching the current task. - -When accuracy or prior decisions matter, call `deep_reference`. When memories may -conflict, call `contradictions` before answering. Compose retrieved evidence into -the answer; do not merely paste memory summaries. - -Save durable preferences, project decisions, recurring corrections, stable facts, -and reusable code patterns with `smart_ingest`. Do not store secrets, credentials, -one-off logs, speculation, or transient command output. - -When the user says a memory was useful, call `memory` with `action="promote"`. -When the user says a memory was wrong or unhelpful, call `memory` with -`action="demote"`. When the user explicitly asks to erase a memory permanently, -call `memory` with `action="purge"` and `confirm=true`. -``` - -## Practical Tool Choices - -| Situation | Tool | -|-----------|------| -| Start of session | `session_context` | -| Find exact identifiers, paths, env vars, names | `search` | -| Answer from prior decisions or evolving facts | `deep_reference` | -| Inspect disagreements before answering | `contradictions` | -| Save a preference, decision, correction, or code pattern | `smart_ingest` | -| Retrieve, promote, demote, edit, or purge one memory | `memory` | -| Create a future reminder | `intention` | -| Check health or maintenance state | `system_status` | - -## What Not To Store - -- API keys, tokens, passwords, private keys, or session cookies. -- Raw logs or command output unless the durable lesson is extracted first. -- Guesswork the agent has not verified. -- Temporary plans that will be obsolete after the current session. -- User data the user asked not to retain. - -## Portability Notes - -The same protocol applies to Claude Code, Codex, Cursor, VS Code, Xcode, -OpenCode, JetBrains, Windsurf, and any other client that can run a stdio MCP server. Claude -Code's Cognitive Sandwich hooks are optional companion files; they are not -required for normal Vestige memory. diff --git a/docs/COGNITIVE_SANDWICH.md b/docs/COGNITIVE_SANDWICH.md index 4ae9703..da4053d 100644 --- a/docs/COGNITIVE_SANDWICH.md +++ b/docs/COGNITIVE_SANDWICH.md @@ -21,7 +21,7 @@ The default Cognitive Sandwich installer only stages files and removes old v2.1. └────────────────────────────────────────────────┘ ``` -Sanhedrin, preflight, and all Vestige Claude Code hooks are optional. The default installer wires none of them; it does not call Claude, start MLX, require a 19 GB model download, or require 20+ GB of RAM. Users who want preflight context can opt in with `--enable-preflight`. Users who want the post-response verifier can opt in with `--enable-sanhedrin` and point it at any OpenAI-compatible `/v1/chat/completions` endpoint and model name. Sanhedrin is model-agnostic: if no verifier model is configured, it fails open and records guidance instead of guessing a large model. On Apple Silicon, an additional `--with-launchd` flag can auto-start the local MLX Qwen backend. +Sanhedrin, preflight, and all Vestige Claude Code hooks are optional. The default installer wires none of them; it does not call Claude, start MLX, require a 19 GB model download, or require 20+ GB of RAM. Users who want preflight context can opt in with `--enable-preflight`. Users who want the post-response verifier can opt in with `--enable-sanhedrin` and point it at any OpenAI-compatible `/v1/chat/completions` endpoint. On Apple Silicon, an additional `--with-launchd` flag can auto-start the local MLX Qwen backend. --- @@ -37,7 +37,7 @@ Sanhedrin, preflight, and all Vestige Claude Code hooks are optional. The defaul 3. **Claude reads the assembled context and generates a draft.** 4. **By default, no Vestige Stop hooks are installed.** If explicitly enabled, Stop hooks fire serially (any can VETO with `exit 2`, forcing a rewrite): - `veto-detector.sh` — fast regex against `veto`-tagged Vestige memories (~50ms) - - `sanhedrin.sh` → `sanhedrin-local.py` — optional Sanhedrin verifier + - `sanhedrin.sh` → `sanhedrin-local.py` — optional single-shot semantic verdict - `synthesis-stop-validator.sh` — regex against forbidden patterns (hedging, summary-instead-of-composition) 5. **If all enabled Stop hooks return `exit 0`, the response is delivered.** @@ -45,44 +45,19 @@ Sanhedrin, preflight, and all Vestige Claude Code hooks are optional. The defaul ## The Sanhedrin Executioner protocol -Sanhedrin has two execution modes: - -- **Legacy mode** (`VESTIGE_SANHEDRIN_CLAIM_MODE=0`) keeps the original broad draft-level semantic check for technical-looking responses. -- **Claim mode** (`VESTIGE_SANHEDRIN_CLAIM_MODE=1`) extracts check-worthy claims, retrieves Vestige evidence per claim, and aggregates structured verdicts before the Stop hook allows delivery. - -The claim-mode Executioner extracts atomic claims from Claude's draft across these classes: +The Executioner extracts atomic claims from Claude's draft across 10 classes: `TECHNICAL` · `BIOGRAPHICAL` · `FINANCIAL` · `ACHIEVEMENT` · `TIMELINE` · `QUANTITATIVE` · `ATTRIBUTION` · `CAUSAL` · `COMPARATIVE` · `EXISTENTIAL` · plus v2.1.0 additions: `VAGUE-QUANTIFIER` · `UNVERIFIED-POSITIVE` -For each check-worthy claim, claim mode calls Vestige's `/api/deep_reference` and judges the claim against high-trust durable evidence plus any optional staged evidence overlay. Decision rules: +For each claim, it checks Vestige's `deep_reference` for high-trust contradicting memories. Decision rules: | Class | Rule | |---|---| -| TECHNICAL / EXISTENTIAL / CAUSAL / COMPARATIVE | VETO only on same-subject durable contradiction; missing memory is `NEI` | -| BIOGRAPHICAL / FINANCIAL / ACHIEVEMENT / TIMELINE / QUANTITATIVE / ATTRIBUTION / VAGUE-QUANTIFIER about the user | zero high-trust durable evidence is `REFUTED_BY_ABSENCE` and blocks | -| **VAGUE-QUANTIFIER** | VETO on vague achievement or financial claims without durable enumeration | +| TECHNICAL / EXISTENTIAL / TIMELINE | VETO if memory trust > 0.55 directly contradicts | +| BIOGRAPHICAL / FINANCIAL / ACHIEVEMENT / ATTRIBUTION | VETO if contradicted OR if factual-shaped with zero supporting evidence (fail-closed) | +| **VAGUE-QUANTIFIER** | VETO on "a few wins / some prize money / most placed" without enumeration | | **UNVERIFIED-POSITIVE** | VETO on specific named institutions/dates/employers not in evidence | -Structured verdicts: - -| Verdict | Meaning | -|---|---| -| `SUPPORTED` | High-trust evidence supports or does not contradict the claim | -| `REFUTED` | High-trust durable evidence directly contradicts the same-subject claim | -| `REFUTED_BY_ABSENCE` | User-critical claim has no high-trust durable Vestige evidence | -| `NEI` | Not enough information; allow unless another claim blocks | - -The bridge still prints legacy one-line `yes` / `no - ...` by default for Stop-hook compatibility. With `VESTIGE_SANHEDRIN_OUTPUT=json`, it emits structured JSON containing `decision`, `reason`, and per-claim verdicts. `sanhedrin.sh` can parse either format. - -### Staged evidence overlay - -`VESTIGE_SANHEDRIN_STAGE_FILE` may point to a JSON array of current-turn evidence candidates. Sanhedrin can read this staged evidence as context, but staged evidence is deliberately non-durable: - -- it never calls `smart_ingest` -- it cannot promote, demote, merge, suppress, or supersede durable memories -- it does not satisfy the durable-evidence requirement for `SUPPORTED`, `REFUTED`, or `REFUTED_BY_ABSENCE` -- durable memory writes remain a separate commit-after-pass step - False-positive guards (added v2.1.0 after dogfood): - Subject-equality gate (memory about Vestige codebase ≠ contradiction with external tools) - Version-discriminator rule (M3 Max ≠ M5 Max; Qwen3.5 ≠ Qwen3.6) @@ -94,19 +69,12 @@ False-positive guards (added v2.1.0 after dogfood): ## Installation -### From an installed Vestige CLI +### One-liner ```bash -vestige sandwich install +curl -fsSL https://raw.githubusercontent.com/samvallad33/vestige/v2.1.0/scripts/install-sandwich.sh | sh ``` -`vestige update` updates binaries only by default. To refresh these optional -Claude Code companion files during an update, run -`vestige update --sandwich-companion`. The companion installer does not activate -any Claude Code hook unless you pass an explicit opt-in flag. It removes old -v2.1.0 Vestige hook wiring from `~/.claude/settings.json` while preserving -unrelated user hooks. - ### From a checkout ```bash @@ -116,13 +84,15 @@ cd vestige ./scripts/check-sandwich-prereqs.sh # verify no Vestige hooks are wired by default ``` +The default command does not activate any Claude Code hook. It removes old v2.1.0 Vestige hook wiring from `~/.claude/settings.json` while preserving unrelated user hooks. + ### Optional Preflight Preflight is a separate opt-in layer. It includes `preflight-swarm.sh`, which uses `claude -p --model claude-haiku-4-5-20251001`; it is not wired by default. ```bash -vestige sandwich install --enable-preflight -scripts/check-sandwich-prereqs.sh --preflight +./scripts/install-sandwich.sh --enable-preflight +./scripts/check-sandwich-prereqs.sh --preflight ``` ### Optional Sanhedrin @@ -130,37 +100,26 @@ scripts/check-sandwich-prereqs.sh --preflight Sanhedrin is a separate opt-in layer. ```bash -# Wire the Sanhedrin Stop hook without choosing a model yet. -# It will fail open until endpoint/model are configured. -vestige sandwich install --enable-sanhedrin +# Wire the Sanhedrin Stop hook, using the default OpenAI-compatible endpoint. +./scripts/install-sandwich.sh --enable-sanhedrin # Apple Silicon only, and only if the machine has enough memory: -vestige sandwich install --enable-sanhedrin --with-launchd +./scripts/install-sandwich.sh --enable-sanhedrin --with-launchd # x86 / Linux / Intel Mac: use any OpenAI-compatible endpoint. -vestige sandwich install \ +./scripts/install-sandwich.sh \ --enable-sanhedrin \ --sanhedrin-endpoint=http://127.0.0.1:11434/v1/chat/completions \ --sanhedrin-model=qwen2.5:14b ``` -Backend presets live at `hooks/sanhedrin-presets.json` and cover custom -OpenAI-compatible servers, small local laptops, balanced local Ollama, MLX, -vLLM, llama.cpp, hosted OpenAI-compatible APIs, and Anthropic via LiteLLM. -Presets are recipes, not requirements. The hook itself only needs an -OpenAI-compatible `/v1/chat/completions` endpoint and a model name chosen by the -user. Backend-specific payload extensions are enabled only by -`VESTIGE_SANHEDRIN_BACKEND=mlx` or `vllm`. For hosted APIs, use -`VESTIGE_SANHEDRIN_API_KEY`; Sanhedrin intentionally does not forward a generic -`OPENAI_API_KEY` to arbitrary configured endpoints. - ### Prerequisites | Tool | Install | |---|---| | Python 3.10+ | typically preinstalled | | `jq` | `brew install jq` | -| `vestige-mcp` | `npm install -g vestige-mcp-server` | +| `vestige-mcp` | `cargo install vestige-mcp` | | Claude Code | https://claude.ai/code | Optional Apple Silicon local Sanhedrin backend: @@ -196,8 +155,7 @@ cp ~/.claude/settings.json.bak.pre-sandwich ~/.claude/settings.json ## Performance notes Optional local MLX backend on M3 Max 16-core (400 GB/s memory bandwidth): -- Legacy Sanhedrin verdict: 5–15 seconds end-to-end (single deep_reference + single Qwen call) -- Claim mode: one `/api/deep_reference` call per extracted check-worthy claim, capped by `VESTIGE_SANHEDRIN_MAX_CLAIMS` +- Sanhedrin verdict: 5–15 seconds end-to-end (single deep_reference + single Qwen call) - mlx_lm.server token generation: ~82 tok/s - mlx_lm.server peak resident memory: ~19.7 GB - Cold model load: ~5 seconds @@ -213,14 +171,8 @@ On M3 Max 14-core or M2/M1 Max: closer to 3–7s prompt processing, ~50–60 tok | `VESTIGE_SANHEDRIN_ENABLED` | `0` | Set to `1` to enable the optional Sanhedrin Stop hook | | `VESTIGE_SWARM_ENABLED` | `1` | Set to `0` to disable preflight lateral-thinker swarm | | `VESTIGE_DASHBOARD_PORT` | `3927` | Vestige MCP HTTP API port used by hooks | -| `VESTIGE_SANHEDRIN_ENDPOINT` | unset | OpenAI-compatible chat completions endpoint for Sanhedrin | -| `VESTIGE_SANHEDRIN_MODEL` | unset | Model name sent to the Sanhedrin endpoint; choose any compatible model | -| `VESTIGE_SANHEDRIN_BACKEND` | unset | Optional backend hint (`ollama`, `llama.cpp`, `mlx`, `vllm`, `openai`, `litellm`) | -| `VESTIGE_SANHEDRIN_CLAIM_MODE` | `1` when installed with `--enable-sanhedrin` | Enables per-claim retrieval and fail-closed user-critical lanes | -| `VESTIGE_SANHEDRIN_OUTPUT` | `json` when installed with `--enable-sanhedrin` | Emits structured JSON from the bridge; shell hook also accepts legacy text | -| `VESTIGE_SANHEDRIN_STAGE_FILE` | unset | Optional JSON-array staged evidence overlay, read-only and non-durable | -| `VESTIGE_SANHEDRIN_MAX_CLAIMS` | `8` | Max check-worthy claims adjudicated per draft | -| `VESTIGE_SANHEDRIN_PYTHON` | `python3` from `PATH` | Optional Python interpreter override for the Stop hook bridge | +| `VESTIGE_SANHEDRIN_ENDPOINT` | `http://127.0.0.1:8080/v1/chat/completions` | OpenAI-compatible chat completions endpoint for Sanhedrin | +| `VESTIGE_SANHEDRIN_MODEL` | `mlx-community/Qwen3.6-35B-A3B-4bit` | Model name sent to the Sanhedrin endpoint | | `MLX_ENDPOINT` / `VESTIGE_SANDWICH_MODEL` | legacy aliases | Backward-compatible names still read by the bridge | | `VESTIGE_MEMORY_DIR` | (auto) | Override per-user Claude memory dir | @@ -228,7 +180,7 @@ On M3 Max 14-core or M2/M1 Max: closer to 3–7s prompt processing, ~50–60 tok ## Architecture provenance -The Cognitive Sandwich originated April 2026 as a defense against a dogfood failure mode: Claude retrieved relevant memories but summarized them instead of composing them into a recommendation. The pre-cognitive layer enforces composition; the post-cognitive layer catches contradictions before they ship. +The Cognitive Sandwich originated April 2026 as a defense against the AIMO3 36/50 failure mode — Claude retrieving relevant memories but summarizing them instead of composing them into recommendations. The pre-cognitive layer enforces composition; the post-cognitive layer catches contradictions before they ship. Full architecture memory: search Vestige for `god-tier-plan` or `cognitive-sandwich` tags after install. diff --git a/docs/COMPOSED_GRAPH.md b/docs/COMPOSED_GRAPH.md deleted file mode 100644 index e0748be..0000000 --- a/docs/COMPOSED_GRAPH.md +++ /dev/null @@ -1,159 +0,0 @@ -# ComposedGraph - -ComposedGraph records memory combinations as durable reasoning events. - -Most memory systems store facts, entities, or relationships. ComposedGraph stores a -different object: which memories were used together, why they were used, and what -happened afterward. - -## Model - -`composition_events` stores the reasoning envelope: - -- tool and mode, such as `deep_reference` or `bounty` -- query and query hash -- confidence, status, and output preview -- metadata for intent, analyzed memory count, activation expansion, and reasoning preview - -`composition_members` stores the participating memories: - -- memory id -- role, such as `primary`, `supporting`, `contradicting`, or `superseded` -- rank, trust, relevance score, preview, and metadata - -`composition_outcomes` stores later labels: - -- `helpful` -- `dead_end` -- `submitted` -- `accepted` -- `rejected` -- `duplicate_risk` -- `needs_poc` -- `bad_severity` -- `user_promoted` -- `user_demoted` -- `closed_by_scope` -- `closed_by_duplicate` -- `closed_by_false_assumption` -- `closed_by_user` -- `expired_lane` - -Member memory ids are intentionally historical references, not foreign keys into -`knowledge_nodes`. Purging or superseding a memory should not erase the fact that -it once participated in a reasoning path. - -## MCP Tool - -Use `composed_graph` for read/write access to the composition ledger. - -```json -{ "action": "recent", "limit": 10 } -``` - -```json -{ "action": "get", "event_id": "" } -``` - -```json -{ "action": "memory", "memory_id": "", "limit": 10 } -``` - -```json -{ "action": "neighbors", "memory_id": "", "limit": 10 } -``` - -```json -{ "action": "never_composed", "tags": ["project:vestige"], "limit": 10 } -``` - -```json -{ - "action": "label", - "event_id": "", - "outcome_type": "helpful", - "notes": "This combination led to the accepted fix." -} -``` - -## Never-Composed Frontier - -`never_composed` returns pairs that have not yet appeared together in a -composition event. - -The ranking is intentionally not just shared-tag matching. It combines: - -- exact shared tags -- shared meaningful content terms -- boundary tags such as `boundary-*`, `oracle`, `queue`, `settlement`, `upgrade`, - `pause`, `accounting`, or `scope` -- node-type diversity -- FSRS retention strength -- composition novelty, so memories that have not already been heavily composed - still get surfaced -- prior composition outcomes from either member, so previously accepted, - duplicate-risk, or dead-end lanes shape the frontier without hiding it - -Each candidate includes: - -- `score` -- `noveltyScore` -- `bridgeScore` -- `trustScore` -- `outcomeScoreAdjustment` -- `sharedTags` -- `boundaryTags` -- `sharedTerms` -- `priorOutcomes` -- `outcomeSignal`, such as `clean`, `prior_success`, `prior_duplicate_risk`, - `prior_closed_door`, or `mixed_prior_outcomes` -- node types -- previews -- a short reason -- a `compositionQuestion` that an agent can answer before taking action - -The output is a frontier queue, not a finding. A never-composed pair means -"worth investigating," not "true," "novel," or "reportable." -Prior outcomes are also guardrails, not verdicts: a duplicate-risk signal should -make the agent check duplicate families first, while a success signal should make -it inspect why the older composition worked. - -Closed-door labels should be specific when possible. Prefer `closed_by_scope`, -`closed_by_duplicate`, `closed_by_false_assumption`, `closed_by_user`, or -`expired_lane` over a generic `dead_end` when the reason is known. - -## Bounty / Research Mode - -`bounty_mode` is a higher-level read shape for investigative workflows. It returns: - -- recent already-composed lanes -- never-composed lanes -- closed doors -- duplicate-risk lanes -- lanes that need proof-of-concept work -- top weird combinations - -This is useful for security research, bug triage, architecture work, and product -strategy because failed or duplicate compositions are preserved instead of being -rediscovered repeatedly. - -## Deep Reference Integration - -`deep_reference` persists composition events automatically when it has evidence -members. Empty evidence does not create a ledger event. - -The response includes: - -- `composition_event_id` when persisted -- `compositionWriteStatus`, usually `persisted` or `skipped_empty` - -## Design Direction - -The next useful upgrades are: - -- triple or n-ary candidate mining, not only pairs -- structural-fit scoring for analogies, separate from surface similarity -- trust-zone scoring so a composition is limited by its weakest provenance -- temporal replay: "what combinations were available when this decision was made?" -- evaluation tasks where success requires combining memories that were never - previously co-composed diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 382eff4..63e9cdf 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -6,7 +6,7 @@ ## First-Run Network Requirement -Vestige downloads the **Nomic Embed Text v1.5** model (~130MB) from Hugging Face on first use. Qwen3 embeddings are opt-in and download their own Hugging Face model when selected. +Vestige downloads the **Nomic Embed Text v1.5** model (~130MB) from Hugging Face on first use. **All subsequent runs are fully offline.** @@ -16,7 +16,7 @@ The embedding model is cached in platform-specific directories: | Platform | Cache Location | |----------|----------------| -| macOS | `~/Library/Caches/vestige/fastembed` | +| macOS | `~/Library/Caches/com.vestige.core/fastembed` | | Linux | `~/.cache/vestige/fastembed` | | Windows | `%LOCALAPPDATA%\vestige\cache\fastembed` | @@ -25,8 +25,6 @@ Override with environment variable: export FASTEMBED_CACHE_PATH="/custom/path" ``` -Qwen3 currently uses Hugging Face Hub's Candle loader directly, so use the standard Hugging Face cache environment such as `HF_HOME` if you need to relocate that larger model cache. - --- ## Environment Variables @@ -34,117 +32,24 @@ Qwen3 currently uses Hugging Face Hub's Candle loader directly, so use the stand | Variable | Default | Description | |----------|---------|-------------| | `VESTIGE_DATA_DIR` | OS per-user data directory | Storage directory fallback; overridden by `--data-dir`; database lives at `/vestige.db` | -| `VESTIGE_EMBEDDING_MODEL` | `nomic-v1.5` | Embedding backend selector. Use `qwen3-0.6b` with a build that enables `qwen3-embeddings` | | `RUST_LOG` | `info` (via tracing-subscriber) | Log verbosity + per-module filtering | -| `FASTEMBED_CACHE_PATH` | Platform cache directory; `./.fastembed_cache` fallback | Embedding model cache location | +| `FASTEMBED_CACHE_PATH` | `./.fastembed_cache` | Embedding model cache location | | `VESTIGE_DASHBOARD_PORT` | `3927` | Dashboard HTTP + WebSocket port | -| `VESTIGE_HTTP_ENABLED` | `false` | Set `true` or `1` to enable optional MCP-over-HTTP | -| `VESTIGE_HTTP_PORT` | `3928` | Optional MCP-over-HTTP port; `--http-port` also enables HTTP | +| `VESTIGE_HTTP_PORT` | `3928` | Optional MCP-over-HTTP port | | `VESTIGE_HTTP_BIND` | `127.0.0.1` | HTTP bind address | -| `VESTIGE_HTTP_ALLOWED_ORIGINS` | localhost origins for the HTTP port | Comma-separated browser origins allowed to call MCP-over-HTTP | | `VESTIGE_AUTH_TOKEN` | auto-generated | Dashboard + MCP HTTP bearer auth | | `VESTIGE_DASHBOARD_ENABLED` | `false` | Set `true` or `1` to enable the web dashboard | | `VESTIGE_CONSOLIDATION_INTERVAL_HOURS` | `6` | FSRS-6 decay cycle cadence | -| `VESTIGE_BACKFILL_AUTOFIRE` | `on` | Retroactive Salience Backfill auto-fire during consolidation. On by default; set `0`/`false`/`off`/`no` to disable. The manual `backfill` tool + CLI stay available either way. When on, promotion is bounded (`stability = MIN(stability * 1.5, stability + 365)`) | -| `VESTIGE_AUTO_CONSOLIDATE_MERGE` | `on` | Auto concat-merge of near-duplicate memories during consolidation (keeps the strongest, folds the rest in as `[MERGED]` blocks, deletes the originals). On by default; set `0`/`false`/`off`/`no` to disable. Protected (`dedup protect`) memories are never absorbed or deleted by this pass, on or off. | > **Storage location precedence:** `--data-dir ` wins over `VESTIGE_DATA_DIR`; if neither is set, Vestige uses your OS's per-user data directory: `~/Library/Application Support/com.vestige.core/` on macOS, `~/.local/share/vestige/core/` on Linux, `%APPDATA%\vestige\core\` on Windows. Custom paths are directories, are created if missing, expand a leading `~`, and store the database at `/vestige.db`. --- -## Output Configuration (`vestige.toml`) - -> Added in **v2.1.26** (Roadmap Phase 2: Configurable Output). - -You can control the default shape and size of high-traffic MCP responses with an -optional config file. It is **local-first** — no cloud service is involved — and -**fully backward-compatible**: with no file present, Vestige behaves exactly as -it did before. - -### Location - -The config file lives in the active Vestige data directory, alongside the -database: - -``` -/vestige.toml # e.g. ~/Library/Application Support/com.vestige.core/vestige.toml -``` - -The data directory is resolved with the same precedence as storage -(`--data-dir` > `VESTIGE_DATA_DIR` > OS per-user data dir). A missing file, or a -file with no recognized keys, falls back to built-in defaults. The parser is -lenient: unknown keys and unknown sections are ignored, so the file can grow in -future releases without breaking older binaries. - -### `[defaults]` table - -```toml -[defaults] -# Detail level for high-traffic tools: "brief" | "summary" | "full" -detail_level = "summary" - -# Default result count for high-traffic tools (positive integer) -limit = 10 - -# Output profile: "lean" | "default" | "audit" | "research" -profile = "default" -``` - -All three keys are optional. `detail_level` and `limit`, when set, override the -selected profile's presets. - -### Output profiles - -A profile presets a coherent bundle of detail level, default limit, and whether -scores and timestamps are included: - -| Profile | Detail | Default limit | Scores | Timestamps | Use when | -|---------|--------|---------------|--------|------------|----------| -| `lean` | `brief` | 5 | dropped | dropped | Context budget matters most | -| `default` | `summary` | tool default | shown | shown | **Historical behavior (unchanged)** | -| `audit` | `full` | tool default | shown | shown | Reviewing or debugging memory state | -| `research` | `full` | 25 | shown | shown | Wide, detailed result sets | - -### Precedence - -Resolved per call, highest to lowest: - -1. **Explicit MCP parameter** (e.g. `detail_level` / `limit` on a `search` - call) — always wins. -2. **`vestige.toml`** — the `[defaults]` keys and the selected profile. -3. **Built-in default** — the `default` profile, identical to pre-v2.1.26 - behavior. - -### Affected tools - -`search`, `memory_timeline`, `codebase` (`get_context`), and `session_context` -resolve their default detail level and result limit through this config. Each of -these tools also echoes the active `profile` in its response so you can confirm -what was applied. Tools that take no `detail_level`/`limit` are unaffected. - -### Example: minimize context cost - -```toml -[defaults] -profile = "lean" -``` - -### Example: detailed audits without changing the profile - -```toml -[defaults] -detail_level = "full" -limit = 50 -``` - ---- - ## Command-Line Options ```bash vestige-mcp --data-dir /custom/path # Custom storage location VESTIGE_DATA_DIR=~/.vestige vestige-mcp # Env fallback storage location -VESTIGE_DATA_DIR=./.vestige vestige stats # Point the CLI at the same custom DB vestige-mcp --help # Show all options ``` @@ -161,10 +66,6 @@ vestige stats --states # Cognitive state distribution vestige health # System health check vestige consolidate # Run memory maintenance vestige restore # Restore from backup -vestige portable-export # Exact Vestige-to-Vestige archive -vestige portable-import # Import exact archive into an empty database -vestige portable-import --merge # Merge exact archive into this database -vestige sync # Pull/merge/push through a file backend ``` --- @@ -230,42 +131,6 @@ Add to `%APPDATA%\Claude\claude_desktop_config.json`: } ``` -### OpenCode - -OpenCode supports global and project-local config. For a project-local setup, add to `opencode.json`: - -```json -{ - "$schema": "https://opencode.ai/config.json", - "mcp": { - "vestige": { - "type": "local", - "command": ["vestige-mcp"], - "enabled": true, - "timeout": 10000 - } - } -} -``` - -For isolated per-project memory, pass the data directory in the command array: - -```json -{ - "$schema": "https://opencode.ai/config.json", - "mcp": { - "vestige": { - "type": "local", - "command": ["vestige-mcp", "--data-dir", "./.vestige"], - "enabled": true, - "timeout": 10000 - } - } -} -``` - -See the [OpenCode integration guide](integrations/opencode.md) for global config, verification, and troubleshooting. - --- ## Custom Data Directory @@ -302,24 +167,9 @@ See [Storage Modes](STORAGE.md) for more options. vestige update ``` -This updates `vestige`, `vestige-mcp`, and `vestige-restore`. It does not mutate -Claude Code Cognitive Sandwich companion files unless you explicitly request it. - -**Also refresh optional Claude Code companion files:** -```bash -vestige update --sandwich-companion -``` - **Pin to specific version:** ```bash -vestige update --version v2.1.21 -``` - -**Manage the optional Cognitive Sandwich layer without updating binaries:** -```bash -vestige sandwich install -vestige sandwich install --enable-preflight -vestige sandwich install --enable-sanhedrin --sanhedrin-endpoint=http://127.0.0.1:11434/v1/chat/completions +vestige update --version v2.1.0 ``` **Check your version:** diff --git a/docs/CONNECTORS.md b/docs/CONNECTORS.md deleted file mode 100644 index a324784..0000000 --- a/docs/CONNECTORS.md +++ /dev/null @@ -1,206 +0,0 @@ -# External-Source Connectors - -> Status: **v2.1.27** — GitHub Issues + Redmine reference connectors, plus -> source-aware investigation filters for search. Tracking issue: -> [#57](https://github.com/samvallad33/vestige/issues/57). - -Connectors let Vestige act as a durable, local **retrieval and reasoning layer** -over a long-lived external system — a ticket tracker, an issue board, a support -queue — **without replacing it**. The external system stays the source of truth. -Vestige indexes its records, embeds them for semantic recall, links them into the -memory graph, and **cites back** to the canonical record. - -## Why this is different from a ticket-system MCP - -The official GitHub / Jira MCP servers are **live API proxies**: every query hits -the upstream API, is rate-limited, keyword-only, online-only, and has no memory -of past state. Vestige instead keeps a **durable local index** of the records, so -you can: - -- search the history **offline** and **semantically** (embeddings, not just - keywords), -- **join** ticket history with the rest of your memory in one search, -- see a **point-in-time** view (records carry temporal validity), -- and re-sync **idempotently** — re-running never duplicates a record. - -## Quick start (GitHub Issues) - -1. (Optional but recommended) export a token so you get the authenticated rate - limit (5,000 req/hr vs 60 for anonymous) and access to private repos: - - ```sh - export GITHUB_TOKEN=ghp_xxx # or VESTIGE_GITHUB_TOKEN - ``` - - The token is read **only** from the environment — never passed as a tool - argument, never logged. - -2. Ask your agent to run the `source_sync` MCP tool: - - ```json - { "repo": "samvallad33/vestige" } - ``` - -3. Search as normal. Connector-sourced results carry a `sourceRecord` object with - the canonical issue URL: - - ```json - { - "content": "[samvallad33/vestige#57] Roadmap: external source connectors …", - "sourceRecord": { - "system": "github", - "id": "57", - "url": "https://github.com/samvallad33/vestige/issues/57", - "project": "samvallad33/vestige", - "type": "issue", - "author": "samvallad33", - "tombstoned": false - } - } - ``` - -## Quick start (Redmine) - -Redmine stays the system of record; Vestige indexes a project's issues + -journals (comments and status/assignment history). - -1. Point Vestige at the Redmine host and key (env only, never tool args): - - ```sh - export REDMINE_URL=https://redmine.example.com - export REDMINE_API_KEY=xxxxxxxx # or VESTIGE_REDMINE_API_KEY - ``` - - The instance must have the REST API enabled (Administration → Settings → API) - or every call returns 401/403 even with a valid key. - -2. Run `source_sync`: - - ```json - { "source": "redmine", "project": "infra" } - ``` - - Results cite the canonical `https://redmine.example.com/issues/` URL. - -## The `source_sync` tool - -| Field | Type | Default | Meaning | -|---|---|---|---| -| `source` | string | `github` | `github` or `redmine`. | -| `repo` | string | — | **GitHub:** `owner/name`, e.g. `samvallad33/vestige`. | -| `project` | string | — | **Redmine:** project identifier (host from `REDMINE_URL`). | -| `reconcile` | bool | `false` | Also tombstone local memories for issues no longer visible upstream (an extra full-enumeration pass). | -| `max_pages` | int | `10` | API pages to fetch this run (≤100 issues each). Lets a first sync of a large project resume across calls. | - -The tool returns counts (`created` / `updated` / `unchanged` / `tombstoned`), -the saved `cursor`, whether it ran authenticated, and a `hint` for the next step. - -## Investigation filters (Phase 4) - -`search` accepts source-aware filters so an agent can scope a query to indexed -records. All are optional post-filters; combine with a larger `limit` if you -expect heavy thinning. A source-scoped query excludes non-connector memories. - -| Filter | Matches | -|---|---| -| `source_system` | `github`, `redmine`, … | -| `source_project` | repo / project (exact) | -| `source_id` | a specific issue/ticket id | -| `source_type` | `issue`, `comment`, … | -| `source_author` | reporter/author (not assignee) | -| `source_updated_after` / `source_updated_before` | RFC3339 date range (inclusive) | -| `source_status` | `valid` (default `any`) or `tombstoned` | - -Status, tracker, and priority are filterable through the existing `tag_prefix` -(the connectors emit lowercase `status:`, `tracker:`, `priority:`, and GitHub -`label:` / `state:` tags) — e.g. `tag_prefix: "status:open"`. Assignee and -linked-issue graph traversal are not yet exposed (see below). - -### Idempotent, incremental sync - -Each run: - -1. resumes from the saved cursor (the high-water mark on the record's upstream - update time), minus a small overlap window so same-second / clock-skewed - updates are never missed; -2. pages issues in ascending update order (`state=all`, so closing an issue is - **not** mistaken for a deletion), folding each issue + its comments into one - memory; -3. routes each record through an **idempotent upsert** keyed on - `(source_system, source_id)`: - - unseen record → **insert**, - - changed content (by content hash) → **update in place** + re-embed, - - unchanged content → **no-op** (only the "last seen" time advances); -4. advances and persists the cursor only after the run, so an interruption - re-scans rather than skips. - -Re-running `source_sync` on the same repo is therefore safe and cheap — it picks -up only what changed. - -### Deletions (tombstoning) - -Neither GitHub nor Redmine exposes a deletion feed, so an incremental sync can -never *see* a delete. Pass `reconcile: true` to run a reconciliation pass: Vestige -enumerates the currently-visible issue ids and **invalidates** (does not purge) -any local record no longer present. A tombstoned record keeps its content for -audit but drops out of "currently valid" retrieval (`sourceRecord.tombstoned` is -`true`). If the record reappears upstream, the next sync un-tombstones it. - -## The source envelope - -Every connector-ingested memory carries structured provenance, distinct from the -legacy free-form `source` label: - -| Field | Purpose | -|---|---| -| `source_system` | `github`, `redmine`, … (namespaces ids). | -| `source_id` | Native id (issue number, ticket id). | -| `source_url` | Canonical link back — the citation. | -| `source_updated_at` | Upstream update time (the sync cursor field). | -| `content_hash` | Change detector → idempotency. | -| `synced_at` | When the connector last saw the record live. | -| `source_project` | Repo / project / space. | -| `source_type` | `issue`, `comment`, … | -| `source_author` | Reporter / author upstream. | - -`(source_system, source_id)` is enforced unique, so there is exactly one memory -per external record. Legacy memories (agent- or user-authored) have no envelope -and are completely unaffected. - -## Building - -The connector HTTP client is behind the `connectors` cargo feature, which is -**on by default in the MCP server** (`vestige-mcp`). A build without it still -exposes the `source_sync` tool but returns a clear "rebuild with `--features -connectors`" message. The core library (`vestige-core`) leaves the feature -**off** by default, so library consumers that don't need connectors link no HTTP -client. - -```sh -# default MCP build already includes connectors -cargo build -p vestige-mcp --release - -# explicit, or for the core lib -cargo build -p vestige-core --features connectors -``` - -## Writing a new connector - -Implement the `Connector` trait in `vestige_core::connectors` (fetch a window of -records updated since a cursor, page forward, and optionally enumerate live ids -for reconciliation), produce `NormalizedRecord`s with a filled -`SourceEnvelope`, and hand them to `run_sync`. Two reference connectors show the -shape — `crates/vestige-core/src/connectors/github.rs` (Link-header pagination, -opaque-url cursor) and `crates/vestige-core/src/connectors/redmine.rs` -(offset pagination, two-phase list-then-detail fetch). The sync driver, -idempotent upsert, cursor checkpointing, and tombstone reconciliation are all -reused for free. - -## Not yet supported - -- **Assignee filter** — the envelope stores `source_author` (reporter) only; no - assignee column yet. -- **Tracker / version dedicated filter params** — reachable today via - `tag_prefix` (`tracker:`, and `version:`/`category:` when emitted). -- **Linked-issue graph traversal** — connectors import relations into the memory - body, but issue-to-issue graph edges are not yet exposed in search. diff --git a/docs/FAQ.md b/docs/FAQ.md index 5f93f25..a5a8162 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -22,13 +22,13 @@ ## Getting Started
                    -"Can Vestige support multiple agents or MCP clients?" +"Can Vestige support a two-Claude household?" -**Yes.** See [Storage Modes](STORAGE.md#option-3-multi-agent-household). You can either: -- **Share memories**: Multiple agents point to the same `--data-dir` -- **Separate identities**: Each agent gets its own data directory +**Yes!** See [Storage Modes](STORAGE.md#option-3-multi-claude-household). You can either: +- **Share memories**: Both Claudes point to the same `--data-dir` +- **Separate identities**: Each Claude gets its own data directory -For two agents with distinct roles sharing the same human, use separate directories but consider a shared "household" memory for common knowledge. +For two Claudes with distinct personas (e.g., "Domovoi" and "Storm") sharing the same human, use separate directories but consider a shared "household" memory for common knowledge.
                    @@ -38,28 +38,28 @@ For two agents with distinct roles sharing the same human, use separate director **For non-technical users:** 1. Have a technical friend do the 5-minute install -2. Add the [agent memory protocol](AGENT-MEMORY-PROTOCOL.md) to your MCP client's instruction file -3. Just talk normally; the agent handles the memory calls +2. Add the CLAUDE.md instructions +3. Just talk to Claude normally—it handles the memory calls -**The magic**: Once set up, you never think about it. Your agent just remembers. +**The magic**: Once set up, you never think about it. Claude just... remembers.
                    "What input do you feed it? How does it create memories?" -Your agent creates memories via MCP tool calls. Three ways: +Claude creates memories via MCP tool calls. Three ways: -1. **Explicit**: You say "Remember that I prefer dark mode" -> the agent calls `smart_ingest` -2. **Automatic**: The agent notices something important -> calls `smart_ingest` proactively -3. **Codebase**: The agent detects patterns/decisions -> calls `codebase(action="remember_pattern")` or `codebase(action="remember_decision")` +1. **Explicit**: You say "Remember that I prefer dark mode" → Claude calls `smart_ingest` +2. **Automatic**: Claude notices something important → calls `smart_ingest` proactively +3. **Codebase**: Claude detects patterns/decisions → calls `remember_pattern` or `remember_decision` -The agent memory protocol tells the client when to create memories proactively. +The CLAUDE.md instructions tell Claude when to create memories proactively.
                    "Can it be filled with a conversation stream in realtime?" -Not currently. Vestige is **tool-based**, not stream-based. The agent decides what's worth remembering, not everything gets saved. +Not currently. Vestige is **tool-based**, not stream-based. Claude decides what's worth remembering, not everything gets saved. This is intentional—saving everything would: - Bloat the knowledge base @@ -160,8 +160,6 @@ Accessibility is calculated as: `0.5 × retention + 0.3 × retrieval_strength + Memories are never deleted automatically. They fade from relevance but can be revived if accessed again (like human memory—"oh, I forgot about that!"). -If you explicitly want content gone, use `memory(action="purge", confirm=true)`. Purge permanently removes the memory content and embeddings, scrubs internal references, and keeps only a non-content tombstone so sync/audit can prove the deletion happened. - **To configure decay**: The FSRS-6 algorithm auto-tunes based on your usage patterns. Memories you access stay strong; memories you ignore fade. No manual tuning needed.
                    @@ -211,9 +209,11 @@ In Vestige's current implementation: In Vestige's implementation: ``` -importance_score( - content="the-important content", - context_topics=["release", "memory"] +importance( + memory_id="the-important-one", + event_type="user_flag", # or "emotional", "novelty", "repeated_access", "cross_reference" + hours_back=9, # Look back 9 hours (configurable) + hours_forward=2 # Capture next 2 hours too ) ``` @@ -328,9 +328,9 @@ The unified `search` always uses hybrid, which gives you the best of both worlds Three approaches: -1. **Mark as important**: `importance_score(content="...", event_type="user_flag")` +1. **Mark as important**: `importance(memory_id="xxx", event_type="user_flag")` 2. **Access regularly**: The Testing Effect strengthens memories each time you retrieve them -3. **Promote explicitly**: `memory(action="promote", id="xxx")` after it proves valuable +3. **Promote explicitly**: `promote_memory(id="xxx")` after it proves valuable For truly critical information, consider also: - Using specific tags like `["critical", "never-forget"]` @@ -547,13 +547,13 @@ Common issues: | Feature | Notes App | Vestige | |---------|-----------|---------| -| Retrieval | You search manually | The agent searches contextually | +| Retrieval | You search manually | Claude searches contextually | | Decay | Everything stays forever | Unused knowledge fades naturally | | Duplicates | You manage manually | Prediction Error Gating auto-merges | | Context | Static text | Active part of AI reasoning | | Strengthening | Manual review | Automatic via Testing Effect | -The key difference: **Vestige is part of the agent's cognitive loop.** Notes are external reference; Vestige is active working memory. +The key difference: **Vestige is part of Claude's cognitive loop.** Notes are external reference—Vestige is internal memory.
                    @@ -617,7 +617,7 @@ Why Nomic: - No API costs or rate limits - Fast enough for real-time search -The model is cached in the platform user cache directory first, with `./.fastembed_cache` as a fallback. Set `FASTEMBED_CACHE_PATH` to choose a specific cache path. +The model is cached at `~/.cache/huggingface/` after first run.
                    @@ -782,16 +782,22 @@ This helps trace why you know something.
                    "What's planned for future versions?" -See the public [Vestige Roadmap](ROADMAP.md) for the current adoption plan. The -near-term focus is reducing first-user confusion before expanding the feature -surface: +Based on codebase exploration, these features exist in various stages: -- first-time memory migration and atomic memory guidance -- configurable MCP output fields and output profiles -- clearer merge/supersede controls -- code/docstring memory workflows -- goals and milestones distinct from intentions -- guided import dry runs and review queues +| Feature | Status | Description | +|---------|--------|-------------| +| Memory Dreams | Partial | Automated offline consolidation | +| Reconsolidation | Planned | Update memories when accessed | +| Memory Chains | Partial | Link related memories explicitly | +| Adaptive Embedding | Planned | Re-embed old memories with better models | +| Cross-Project Learning | Planned | Share patterns across codebases | + +**Community wishlist** (from Reddit): +- Stream ingestion mode +- GUI for memory browsing +- Export/import formats +- Sync between devices (encrypted) +- Team collaboration features Contributions welcome!
                    @@ -809,11 +815,11 @@ See [CLAUDE-SETUP.md](CLAUDE-SETUP.md) for the full template. The key elements: **During Work**: - Notice a pattern? `codebase(action="remember_pattern")` - Made a decision? `codebase(action="remember_decision")` with rationale -- Something important? `importance_score(content="...")` to score it before saving or promoting +- Something important? `importance()` to strengthen recent memories **Memory Hygiene**: -- When a memory helps: `memory(action="promote", id="...")` -- When a memory misleads: `memory(action="demote", id="...")` +- When a memory helps: `promote_memory` +- When a memory misleads: `demote_memory`
                    --- diff --git a/docs/GETTING-STARTED.md b/docs/GETTING-STARTED.md deleted file mode 100644 index eeccbd6..0000000 --- a/docs/GETTING-STARTED.md +++ /dev/null @@ -1,126 +0,0 @@ -# Getting Started with Vestige - -Your first 30 minutes, start to finish. By the end you'll have Vestige installed, -connected to your agent, storing memories, and you'll know how to look at exactly -what it kept. - -Vestige is local-first: one binary, your data on your disk, no account, no cloud. - ---- - -## 1. Install and connect (5 minutes) - -Install is one command and connecting is one JSON block. The canonical, always-current -steps live in the README so this guide never drifts from them: - -- **[Install + connect →](../README.md#-get-it-running-in-60-seconds)** -- Using a specific editor? Pick your per-agent guide — - [Cursor](integrations/cursor.md), [VS Code](integrations/vscode.md), - [Windsurf](integrations/windsurf.md), [JetBrains](integrations/jetbrains.md), - [Xcode](integrations/xcode.md), [OpenCode](integrations/opencode.md), - [Codex](integrations/codex.md) — or the - **[Claude Desktop 2-minute setup](CONFIGURATION.md#claude-desktop-macos)**. - -Confirm it's alive: - -```bash -vestige-mcp --version # prints the installed version -vestige stats # 0 memories on a fresh install -``` - ---- - -## 2. What Vestige saves (and what it doesn't) - -This is the thing most people get wrong on day one, so it's worth 60 seconds. - -**Vestige does not record everything you type.** It is not a transcript logger. A -memory is written when: - -- **You ask your agent to remember something** ("remember: we disable SimSIMD on - release builds"), or -- **Your agent decides a fact is worth keeping** and calls the memory tool, or -- **You ingest deliberately** from the CLI: `vestige ingest "..."`. - -Every write goes through **prediction-error gating** — near-duplicates of what you -already know are down-weighted, so the store doesn't fill with restatements of the -same fact. What you get is a curated set of durable facts, decisions, and the -occasional "this didn't work," not a firehose of your chat history. - -If you want the deeper model (FSRS-6 decay, spreading activation, the science), see -**[The Science](SCIENCE.md)**. You don't need it to start. - ---- - -## 3. Try the one thing nothing else does - -The headline feature is a backward reach through time. Store a decision, then later -store a failure, and Vestige can surface the earlier decision as the *cause* — even -though the two share no words. - -```bash -vestige ingest "We switched the prod cache from Redis to an in-memory LRU to cut costs." -# ...later... -vestige ingest "Prod is dropping sessions under load and users are getting logged out." - -vestige backfill --contrast -``` - -`--contrast` shows you, side by side, what a plain similarity search returns versus -the real causal cause. That contrast is the whole pitch — more on the mechanism in -**[the README](../README.md#-the-thing-nothing-else-does-memory-with-hindsight)**. - -You can also just talk to your agent normally; it will write and recall memories for -you through MCP. The CLI is for when you want to drive it directly. - ---- - -## 4. Inspect what's stored - -Vestige is built to be looked at. Nothing is hidden in an opaque index. - -- **Quick counts:** `vestige stats` -- **Health check** (coverage, warnings): `vestige health` -- **Recall + reasoning** over your memories: `vestige recall "your question"` -- **Full export** to JSON/JSONL (grep it, diff it, back it up): - `vestige export memories.jsonl --format jsonl` -- **The 3D dashboard** — watch your memory as a living graph: - - ```bash - vestige dashboard # opens http://localhost:3927 - ``` - - (When running as the MCP server, enable it with `VESTIGE_DASHBOARD_ENABLED=1`.) - -Because the store is a single SQLite database, you can also open it with any SQLite -browser if you want the raw truth. - ---- - -## 5. Scope it: global vs per-project - -By default Vestige uses one global memory in your OS data directory. For a -project-local memory that lives with the repo, point it at a directory: - -```bash -vestige stats --data-dir ./.vestige # this project's memory -VESTIGE_DATA_DIR=./.vestige vestige-mcp # run the server against it -``` - -Precedence is `--data-dir` > `VESTIGE_DATA_DIR` > OS per-user default. Full details, -including multi-instance setups, are in **[Storage Modes](STORAGE.md)** and -**[Configuration](CONFIGURATION.md)**. - ---- - -## Where to go next - -| Want to… | Read | -|---|---| -| Understand a specific feature or the research | [The Science](SCIENCE.md) | -| Tune knobs, env vars, ports | [Configuration](CONFIGURATION.md) | -| Global vs per-project vs multi-instance | [Storage Modes](STORAGE.md) | -| Make your agent use memory automatically | [README → automatic memory](../README.md#-make-your-ai-use-memory-automatically) | -| Ask a real question | [FAQ](FAQ.md) | - -Welcome. Vestige gets more useful the longer you use it — that's the point. diff --git a/docs/INSTALL-INTEL-MAC.md b/docs/INSTALL-INTEL-MAC.md index 3ec02e0..ee42975 100644 --- a/docs/INSTALL-INTEL-MAC.md +++ b/docs/INSTALL-INTEL-MAC.md @@ -17,8 +17,9 @@ brew install onnxruntime ## Install ```bash -# 1. Install the binary -npm install -g vestige-mcp-server@latest +# 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 diff --git a/docs/MEMORY_CINEMA.md b/docs/MEMORY_CINEMA.md deleted file mode 100644 index 6947ce0..0000000 --- a/docs/MEMORY_CINEMA.md +++ /dev/null @@ -1,172 +0,0 @@ -# Memory Cinema — Complete Feature Reference - -Memory Cinema turns your real memory graph into a directed, narrated, infinitely- -diving cinematic experience rendered as a 150,000-particle WebGPU compute storm. -It is the dashboard's signature pillar. - -The whole thing is **dynamically imported only on launch** — the heavy WebGPU/TSL -bundles never load for normal dashboard use. It boots a **separate WebGPU canvas** -so the underlying WebGL graph (every current user's experience) is never touched — -zero regression by construction. - -## Architecture — the 8 modules - -| Module | Role | -|---|---| -| `pathfinder.ts` | Plans the narrative path through the real graph (a story, not a BFS dump) | -| `topology.ts` | Extracts graph signals (betweenness, contradictions, surprise, decay) | -| `auteur.ts` | The director's brain — a typed cinematography grammar + shot-plan contract | -| `narrator.ts` | Captions — 3-tier narration (LLM / local / deterministic) | -| `director.ts` | The camera runtime — executes the shot plan frame-by-frame | -| `sandbox.ts` | The isolated WebGPU stage — renderer, camera, selective bloom | -| `storm.ts` | The 150k-particle GPU compute storm — physics, geometry, color, immersion | -| `components/MemoryCinema.svelte` | The orchestration overlay — input, dream mode, UI | - -## Tier 1 — the narrative path (`pathfinder.ts`) - -The bulletproof core that always runs, using only the nodes + edges the backend -returns. It plans a story: start at the origin (focused memory) → visit its -strongest-weighted connections → detour to a contradiction edge if one exists -(tension = interesting) → end on a recently-created node ("where the mind is now"). -Each stop is a beat with a `kind` (origin/connection/contradiction/recent/bridge/ -surprise). Falls back to weighted BFS. No LLM, no WebGPU, no network — if -everything else fails this alone produces a coherent, watchable flythrough. - -## Tier 2 — graph signal extraction (`topology.ts`) - -Pure statistics over the real `/api/graph` data, computed once per launch: -- **Brandes betweenness centrality** — the most load-bearing memory (graph keystone) -- **Connected-component clustering** -- **Contradiction detection** + **merge/supersede detection** -- **Surprise score** (shared neighbors yet low edge weight = non-obvious link) -- **Recency rank**, **FSRS retention**, **suppression pressure** - -## The Auteur — the director's brain (`auteur.ts`) - -A real cinematography grammar applied to your memories. The director (LLM Tier-1 or -a deterministic rule table Tier-2) produces a `DirectorPlan`: typed `Shot`s, each -grounded in a real node and justified by a real metric. -- **Moves:** push_in, pull_back, orbit, crane, whip_pan, rack_focus, hold -- **Angles:** low = look up (power), high = look down (decay), eye -- **Cuts:** fly, hard_cut, match_cut -- **Per shot:** dutch roll, standoff, flight/dwell seconds, spring half-life, - intensity, tension (0–1) -- **Three acts** (I/II/III), an **emotional arc** (man_in_hole, rags_to_riches, - icarus, cinderella, oedipus, flat), a **caption tone**, a **score cue** -- A **required `why`** per shot citing the real graph metric -- **Carry-forward semantics:** sparse/half-hallucinated plans always resolve to a - coherent film. - -## Narration (`narrator.ts`) - -- **Tier 1:** backend LLM (`/api/narrative`) or opt-in on-device model (lazy-loaded - only when "Local AI" enabled; never downloads weights unprompted). -- **Tier 2:** deterministic structured captions from real data — instant, no network. -- **Tier 3:** can't fail — falls back to Tier 2. -- Optional voice via `speechSynthesis`; typewriter caption stream (instant under - reduced-motion). - -## The 150k-particle storm (`storm.ts`) - -150,000 particles whose physics run entirely on the GPU via Three Shading Language -compute nodes. One particle pool, one compute kernel, ~32 uniforms. - -### The 7-beat world journey -Each beat is a unique world — particles aren't swapped, only the forces (uWorld -selects; uBlend crossfades over ~1s): -0 nebula mist (curl-noise flow) · 1 orbital anchor (cross-product spin) · 2 strange -attractor (Thomas) · 3 detonation void · 4 crystal lattice (voxel snap) · 5 fluid -galaxy (curl + swirl) · 6 phyllotaxis bloom (Vogel sunflower). - -### The impossible-geometry form pack (dream worlds 7–11) -Forms nobody renders as living particles, mapped over a (u,v) manifold grid (387²) -so they read as a sculpted skin, not spaghetti: -7 supershape (3D superformula) · 8 Calabi–Yau (6D string-theory manifold, 4D→3D -projection; α rotates through the 4th dimension) · 9 Boy's surface (non-orientable -minimal immersion) · 10 Aizawa attractor shell · 11 gyroid↔Schwarz-D (triply- -periodic minimal surface morph). Inline complex-math + hyperbolics (sinh/cosh -expanded via exp; absent in three@0.172). - -### Color -- Full-spectrum iridescent palette (per-particle phase + radial shells + spatial - bands + time + global drift). -- Per-world cosine (IQ) palettes — each world a distinct identity. -- **The Color Blast:** a long-lived uBlast envelope (~2.8s, decoupled from the fast - physics burst so color outlives the shockwave) drives an outward spectral- - dispersion wave (uBlastTime; prism order) over a blackbody ember core. Spectrum - dominates (reads rainbow, not white); capped at 0.6 mix. -- **Jarring inner/outer clash:** the nested figure and shell use opposing color - universes (ice↔fire, acid↔blood, gold↔violet, mint↔crimson, electric↔gold); - uClash cycles the pair every beat. - -### 3D-within-3D nesting -~34% of particles form a second, smaller, counter-rotating figure (a different -world, ~52% scale) inside the outer shell — a figure within a figure. - -### Anti-white-out / "solid" systems -- Rim glow (Fresnel): bright edges, dim readable center. -- Emissive routing: the rainbow goes to BOTH colorNode and emissiveNode (the - selective bloom reads emissive — the original white-out was an unset emissive). -- Hollow-shell spawn (the old tiny dense ball flashed white on frame 0). -- Act/beat-aware brightness (uActDim): beats 0/1 fade in soft, Acts II/III blaze. - -### The immersion stack -- **Infinite Droste zoom** (uZoomPeriod/uLambda/uZoomOn): the cloud dives inward - forever. Two layers ride offset phases of fract(uTime/T); the outer grows - pow(λ, phase) then snaps back invisibly (λ=1.923=1/0.52 makes inner→outer exact); - the inner promotes by λ to become the next shell; a fresh inner spawns inside. A - sin(phase·π) seam cross-fade in rimFactor makes each layer transparent at its - snap → zero pop, seamless loop. Particle-space, not a camera dolly. -- **Velocity-stretch flythrough** (uCamVelView/uStreak/uMaxStretch): sprites stretch - along screen-space camera velocity into warp streaks; the camera clamp floor - relaxes (30→6) so it plunges inside the shell. -- **DOF + volumetric fog** (uFocus/uFocusRange/uDofDim/uFogDensity): off-focus - particles dim (bokeh under bloom) + exp-falloff fog tints the far field → 3D - atmospheric depth; rack-focus tracks the dive. All folded into a single depthFade - Fn (three@0.172 constraint: positionView may be read from only ONE Fn per - material output or the node-builder stack-overflows). -- **Interactive parallax** (overlay): pointer orbits, scroll/pinch zooms, damped; - composed onto the director's base pose; idle eases back to 0; the sandbox re-clamp - means framing can't break. - -### Physics safety -Hard velocity clamp, strong damping, boundary snap, dt clamp, serialized compute -dispatches. - -## Endless dream mode - -When the 7-beat tour ends, instead of freezing it drops into an infinite generative -loop (`dreamBeat()` every ~5.5s): a fresh random procedural figure (worlds 7–11, new -uMorphSeed → never the same twice), uChaos ramps up so each is wilder than the last, -a random clash pair, full color blast, infinite zoom + flythrough on. Overlay shows -"∞ Dreaming." Never idle. - -## The stage (`sandbox.ts`) - -- Separate WebGPU renderer + scene + PerspectiveCamera (clamped to a safe distance - band, always lookAt(origin) → the storm can never leave frame). -- Selective MRT bloom — blooms only the emissive channel against a clean void; falls - back to a plain pass if MRT is unavailable on a driver. -- Per-frame camera-velocity tracking (one Vector3, zero compute) feeding the streak. -- Pass-throughs: setZoom, setStreak, setFlythrough, setCameraVel, setContainRadius. - -## Overlay & UX (`MemoryCinema.svelte`) - -- Fullscreen launch, staged status (idle → planning → playing → done). -- Pre-roll "Director's Plan" card (logline + arc). -- Live captions, beat chips, director's-note ("why," citing a real metric), act - indicator, tension sparkline, progress bar. -- WebGPU / Auteur / Live-captions badges; Voice + Local AI toggles. -- `H` hides all chrome for clean demo capture (faint restore hint; `body.cinema-open` - hides the graph page's stats pill; overlay z-index 200). -- Replay on completion. -- Graceful degradation: if WebGPU is unavailable or render fails 3× consecutively, - drops to camera-only (captions still play) — never stalls. -- Reduced-motion fully honored: no parallax listeners, zoom/flythrough/streak gated - off, jump-cuts instead of flights, instant captions. - -## Protection contract - -On the shared graph page, `` is wrapped in a clearly-commented -`PROTECTED MEMORY CINEMA — DO NOT MODIFY` boundary so the graph-control overhaul and -the cinema engine stay visibly distinct. diff --git a/docs/MERGE_SUPERSEDE.md b/docs/MERGE_SUPERSEDE.md deleted file mode 100644 index a35fb06..0000000 --- a/docs/MERGE_SUPERSEDE.md +++ /dev/null @@ -1,152 +0,0 @@ -# Merge / Supersede Controls (Phase 3) - -> Diff-previewed, confidence-gated, reversible, self-explaining -> combine/dedupe/supersede on a never-delete (bitemporal) store. - -Memory systems accumulate duplicates, near-duplicates, and outdated facts. The -naive fixes are all bad: dumb hashing under-merges (misses paraphrases), -aggressive LLM merging over-merges and destroys the audit trail, and -auto-deleting on contradiction silently loses information. Vestige's Phase 3 -takes the opposite stance: - -- **Opt-in, never silent.** The default is preview/review. Nothing mutates your - memory unless you explicitly apply a plan. -- **Diff-previewed.** `plan_merge` / `plan_supersede` show exactly what *would* - change before anything does. -- **Confidence-gated.** A Fellegi-Sunter two-threshold score classifies each - candidate as `match` / `possible` / `non_match`. -- **Reversible.** Every applied operation is recorded with an undo payload — a - *git reflog for your agent's memory*. -- **Self-explaining.** Each candidate carries the signals that explain *why* two - memories were judged duplicates. -- **Audit-preserving.** Superseding does not delete: it stamps `valid_until` and - keeps the old memory queryable (Graphiti-style "invalidate, don't delete"). - -## The bitemporal model: invalidate, don't delete - -Superseding memory A with memory B does **not** erase A. Instead: - -- `A.valid_until` is stamped with the supersede time. -- `A.superseded_by` is set to `B.id` (a lineage pointer). -- A remains fully queryable for audit. Searches and timelines can still surface - it; it is simply marked as no longer the current truth. - -This reuses the existing `valid_from` / `valid_until` columns on -`knowledge_nodes` (migration V2) plus a new `superseded_by` column (migration -V14). Merges work the same way: the survivor absorbs the others' content, and -each absorbed node is bitemporally invalidated rather than deleted. - -## Fellegi-Sunter two-threshold scoring - -Candidate scoring combines three signals into a weighted score in `[0, 1]`: - -| Signal | Weight | Source | -| ----------------------- | -----: | ------------------------------------------ | -| Embedding cosine sim | 0.70 | stored embeddings (`node_embeddings`) | -| Tag overlap (Jaccard) | 0.15 | `knowledge_nodes.tags` | -| Content token overlap | 0.15 | Jaccard over content tokens (len > 2) | - -The combined score is then classified against **two** thresholds: - -``` -score >= match_threshold => "match" (auto-merge eligible) -possible_threshold <= score => "possible" (surfaced for review) -score < possible_threshold => "non_match" (never offered) -``` - -Defaults: `match_threshold = 0.86`, `possible_threshold = 0.72`. The two-band -design means borderline cases are surfaced for review instead of being -force-decided in either direction. - -A cluster's confidence is the **weakest** pairwise score within it (the loosest -link), so a cluster is only as confident as its least-similar member. - -## The reversible operation log (the "memory reflog") - -Every applied merge/supersede writes one row to `merge_operations`: - -- `op_type` — `merge` | `supersede` | `undo` -- `status` — `applied` | `reverted` -- `survivor_id`, `affected_ids` — what was touched -- `confidence`, `signals` — the score and *why* the memories combined -- `reason` — a human-readable explanation -- `undo_payload` — a JSON snapshot capturing everything needed to reverse it - -`merge_undo` consumes the undo payload to restore the survivor's prior -content/tags and clear the bitemporal invalidation on every affected node, then -records a compensating `undo` operation. Calling `merge_undo` with no -`operation_id` returns the operation log so you can pick one. - -## Memory protection (pinning) - -`protect` sets the `protected` flag on a memory. A protected memory: - -- is never offered for auto-merge (it is flagged in `merge_candidates`), -- cannot be merged *away* (it may only be the survivor of a merge), -- cannot be superseded, -- is excluded from garbage collection. - -Pass `protected: false` to unpin. - -## Tool surface - -| Tool | Mutates? | Purpose | -| ------------------ | :------: | ------------------------------------------------------------------------- | -| `merge_candidates` | No | Surface likely duplicate clusters with confidence + signals. | -| `plan_merge` | No | Preview a merge of 2+ memories (a diff). Returns a `plan_id`. | -| `plan_supersede` | No | Preview superseding A with B (bitemporal). Returns a `plan_id`. | -| `apply_plan` | **Yes** | Execute a plan by id; recorded as a reversible operation. | -| `merge_undo` | **Yes** | Reverse an operation, or list the operation log when given no id. | -| `protect` | **Yes** | Pin / unpin a memory so it can never be auto-merged/superseded/forgotten. | -| `merge_policy` | **Yes** | Get/set the two thresholds + `auto_apply`. | - -### Typical flow - -```text -1. merge_candidates -> review clusters + confidence + signals -2. plan_merge { member_ids: [...] } -> inspect the diff, get plan_id -3. apply_plan { plan_id, confirm } -> apply; get operation_id (reversible) -4. merge_undo { operation_id } -> reverse if it was wrong -``` - -`apply_plan` requires `confirm: true` for `possible` / `non_match` plans. A -`match` plan applies without `confirm` only when the policy has -`auto_apply: true` (default `false`). - -## Configuration - -The merge policy persists per project (stored in `fsrs_config`). It can also be -overridden via environment variables: - -| Variable | Meaning | -| ----------------------------------- | ------------------------------------ | -| `VESTIGE_MERGE_MATCH_THRESHOLD` | Score ≥ this ⇒ `match`. | -| `VESTIGE_MERGE_POSSIBLE_THRESHOLD` | Score ≥ this ⇒ at least `possible`. | -| `VESTIGE_MERGE_AUTO_APPLY` | `1`/`true` to allow auto-apply. | - -A persisted policy (set via `merge_policy`) takes precedence over the -environment, which takes precedence over the built-in defaults. When -`vestige.toml` configuration lands, the policy will read from there as well. - -## Schema (migration V14) - -- `knowledge_nodes.protected INTEGER NOT NULL DEFAULT 0` -- `knowledge_nodes.superseded_by TEXT` -- `merge_plans(id, kind, status, created_at, applied_at, survivor_id, - member_ids, confidence, classification, payload)` -- `merge_operations(id, plan_id, op_type, status, created_at, reverted_at, - reverts_op_id, survivor_id, affected_ids, confidence, signals, reason, - undo_payload)` - -The two `ALTER TABLE ... ADD COLUMN` statements are applied with duplicate-column -guards so the migration is idempotent on replay; the rest of V14 uses -`CREATE ... IF NOT EXISTS`. - -## Anti-patterns this design avoids - -- **Silently double-storing contradictions.** Merge composition attributes and - de-duplicates content instead of blindly concatenating or dropping it. -- **Auto-deleting on contradiction.** Supersede invalidates bitemporally; the - old memory is retained and queryable. -- **Trading away the audit trail for auto-merge convenience.** Every operation is - logged and reversible, with provenance for why memories combined. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md deleted file mode 100644 index b435271..0000000 --- a/docs/ROADMAP.md +++ /dev/null @@ -1,204 +0,0 @@ -# Vestige Roadmap - -> Public adoption roadmap for making Vestige easier to start, easier to trust, -> and easier to configure. - -Last updated: June 7, 2026 - -Vestige already has the core primitives for durable local memory: `search`, -`session_context`, `smart_ingest`, `memory`, `intention`, `codebase`, -`deep_reference`, suppression, portable storage, and the dashboard. The next -product step is reducing first-user confusion so more people can get value from -those primitives without inventing their own fragile memory vocabulary. - -This roadmap turns early community feedback into a staged plan. - -## Principles - -- Make first use obvious. A new user should know what to import, how atomic each - memory should be, and which tool to use for current session context. -- Keep memory legible. Agents and humans should understand whether a memory was - created, reinforced, updated, superseded, suppressed, or purged. -- Prefer progressive disclosure. The default MCP response should be lean, with - explicit ways to request more detail. -- Keep local-first behavior. New onboarding, code memory, and configuration - features must not require a cloud service. -- Optimize for many users. Defaults should work for non-experts, while power - users can tune fields, merge behavior, and formats. - -## Already Shipped, Needs Clearer Guidance - -| Area | Current State | Next Documentation Fix | -|------|---------------|------------------------| -| Session startup | `session_context` combines memories, intentions, status, predictions, and codebase context. | Update all agent setup templates to make `session_context` the default startup call. | -| Batch memory saves | `smart_ingest` batch mode defaults to `batchMergePolicy="force_create"` so caller-separated items stay separate. | Document when to use batch force-create vs smart merge. | -| Device migration | `portable-export`, `portable-import`, and `sync` preserve exact Vestige storage state. | Separate device migration from first-time document import so users do not confuse them. | -| Supersede semantics | Supersede demotes the old memory and creates a new one; it does not purge the old memory. | Add plain-language vocabulary for create, update, supersede, suppress, demote, and purge. | - -## Phase 1: Onboarding And Memory Hygiene - -Target: make the first 30 minutes with Vestige hard to mess up. - -| Work | Outcome | -|------|---------| -| First-time memory migration guide | Users can import notes/docs without Claude tagging everything as `verified` or flattening unrelated facts together. | -| Atomic memory guide | Clear examples for one fact, one preference, one decision, one bug fix, one source note, and one code pattern per memory. | -| Default tag vocabulary | Recommended tags for source quality, confidence, project, type, urgency, and lifecycle without overloading words like `verified`. | -| Smart vs force-create guide | Agents know when to use `forceCreate`, `batchMergePolicy="force_create"`, or normal PE gating. | -| Updated agent templates | Claude, Codex, Cursor, VS Code, Xcode, OpenCode, JetBrains, and Windsurf templates start with `session_context` and use the same memory vocabulary. | - -Planned docs: - -- `docs/MIGRATION.md` -- `docs/MEMORY-HYGIENE.md` -- revised `docs/AGENT-MEMORY-PROTOCOL.md` -- revised `docs/CLAUDE-SETUP.md` - -## Phase 2: Configurable Output - -Target: let users control context cost without losing important evidence. - -| Work | Outcome | -|------|---------| -| Field masks for MCP results | Users can drop fields they never want in model context, such as temporal hints, scores, or timestamps. | -| Output profiles | Presets like `lean`, `default`, `audit`, and `research` tune result size and metadata detail. | -| Markdown output mode | Users can request compact Markdown summaries when that is more context-efficient than JSON. | -| Context reinstatement controls | `contextReinstatement` becomes opt-in or configurable, and temporal hints are based on stored memory context when available. | -| Per-tool defaults | Users can define default detail level, result limit, and response shape for search, timeline, codebase, and session context. | - -Likely implementation paths: - -- config file under the active Vestige data directory -- environment-variable override for simple deployments -- MCP parameters still win over defaults for one-off calls - -## Phase 3: Merge And Supersede Controls - -Target: make memory mutation predictable. - -| Work | Outcome | -|------|---------| -| Merge policy configuration | Users can keep some tags or node types atomic while allowing others to merge. | -| Prediction Error threshold knobs | Advanced users can tune create/update/reinforce boundaries without recompiling. | -| Merge previews before mutation | Agents can show what would change before updating an existing durable memory. | -| Safer consolidation dedup | Consolidation respects user-configured atomic tags and source boundaries. | -| Friendlier lifecycle labels | Agent-facing copy explains that superseded memories are old versions, not destroyed records. | - -## Phase 4: Code Memory - -Target: make code memories useful without blending source code, docstrings, and -human project notes into one noisy search space. - -| Work | Outcome | -|------|---------| -| Code memory import guide | Developers know when to save patterns/decisions versus code entities or docstrings. | -| Exposed code entity workflow | The existing core `CodeEntity` concept becomes usable through MCP or CLI. | -| Docstring/code symbol ingestion | Users can ingest functions, types, modules, docstrings, and call-site notes with source file provenance. | -| Code/prose retrieval separation | Search can filter or rank code memories separately from user preferences and project decisions. | -| Codebase dashboard review | Developers can inspect imported code memories and remove noisy entries. | - -## Phase 5: Goals And Milestones - -Target: support durable direction without pretending every future task is just a -reminder. - -| Work | Outcome | -|------|---------| -| Goal primitive | Non-fading, manually pivoted goals that survive normal memory decay. | -| Milestone tracking | Goals can have milestones, status, evidence, and blockers. | -| Goal-aware session context | `session_context` can include active goals when relevant. | -| Manual pivot semantics | Agents can update goals only when the user explicitly pivots, completes, or cancels them. | -| Dashboard surface | Users can inspect active, completed, paused, and cancelled goals. | - -This is distinct from `intention`: intentions are reminders triggered by time, -topic, file, event, or context. Goals are longer-lived direction and should not -fire as reminders unless the user attaches an intention. - -## Phase 6: Guided Import Tools - -Target: turn "I have 300 notes" into a reliable workflow. - -| Work | Outcome | -|------|---------| -| Import dry run | Vestige previews proposed memories, tags, node types, and merge decisions before writing. | -| Source-aware import | Imported memories keep file/source provenance and confidence metadata. | -| Chunking strategies | Users choose atomic facts, section summaries, decision records, or source notes. | -| Review queue | Users can approve, edit, split, merge, or reject proposed memories. | -| Post-import health pass | Vestige recommends consolidation, duplicate review, or tag cleanup after import. | - -## Tracked Issues (Consolidated 2026-07-02) - -The following roadmap issues were consolidated here and closed so the issue tracker -reflects active work, not a standing backlog. Nothing is lost — each entry keeps its -scope, the backend anchors that already exist, and why it is deferred. Most are -deferred behind the dashboard focus; several are security/data-integrity boundaries -that are deliberately *not* shipped half-done. - -### A. Reliability & Trust surfaces - -- **Agent Reliability Record** (was #84) — Unify traces, receipts, contradictions, - and composed-graph events into one per-run record with 5 evidence states - (supported / missing / stale / contradicted / suppressed) + Markdown export. - Backend already exists (`crates/vestige-core/src/trace/`). Remaining work is the - dashboard record view — see the dashboard Discussion. -- **Trust Zones + Memory Quarantine** (was #85) — Provenance/trust class on nodes, - score-capping for weak-provenance content, quarantine of untrusted sources. - Security boundary; unsafe half-done, and depends on ACL Memory primitives that - don't exist yet. Deferred post-dashboard. -- **ComposeBench** (was #86) — Reliability benchmark across 8 scenarios. Will reuse - the existing `benchmarks/causebench/` harness pattern; the ACL scenario is gated - on ACL Memory. Deferred. - -### B. Access & Governance boundary - -- **ACL Memory for source-aware connectors** (was #82) — Source-authorization-aware - memory: connector-ingested memories preserve upstream access rules and retrieval - fails closed for unauthorized callers. A hard security boundary with no foundation - today (no per-caller identity model; `search()` takes no subject). Must be designed - as one deliberate pass, not sliced. Design + user-permission-shape input welcome in - the Discussion. -- **Team Pro Reliability Foundation** (was #92) — Commercial team tier (RBAC/SSO/SCIM, - admin review, audit export, team lanes, Postgres, hosted backups). A product-strategy - meta-issue, upstream of coding; depends on ACL Memory + HTTP/Postgres plans. - -### C. Ingest & Projection integrity - -- **Markdown + Rules Projection** (was #87) — Project memories into client-native - rule files (AGENTS.md, CLAUDE.md, `.cursor/rules`, Windsurf, Cline) with provenance - and an optional bidirectional re-import. The re-import leg is a data-integrity - boundary (must never silently overwrite user files). Target-format priorities are an - open user question — Discussion. -- **Code Memory Workflow** (was #88) — First-class, inspectable code memory - (patterns/decisions with file+line provenance) kept separate from prose. The typed - model exists (`crates/vestige-core/src/codebase/`) but is unpersisted/unwired; needs - schema + a review/prune dashboard surface. -- **Guided Import + Review Queue** (was #89) — Dry-run import → proposed memories → - approve/edit/split/merge/reject queue → post-import health pass. Ingest/corruption - boundary; needs a real no-write dry run + review-queue state machine. -- **Goals + Milestones** (was #90) — A durable, non-decaying goal/milestone primitive - (paralleling the intentions subsystem) with lifecycle states and evidence/blockers. - A create-only slice would ship the primitive without its defining non-decay - guarantee, so it waits for the full build. - -### D. Dashboard productization - -- **ComposedGraph Productization** (was #91) — The MCP/CLI/storage backend already - ships in v2.2 (`crates/vestige-mcp/src/tools/composed_graph.rs`, all 7 modes). The - remaining slice is the dashboard surface: composition history, the never-composed - frontier, and closed doors. This is the natural first move in the dashboard focus — - shape it in the Discussion. - -## Non-Goals - -- Do not auto-store every conversation turn by default. -- Do not require cloud services for memory creation, search, or configuration. -- Do not hide irreversible deletion. `purge` must stay explicit. -- Do not make code ingestion pollute general personal memory by default. -- Do not make advanced tuning required for ordinary users. - -## How To Read This Roadmap - -This is directional, not a release guarantee. The priority is adoption: fewer -surprises, clearer defaults, and better tool descriptions before adding complex -new surfaces. Community feedback that reveals a confusing first-use path should -usually become either a documentation fix, a safer default, or a guided workflow. diff --git a/docs/SANHEDRIN_RECEIPTS.md b/docs/SANHEDRIN_RECEIPTS.md deleted file mode 100644 index 2213c58..0000000 --- a/docs/SANHEDRIN_RECEIPTS.md +++ /dev/null @@ -1,98 +0,0 @@ -# Sanhedrin Receipt Schema - -Sanhedrin writes local, inspectable receipts so a Stop-hook veto is appealable -instead of opaque. The current schema is `vestige.sanhedrin.receipt.v1`. - -## Locations - -- Latest JSON: `~/.vestige/sanhedrin/latest.json` -- Latest HTML: `~/.vestige/sanhedrin/latest.html` -- Receipt archive: `~/.vestige/sanhedrin/receipts/.json` -- Command receipt ledger: `~/.vestige/sanhedrin/command-receipts.jsonl` -- Appeals: `~/.vestige/sanhedrin/appeals.jsonl` -- Fail-open events: `~/.vestige/sanhedrin/fail-open.jsonl` - -Optional companion schema: [`SANHEDRIN_TEST_INTEGRITY_DELTAS.md`](SANHEDRIN_TEST_INTEGRITY_DELTAS.md) describes mechanical deltas for cases where a verifier command passed but the test artifact changed after implementation. - -## v1 JSON Shape - -```json -{ - "schema": "vestige.sanhedrin.receipt.v1", - "id": "receipt_", - "draftId": "draft_", - "createdAt": "2026-05-25T18:00:00+00:00", - "overall": "pass|pass_with_warnings|veto|appealed", - "verdictBar": "PASS|NOTE|CAUTION|VETO|APPEALED", - "summary": "Human-readable result", - "draftPreview": "First 1000 chars of the assistant draft", - "claims": [ - { - "id": "c001", - "text": "All tests passed.", - "fingerprint": "16-char sha256 prefix", - "class": "receipt_lock|TECHNICAL|ACHIEVEMENT|...", - "subject": "Sam|draft|command receipt", - "risk": "normal|hard", - "evidence_state": "supported|missing_receipt|contradicted|appealed|...", - "decision": "pass|pass_unverified|veto|appealed", - "precedent": [ - { - "type": "command|receipt_lock|vestige|appeal", - "summary": "Why this claim passed or failed", - "command": "cargo test --workspace", - "exitCode": 0 - } - ], - "fix": "Suggested rewrite", - "appeal": { - "status": "open|appealed", - "actions": ["stale", "wrong", "too_strict"] - } - } - ], - "receipts": [ - { - "source": "transcript|codex-transcript", - "command": "cargo test --workspace", - "exitCode": 0, - "success": true, - "timestamp": "2026-05-25T18:00:00+00:00" - } - ], - "source": { - "stateDir": "~/.vestige/sanhedrin", - "transcript": "/path/to/session.jsonl" - } -} -``` - -## Compatibility Rules - -- Readers should accept `vestige.sanhedrin.receipt.v1` without warning. -- Readers should keep rendering unknown schemas defensively, but surface a - warning instead of silently treating them as v1. -- New schema versions must keep `id`, `createdAt`, `verdictBar`, `summary`, and - `claims` stable or provide a dashboard migration. - -## Staged Evidence Boundary - -`VESTIGE_SANHEDRIN_STAGE_FILE` is a non-durable overlay for current-turn context. -It may help the executioner understand a draft, but code enforces that staged -evidence cannot satisfy durable evidence requirements for `SUPPORTED`, -`REFUTED`, or `REFUTED_BY_ABSENCE`. Durable support must come from Vestige memory -or command receipts. - -## Receipt Lock Compatibility Flags - -`VESTIGE_SANHEDRIN_ALLOW_COMMAND_LEDGER=1` lets Receipt Lock read -`command-receipts.jsonl` when no live transcript path is available. - -`VESTIGE_SANHEDRIN_ALLOW_LOOSE_LEDGER=1` re-enables the legacy fallback that -regex-scans transcript JSON blobs for `command` or `cmd` fields. Keep this off -unless you are migrating old transcripts; structured tool-use receipts are safer -because loose scanning can mistake quoted text for a real command execution. - -Hosted Sanhedrin backends should use `VESTIGE_SANHEDRIN_API_KEY` in -`~/.claude/hooks/vestige-sanhedrin.env`. The installer keeps that file at mode -`0600`; do not store shared or unrelated API keys there. diff --git a/docs/SANHEDRIN_TEST_INTEGRITY_DELTAS.md b/docs/SANHEDRIN_TEST_INTEGRITY_DELTAS.md deleted file mode 100644 index c249819..0000000 --- a/docs/SANHEDRIN_TEST_INTEGRITY_DELTAS.md +++ /dev/null @@ -1,111 +0,0 @@ -# Sanhedrin Test-Integrity Delta Receipts - -Receipt Lock proves a narrower claim: a verification command actually ran and -succeeded. Test-integrity deltas are an optional companion receipt for the -stronger claim that the tests still mean what the draft says they mean. - -This receipt is intentionally mechanical. It is not a broad correctness oracle -and it does not ask a second model to decide whether the implementation is good. -It records whether the verification artifact changed in ways that should -upgrade, downgrade, or send the verification claim to human review. - -## Boundary - -Keep these claims separate: - -1. **Command receipt:** `cargo test`, `npm test`, `pytest`, or another verifier - command ran after the relevant edit and exited successfully. -2. **Test-integrity delta:** the tests/specs behind that verifier were not - removed, skipped, weakened, or replaced after implementation in a way that - makes the green result less admissible. - -A run can have a valid command receipt and still receive a downgraded -integrity decision. - -## Optional JSON Shape - -```json -{ - "schema": "vestige.sanhedrin.test_integrity_delta.v1", - "id": "tid_", - "commandReceiptId": "receipt_", - "verificationClaim": "All tests passed.", - "specSource": { - "contextId": "spec_ctx_04", - "testFiles": [ - { - "path": "tests/cart.test.ts", - "hashBeforeImplementation": "sha256:...", - "hashAfterVerification": "sha256:..." - } - ] - }, - "implementationContext": "impl_ctx_09", - "verifierContext": "verify_ctx_02", - "delta": { - "testFilesChangedAfterImplementation": true, - "removedOrDisabledTests": [ - { - "kind": "skip_or_only", - "path": "tests/cart.test.ts", - "line": 42 - } - ], - "removedAssertions": 2, - "weakenedExpectations": [ - { - "path": "tests/cart.test.ts", - "from": "throws InvalidCouponError", - "to": "does not throw" - } - ], - "snapshotChurnWithoutSourceChange": false, - "coverageDelta": -3.8, - "mocksReplacingRealBoundary": [ - { - "module": "PaymentGateway", - "before": "integration-ish fake", - "after": "empty stub" - } - ] - }, - "freshVerifier": { - "commandReceiptId": "receipt_", - "exitCode": 0, - "checkedAfterLastRelevantEdit": true - }, - "decision": "downgraded", - "reason": "tests passed, but the tests were weakened after implementation" -} -``` - -## Decisions - -- `accepted` — a verifier command succeeded after the last relevant edit and no - integrity downgrade was detected. -- `downgraded` — the command succeeded, but the tests/specs changed in a way - that makes the verification claim weaker than stated. -- `needs_human_review` — the delta may be legitimate, but a local mechanical - check cannot safely classify it. Snapshot updates are a common example. - -## Minimal Fixture Suite - -These cases are small enough to live as fixtures without turning Sanhedrin into -a correctness judge. Machine-readable examples live in -[`docs/fixtures/sanhedrin-test-integrity-deltas/`](fixtures/sanhedrin-test-integrity-deltas/). - -| Case | Input pattern | Expected decision | Why | -| --- | --- | --- | --- | -| unchanged-good | implementation changes source; tests unchanged; fresh verifier succeeds | `accepted` | Green tests are supported by a fresh command receipt and unchanged test artifact. | -| skipped-test | implementation adds `.skip`, `.only`, `#[ignore]`, or equivalent before verifier succeeds | `downgraded` | The command ran, but the claim no longer represents the original test obligation. | -| weakened-assertion | expectation is relaxed after implementation, e.g. `throws InvalidCouponError` -> `does not throw` | `downgraded` | The verifier passed against a weaker assertion than the one available before implementation. | -| justified-snapshot | snapshot changes alongside an intentional source/UI change | `needs_human_review` or `accepted` by policy | Snapshot churn can be valid, but the receipt should make the policy decision explicit. | - -## Non-goals - -- Do not infer whether the implementation is correct in the world. -- Do not require full semantic diffing before Receipt Lock can operate. -- Do not treat staged evidence or a model explanation as equivalent to a fresh - command receipt. -- Do not block every test edit. The goal is to keep the verification claim - honest when the test artifact changed after implementation. diff --git a/docs/SCIENCE.md b/docs/SCIENCE.md index fad2ca0..795708d 100644 --- a/docs/SCIENCE.md +++ b/docs/SCIENCE.md @@ -126,9 +126,11 @@ In Vestige's implementation: In Vestige: ``` -importance_score( - content="the-important content", - context_topics=["release", "memory"] +importance( + memory_id="the-important-one", + event_type="user_flag", + hours_back=9, + hours_forward=2 ) ``` @@ -181,7 +183,7 @@ This gives you exact keyword matching AND semantic understanding in one search. - Runs 100% local (after first download) - Competitive with OpenAI's ada-002 -The model is cached in the platform user cache directory after first run, with `./.fastembed_cache` as a fallback. Set `FASTEMBED_CACHE_PATH` to choose a specific cache path. +The model is cached at `~/.cache/huggingface/` after first run. --- diff --git a/docs/STORAGE.md b/docs/STORAGE.md index 1a82687..004dcee 100644 --- a/docs/STORAGE.md +++ b/docs/STORAGE.md @@ -1,6 +1,6 @@ # Storage Configuration -> Global, per-project, and multi-agent setups +> Global, per-project, and multi-Claude setups --- @@ -24,44 +24,6 @@ Override precedence: --- -## Moving Memories Between Devices - -For device-to-device migration, use a portable archive instead of the normal JSON export: - -```bash -# On the source machine -vestige portable-export ~/Desktop/vestige-portable.json - -# On the destination machine, before adding memories -vestige portable-import ~/Desktop/vestige-portable.json -``` - -Portable archives preserve raw Vestige storage rows: memory IDs, FSRS state, graph connections, suppression state, timestamps, audit history, and embedding blobs. - -For one-time migration, keep the conservative empty-database import: - -```bash -vestige portable-import ~/Desktop/vestige-portable.json -``` - -For cross-device sync, use merge mode or the file-backed sync command: - -```bash -# Merge a portable archive into an existing database. -vestige portable-import ~/Dropbox/vestige/portable.json --merge - -# Pull, merge, and push through a shared archive file. -vestige sync ~/Dropbox/vestige/portable.json -``` - -`vestige sync` uses the same pluggable portable-sync backend interface as the core library. v2.1.1 ships a file backend, which works with Dropbox, iCloud Drive, Syncthing, Git, network shares, or any folder-sync system. The merge algorithm applies delete tombstones, keeps newer local memories on timestamp conflicts, preserves stable IDs, rebuilds FTS after import, and writes the pushed archive atomically when the filesystem supports rename. v2.1.2 also carries non-content purge tombstones so a hard purge can sync without retaining the deleted memory text. - -When using the MCP `export` tool with `format: "portable"`, Vestige writes the archive under the active data directory's `exports/` folder. The MCP `restore` tool only reads from that `exports/` or `backups/` folder by default; pass `allowAnyPath: true` only for a trusted local file you selected manually. - -The regular `vestige export` / `vestige restore` path remains useful for human-readable backups, partial exports, and older files, but it re-ingests memory content and does not preserve every storage-level relationship. - ---- - ## Storage Modes ### Option 1: Global Memory (Default) @@ -89,9 +51,9 @@ Separate memory per codebase. Good for: - Different coding styles per project - Team environments -**MCP Client Setup:** +**Claude Code Setup:** -Add an MCP server entry to your client or project config: +Add to your project's `.claude/settings.local.json`: ```json { "mcpServers": { @@ -107,13 +69,6 @@ This creates `.vestige/vestige.db` in your project root. Add `.vestige/` to `.gi If both `VESTIGE_DATA_DIR` and `--data-dir` are set, the CLI flag wins. Use the env var for a machine-wide default and the CLI flag for per-client or per-project overrides. -The `vestige` CLI also honors `VESTIGE_DATA_DIR`, so use the same directory when inspecting or exporting a custom MCP instance: - -```bash -VESTIGE_DATA_DIR=./.vestige vestige stats -VESTIGE_DATA_DIR=./.vestige vestige portable-export ./vestige-portable.json -``` - **Multiple Named Instances:** For power users who want both global AND project memory: @@ -131,11 +86,11 @@ For power users who want both global AND project memory: } ``` -### Option 3: Multi-Agent Household +### Option 3: Multi-Claude Household -For setups with multiple MCP clients or agent personas: +For setups with multiple Claude instances (e.g., Claude Desktop + Claude Code, or two personas): -**Shared Memory (all clients share memories):** +**Shared Memory (Both Claudes share memories):** ```json { "mcpServers": { @@ -147,27 +102,27 @@ For setups with multiple MCP clients or agent personas: } ``` -**Separate Identities (each agent has its own memory):** +**Separate Identities (Each Claude has own memory):** -Client config for "Research": +Claude Desktop config - for "Domovoi": ```json { "mcpServers": { "vestige": { "command": "vestige-mcp", - "args": ["--data-dir", "~/vestige-research"] + "args": ["--data-dir", "~/vestige-domovoi"] } } } ``` -Client config for "Builder": +Claude Code config - for "Storm": ```json { "mcpServers": { "vestige": { "command": "vestige-mcp", - "args": ["--data-dir", "~/vestige-builder"] + "args": ["--data-dir", "~/vestige-storm"] } } } @@ -177,7 +132,7 @@ Client config for "Builder": ## Data Safety -**Important:** Vestige stores data locally. v2.1.1 adds user-controlled file-backed sync through `vestige sync`, but Vestige does not run a hosted cloud service, background replication daemon, or automatic backup for you. +**Important:** Vestige stores data locally with no cloud sync, redundancy, or automatic backup. | Use Case | Risk Level | Recommendation | |----------|------------|----------------| @@ -263,9 +218,9 @@ Internally the `Storage` type holds **separate reader and writer connections**, | Pattern | Status | Notes | |---------|--------|-------| -| One `vestige-mcp` + one MCP client | **Supported** | The default case. Zero contention. | -| Multiple MCP clients, separate `--data-dir` | **Supported** | Each process owns its own DB file. No shared state. | -| Multiple MCP clients, **shared** `--data-dir`, **one** `vestige-mcp` | **Supported** | Clients talk to a single MCP process that owns the DB. Recommended for multi-agent setups. | +| 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. | diff --git a/docs/VESTIGE_STATE_AND_PLAN.md b/docs/VESTIGE_STATE_AND_PLAN.md index 5f22469..0ee5821 100644 --- a/docs/VESTIGE_STATE_AND_PLAN.md +++ b/docs/VESTIGE_STATE_AND_PLAN.md @@ -1,102 +1,1273 @@ -# Vestige State And Plan +# Vestige: State of the Engine & Next-Phase Plan -This document is a public, sanitized replacement for an older internal planning -snapshot. It intentionally omits private local paths, personal operating -context, unpublished roadmap notes, and private repository locations. +> **For:** 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. -For current user-facing release information, use: +--- -- `README.md` -- `CHANGELOG.md` -- `docs/STORAGE.md` -- `docs/COGNITIVE_SANDWICH.md` -- `docs/AGENT-MEMORY-PROTOCOL.md` -- `docs/CLAUDE-SETUP.md` +## Table of Contents -## Current Release Shape +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) -Vestige v2.1.21 is the "Agent-Neutral Hardening" release. Its public scope is: +--- -- stdio MCP as the default agent transport, with HTTP MCP opt-in only -- binary-only `vestige update` by default -- delete and purge confirmation parity for destructive memory removal -- portable sync fixes for purge tombstones, UPSERT merge, and vector index - reloads -- safer release packaging with dashboard freshness checks and checksums -- agent-neutral memory instructions for any MCP-compatible client +## 0. Executive Summary (60-second read) -The release keeps the local-first baseline intact. Heavy model hooks, local -verifier models, and preflight automation remain optional. +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. -## Release Gates +**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 ✅. -Before tagging a release, run: +**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. -```sh -cargo test --workspace --no-fail-fast -cargo clippy --workspace -- -D warnings -pnpm --filter @vestige/dashboard check -pnpm --filter @vestige/dashboard build -git diff --check +**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 ``` -For dashboard route changes, rebuild and stage `apps/dashboard/build/` so the -embedded static assets match `apps/dashboard/src/`. +### 2.2 Dependency flow -## Product Principles +``` +┌─────────────────────┐ +│ 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) + └───────────────────┘ +``` -- Exact things should stay exact. Literal identifiers should not lose to - semantic expansion. -- Forgetting should be honest. A hard purge should remove content, embeddings, - graph edges, and derived references while retaining only non-content proof - that deletion happened. -- Contradictions should be visible. Trust-weighted disagreement should be - inspectable directly instead of hidden inside a broader reasoning tool. -- Installation should remain boring. Users should not need a large local model - or background hook system just to use memory. -- Pro features should add managed convenience without weakening local-first - ownership. +### 2.3 Build profile -## Public Architecture Summary +```toml +[profile.release] +lto = true +codegen-units = 1 +panic = "abort" +strip = true +opt-level = "z" # Size-optimized; binary is ~22 MB with dashboard +``` -Vestige is organized as: +### 2.4 Workspace Cargo.toml pinned version -- `crates/vestige-core`: storage, search, embeddings, memory lifecycle, FSRS, - graph, dream, and cognitive modules -- `crates/vestige-mcp`: MCP server, CLI, dashboard backend, tools, update flow -- `apps/dashboard`: SvelteKit dashboard source -- `packages/vestige-mcp-npm`: npm wrapper for the MCP binary -- `packages/vestige-init`: installer helper -- `docs`: user and integration documentation +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`). -## v2.1.21 Implementation Notes +### 2.5 MSRV & editions -HTTP MCP is disabled unless the user passes `--http`, passes `--http-port`, or -sets `VESTIGE_HTTP_ENABLED=1`. The stdio MCP server remains the portable default -for Claude Code, Codex, Cursor, VS Code, Xcode, OpenCode, JetBrains, Windsurf, -and other clients. +- **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. -Purge is implemented transactionally in storage and surfaced through the MCP -`memory` tool. `memory(action="purge", confirm=true)` is the explicit hard -delete path. `delete` remains a backwards-compatible alias but also requires -`confirm=true`. +--- -Portable merge imports preserve both sync tombstones and non-content deletion -tombstones. Keyed table writes use UPSERT rather than `INSERT OR REPLACE` so -related rows are not accidentally cascaded away. +## 3. `vestige-core` — Cognitive Engine -Claude Code Cognitive Sandwich files are optional companion files, not the -default Vestige setup path. Use `vestige update --sandwich-companion` or -`vestige sandwich install` only when that hook layer is wanted. +### 3.1 Purpose -## 15. Autopilot Rationale +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. -The backend event bus exists so dashboard and MCP activity can be observed by -the cognitive engine without making user-facing agent hooks mandatory. Any -autonomous behavior should be conservative, rate-limited, and local-first. +### 3.2 Metadata -Autopilot-style routing should never require a remote model, a heavy local -model, or a Claude hook to make normal memory useful. It should only connect -already-emitted Vestige events to existing cognitive modules when that improves -maintenance, retrieval quality, or dashboard fidelity without surprising the -user. +```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/adr/0001-pluggable-storage-and-network-access.md b/docs/adr/0001-pluggable-storage-and-network-access.md deleted file mode 100644 index c150c70..0000000 --- a/docs/adr/0001-pluggable-storage-and-network-access.md +++ /dev/null @@ -1,303 +0,0 @@ -# ADR 0001: Pluggable Storage Backend, Network Access, and Emergent Domains - -**Status**: Accepted -**Date**: 2026-04-21 -**Related**: [docs/prd/001-getting-centralized-vestige.md](../prd/001-getting-centralized-vestige.md) - ---- - -## Context - -Vestige v2.x runs as a per-machine local process: stdio MCP transport, SQLite + -FTS5 + USearch HNSW in `~/.vestige/`, fastembed locally for embeddings. This is -ideal for single-machine single-agent use but blocks three real needs: - -- **Multi-machine access** -- same memory brain from laptop, desktop, server -- **Multi-agent access** -- multiple AI clients against one store concurrently -- **Future federation** -- syncing memory between decentralized nodes (MOS / - Threefold grid) - -SQLite's single-writer model and lack of a native network protocol make it -unsuitable as a centralized server. PostgreSQL + pgvector collapses our three -storage layers (SQLite, FTS5, USearch) into one engine with MVCC concurrency, -auth, and replication. - -Separately, Vestige today has no notion of domain or project scope -- all memories -share one namespace. For a multi-machine brain, users want soft topical -boundaries ("dev", "infra", "home") without manual tenanting. HDBSCAN clustering -on embeddings produces these boundaries from the data itself. - -The PRD at `docs/prd/001-getting-centralized-vestige.md` sketches the full design. -This ADR records the architectural decisions and resolves the open questions from -that document. - ---- - -## Decision - -Introduce two new trait boundaries, a network transport layer, and a domain -classification module. All four changes ship in parallel phases. - -**Trait boundaries:** - -1. `MemoryStore` -- single trait covering CRUD, hybrid search, FSRS scheduling, - graph edges, and domains. One big trait, not four. -2. `Embedder` -- separate trait for text-to-vector encoding. Storage never calls - fastembed directly. Callers (cognitive engine locally, HTTP server remotely) - compute embeddings and pass them into the store. - -**Backends:** - -- `SqliteMemoryStore` -- existing code refactored behind the trait, no behavior - change. -- `PgMemoryStore` -- new, using sqlx + pgvector + tsvector. Selectable at runtime - via `vestige.toml`. - -**Network:** - -- MCP over Streamable HTTP on the existing Axum server. -- API key auth middleware (blake3-hashed, stored in `api_keys` table). -- Dashboard uses the same API keys for login, then signed session cookies for - subsequent requests. - -**Domain classification:** - -- HDBSCAN clustering over embeddings to discover domains automatically. -- Soft multi-domain assignment -- raw similarity scores stored per memory, every - domain above a threshold is assigned. -- Conservative drift handling -- propose splits/merges, never auto-apply. - ---- - -## Architecture Overview - -### Component Breakdown - -1. **`Embedder` trait** (new module `crates/vestige-core/src/embedder/`) - - `async fn embed(&self, text: &str) -> Result>` - - `fn model_name(&self) -> &str` - - `fn dimension(&self) -> usize` - - Impls: `FastembedEmbedder` (local ONNX, today), future `JinaEmbedder`, - `OpenAiEmbedder`, etc. - - Stays pluggable forever -- no lock-in to fastembed or to nomic-embed-text. - -2. **`MemoryStore` trait** (new module `crates/vestige-core/src/storage/trait.rs`) - - One trait, ~25 methods across CRUD, search, FSRS, graph, domain sections. - - Uses `trait_variant::make` to generate a `Send`-bound variant for - `Arc` in Axum/tokio contexts. - - The 29 cognitive modules operate exclusively through this trait. No direct - SQLite or Postgres access from the modules. - -3. **`SqliteMemoryStore`** (refactor of existing `crates/vestige-core/src/storage/sqlite.rs`) - - Existing rusqlite + FTS5 + USearch code, wrapped behind the trait. - - Add `domains TEXT[]` equivalent (JSON-encoded array column in SQLite). - - Add `domain_scores` JSON column. - - No behavioral change for current users. - -4. **`PgMemoryStore`** (new `crates/vestige-core/src/storage/postgres.rs`) - - `sqlx::PgPool` with compile-time checked queries. - - pgvector HNSW index for vector search, tsvector + GIN for FTS. - - Native array columns for `domains`, JSONB for `domain_scores` and `metadata`. - - Hybrid search via RRF (Reciprocal Rank Fusion) in a single SQL query. - -5. **Model registry** - - Per-database table `embedding_model` with `(name, dimension, hash, created_at)`. - - Both backends refuse writes from an embedder whose signature doesn't match - the registered row. - - Model swap = `vestige migrate --reembed --model=`, O(n) cost, explicit. - -6. **`DomainClassifier` cognitive module** (new `crates/vestige-core/src/neuroscience/domain_classifier.rs`) - - Owns the HDBSCAN discovery pass (using the `hdbscan` crate). - - Computes soft-assignment scores for every memory against every centroid. - - Stores raw `domain_scores: HashMap` per memory; thresholds into - the `domains` array using `assign_threshold` (default 0.65). - - Runs discovery on demand (`vestige domains discover`) or during dream - consolidation passes. - -7. **HTTP MCP transport** (extension of existing Axum server in `crates/vestige-mcp/src/`) - - New route `POST /mcp` for Streamable HTTP JSON-RPC. - - New route `GET /mcp` for SSE (for long-running operations). - - REST API under `/api/v1/` for direct HTTP clients (non-MCP integrations). - - Auth middleware validates `Authorization: Bearer ...` or `X-API-Key`, plus - signed session cookies for dashboard. - -8. **Key management** (new `crates/vestige-mcp/src/auth/`) - - `api_keys` table -- blake3-hashed keys, scopes, optional domain filter, - last-used timestamp. - - CLI: `vestige keys create|list|revoke`. - -9. **FSRS review event log** (future-proofing for federation) - - New table `review_events` -- append-only `(memory_id, timestamp, rating, - prior_state, new_state)`. - - Current `scheduling` table becomes a materialized view over the event log - (reconstructible from events). - - Phase 5 federation merges event logs, not derived state. Zero cost today, - avoids lock-in tomorrow. - -### Data Flow - -**Local mode (stdio MCP, unchanged UX):** -``` -stdio client -> McpServer -> CognitiveEngine -> FastembedEmbedder -> MemoryStore (SQLite) -``` - -**Server mode (HTTP MCP, new):** -``` -Remote client -> Axum HTTP -> auth middleware -> CognitiveEngine - -> FastembedEmbedder (server-side) -> MemoryStore (Postgres) -``` - -The cognitive engine is backend-agnostic. The embedder and the store are both -swappable. The 7-stage search pipeline (overfetch -> cross-encoder rerank -> -temporal -> accessibility -> context match -> competition -> spreading activation) -sits *above* the `MemoryStore` trait and works identically against either backend. - -### Orthogonality of HDBSCAN and Reranking - -HDBSCAN and the cross-encoder reranker solve different problems and both stay: - -- **HDBSCAN** discovers domains by clustering embeddings. Runs once per discovery - pass. Produces centroids. Used to *filter* search candidates, not to rank them. -- **Cross-encoder reranker** (Jina Reranker v1 Turbo) scores query-document pairs - at search time. Runs on every search. Produces ranked results. - -Domain membership is a filter applied before or during overfetch; reranking runs -on whatever candidate set survives the filter. - ---- - -## Alternatives Considered - -| Alternative | Pros | Cons | Why Not | -|-------------|------|------|---------| -| Split into 4 traits (`MemoryStore + SchedulingStore + GraphStore + DomainStore`) | Cleaner interface segregation | Every module holds 4 trait objects, coordinates transactions across them | One trait is fine in Rust; extract sub-traits later if a genuine need appears | -| Embedding computed inside the backend | Simpler call sites for callers | Backend becomes aware of embedding models; can't support remote clients without local fastembed | Keep storage pure; separate `Embedder` trait handles pluggability | -| Unconstrained pgvector `vector` (no dimension) | Flexible for model swaps | HNSW still needs fixed dims at index creation; hides a meaningful migration as "silent" | Fixed dimension per install, explicit `--reembed` migration | -| Dashboard separate auth (cookies only, no keys) | Simpler dashboard UX | Two auth systems to maintain | Shared API keys with session cookie layer on top | -| Auto-tuned `assign_threshold` targeting an unclassified ratio | Adapts to corpus | Hard to debug ("why did this memory change domain?"); magical | Static 0.65 default, config-tunable, dashboard shows `domain_scores` for manual retuning | -| Aggressive drift (auto-reassign memories whose scores drifted) | Always up-to-date domains | Breaks user muscle memory; silent reshuffling | Conservative: always propose, user accepts | -| CRDTs for federation state | Mathematically clean merges | Massive complexity, performance cost, overkill | Defer; design FSRS as event log now so any future sync model works | - ---- - -## Consequences - -### Positive - -- Single memory brain accessible from every machine. -- Multi-agent concurrent access via Postgres MVCC. -- Natural topical scoping emerges from data, not manual tenants. -- Future embedding model swaps are a config + migration, not a rewrite. -- Federation has a clean on-ramp (event log merge) without committing now. -- The `Embedder` / `MemoryStore` split unlocks other storage backends later - (Redis, Qdrant, Iroh-backed blob store, etc.) with minimal work. - -### Negative - -- Operating a Postgres instance is more work than managing a SQLite file. -- Users who stay on SQLite gain nothing from this ADR (but lose nothing either). -- Migration (`vestige migrate --from sqlite --to postgres`) is a sensitive - operation for users with months of memories -- needs strong testing. -- HDBSCAN + re-soft-assignment runs in O(n) over all embeddings. At 100k+ - memories this starts to matter; manageable but not free. - -### Risks - -- **Trait abstraction leaks**: a cognitive module might need backend-specific - behavior (e.g., Postgres triggers for tsvector). Mitigation: keep such logic - inside the backend impl; the trait stays pure. - Escalation: if a module genuinely cannot express what it needs through the - trait, the trait grows, not the module bypasses. -- **Embedding model drift**: users on older fastembed versions silently - producing slightly different vectors after a fastembed upgrade. Mitigation: - model hash in the registry, refuse mismatched writes, surface a clear error. -- **Auth misconfiguration**: a user binds to `0.0.0.0` without setting - `auth.enabled = true`. Mitigation: refuse to start with non-localhost bind - and auth disabled. Hard error, not a warning. -- **Re-clustering feedback loop**: dream consolidation proposes re-clusters, - which the user accepts, which changes classifications, which affects future - retrievals, which affect future dreams. Mitigation: cap re-cluster frequency - (every 5th dream by default), require explicit user acceptance of proposals. -- **Cross-domain spreading activation weight (0.5 default)**: arbitrary choice; - could be too aggressive or too lax. Mitigation: config-tunable; instrument - retrieval quality metrics in the dashboard so the user sees impact. - ---- - -## Resolved Decisions (from Q&A) - -| # | Question | Resolution | -|---|----------|------------| -| 1 | Trait granularity | Single `MemoryStore` trait | -| 2 | Embedding on insert | Caller provides; separate `Embedder` trait for pluggability | -| 3 | pgvector dimension | Fixed per install, derived from `Embedder::dimension()` at schema init | -| 4 | Federation sync | Defer algorithm; store FSRS reviews as append-only event log now | -| 5 | Dashboard auth | Shared API keys + signed session cookie | -| 6 | HDBSCAN `min_cluster_size` | Default 10; user reruns with `--min-cluster-size N`; no auto-sweep | -| 7 | Domain drift | Conservative -- always propose splits/merges, never auto-apply | -| 8 | Cross-domain spreading activation | Follow with decay factor 0.5 (tunable) | -| 9 | Assignment threshold | Static 0.65 default, config-tunable, raw `domain_scores` stored for introspection | - ---- - -## Implementation Plan - -Five phases, each independently shippable. - -### Phase 1: Storage trait extraction -- Define `MemoryStore` and `Embedder` traits in `vestige-core`. -- Refactor `SqliteMemoryStore` to implement `MemoryStore`; no behavior change. -- Refactor `FastembedEmbedder` to implement `Embedder`. -- Add `embedding_model` registry table; enforce consistency on write. -- Add `domains TEXT[]`-equivalent and `domain_scores` JSON columns to SQLite - (empty for all existing rows). -- Convert all 29 cognitive modules to operate via the traits. -- **Acceptance**: existing test suite passes unchanged. Zero warnings. - -### Phase 2: PostgreSQL backend -- `PgMemoryStore` with sqlx, pgvector, tsvector. -- sqlx migrations (`crates/vestige-core/migrations/postgres/`). -- Backend selection via `vestige.toml` `[storage]` section. -- `vestige migrate --from sqlite --to postgres` command. -- `vestige migrate --reembed` command for model swaps. -- **Acceptance**: full test suite runs green against Postgres with a testcontainer. - -### Phase 3: Network access -- Streamable HTTP MCP route on Axum (`POST /mcp`, `GET /mcp` for SSE). -- REST API under `/api/v1/`. -- API key table + blake3 hashing + `vestige keys create|list|revoke`. -- Auth middleware (Bearer, X-API-Key, session cookie). -- Refuse non-localhost bind without auth enabled. -- **Acceptance**: MCP client over HTTP works from a second machine; dashboard - login flow works; unauth requests return 401. - -### Phase 4: Emergent domain classification -- `DomainClassifier` module using the `hdbscan` crate. -- `vestige domains discover|list|rename|merge` CLI. -- Automatic soft-assignment pipeline (compute `domain_scores` on ingest, threshold - into `domains`). -- Re-cluster every Nth dream consolidation (default 5); surface proposals in the - dashboard. -- Context signals (git repo, IDE) as soft priors on classification. -- Cross-domain spreading activation with 0.5 decay. -- **Acceptance**: on a corpus of 500+ mixed memories, discover produces sensible - clusters; search scoped to a domain returns tightly relevant results. - -### Phase 5: Federation (future, explicitly out of scope for this ADR's -acceptance) -- Node discovery (Mycelium / mDNS). -- Memory sync protocol over append-only review events and LWW-per-UUID for - memory records. -- Explicit follow-up ADR before any code. - ---- - -## Open Questions - -None at ADR acceptance time. Follow-up items that are *implementation choices*, -not architectural: - -- Precise cross-domain decay weight (start at 0.5, instrument, tune) -- Dashboard histogram of `domain_scores` (UX design detail) -- Whether to gate Postgres behind a Cargo feature flag (`postgres-backend`) or - always compile it in (lean toward feature flag to keep SQLite-only builds small) diff --git a/docs/adr/0002-phase-2-execution.md b/docs/adr/0002-phase-2-execution.md deleted file mode 100644 index 5d590b4..0000000 --- a/docs/adr/0002-phase-2-execution.md +++ /dev/null @@ -1,545 +0,0 @@ -# ADR 0002: Phase 2 Execution -- Postgres Backend Integration, Phase 1 Amendment - -**Status**: Accepted -**Date**: 2026-05-26 -**Related**: [docs/adr/0001-pluggable-storage-and-network-access.md](0001-pluggable-storage-and-network-access.md), [docs/plans/0002-phase-2-postgres-backend.md](../plans/0002-phase-2-postgres-backend.md) - ---- - -## Context - -ADR 0001 set the architectural direction: introduce `MemoryStore` and `Embedder` -traits, ship a Postgres backend behind a feature flag, and reach a single shared -memory brain across machines. Phase 1 (storage trait extraction) shipped on -`feat/storage-trait-phase1` (790c0c8). The Phase 2 master plan at -`docs/plans/0002-phase-2-postgres-backend.md` was drafted before Phase 1 was -frozen. - -Starting Phase 2 surfaces a small set of execution-level decisions that ADR 0001 -did not cover and that the master plan now disagrees with the live code on. -These decisions are too big to silently absorb into a per-step plan and too -small to amend ADR 0001. They live here. - -Three concrete realities driving this ADR: - -1. **Trait shape mismatch.** Master plan 0002 assumed `trait_variant::make` - produced distinct `MemoryStore` (Send-bound) and `LocalMemoryStore` - (non-Send) variants, and that errors were `StoreError`. Phase 1 froze on - `#[async_trait::async_trait]` with `pub use MemoryStore as LocalMemoryStore` - and an error type called `MemoryStoreError`. The Postgres backend has to - follow Phase 1, not the master plan -- but we should record that explicitly. -2. **`SqliteMemoryStore` is monolithic.** - `crates/vestige-core/src/storage/sqlite.rs` is ~8200 lines. Phase 1 appended - the trait impl block at the bottom of the same file. Adding a similarly - large `postgres.rs` perpetuates the pattern; this is the natural moment to - decide whether the SQLite file gets split. -3. **Constructor surface drift.** Master plan 0002 specifies - `PgMemoryStore::connect(url, max_connections, &dyn Embedder)`. The Phase 1 - `SqliteMemoryStore` constructor takes no embedder -- registry consistency - runs through `registered_model()` / `register_model()` on the trait, - invoked by the caller. The two backends should look the same to a caller; - right now they would not. -4. **Multi-tenancy is a one-way door.** The Postgres schema is the place to - reserve user/group/visibility columns *now*, even though Phase 3 is the - phase that wires the auth filter using them. Adding `owner_user_id` and - GIN indexes to a populated, HNSW-indexed `knowledge_nodes` table later is an - expensive online migration; reserving NULL-defaulted columns at schema - creation is ~10 lines of SQL. The same logic applies to per-memory - context capture (codebase, MCP caller, session) -- promoting `codebase` - to a first-class column now keeps the door open for context-aware - sharing rules in Phase 4 without touching `knowledge_nodes`. See D7 and D8. - -This ADR is also the umbrella under which Phase 2 sub-plans (`0002a-...`, -`0002b-...`, etc.) sit. The intent is: ADR + sub-plans land as one PR for -review; the implementation lands as a second PR with many commits inside. - ---- - -## Already Decided (carried in by reference) - -These are settled by ADR 0001 or by explicit agreement during this session. -Listed here so the discussion frame is clear; not re-litigated below. - -- Postgres backend ships behind a `postgres-backend` Cargo feature, default - OFF. Mutually compilable with SQLite. (ADR 0001.) -- Single big `MemoryStore` trait. `PgMemoryStore` implements the same surface - as `SqliteMemoryStore`. (ADR 0001.) -- pgvector HNSW + tsvector + GIN + RRF hybrid search in one SQL statement. - (Master plan 0002, D4-D5.) -- sqlx 0.8 + pgvector 0.4 + compile-time-checked queries + offline `.sqlx/` - cache committed. (Master plan 0002.) -- Two sqlx migration files: `0001_init` (extensions, tables, non-vector - indexes) and `0002_hnsw` (HNSW separated for re-embed drop/recreate). - (Master plan 0002, D4.) -- `vestige migrate --from sqlite --to postgres` and - `vestige migrate --reembed --model=` CLI subcommands. (ADR 0001 + - master plan 0002, D8-D10.) -- PR cadence: PR #1 carries this ADR plus all sub-plans; PR #2 carries the - implementation as many commits. -- Sub-plans use `0002a-`, `0002b-`, ... suffixes off `0002-`. -- `PgMemoryStore::connect` lands as `todo!()` in the skeleton; real body - comes later. - ---- - -## Decisions - -### D1. Sunset async_trait across the Phase 1 traits - -Phase 1 froze with `#[async_trait::async_trait]` on both the `MemoryStore` -trait (`storage/memory_store.rs:194`) and the `Embedder` trait -(`embedder/mod.rs:27`), plus their SQLite and Fastembed impl blocks. async_trait -boxes every async fn into `Pin>` -- one heap allocation -per call inside the hottest code path. We are amending Phase 1 to remove -async_trait entirely and replace it with `trait_variant::make`, so each trait -becomes two real generated variants (`MemoryStore` / `LocalMemoryStore`, -`Embedder` / `LocalEmbedder`) with `Send` bounds on the outer variant. - -Scope split across three Phase 1 amendment sub-plans: - -- **`0001a-trait-rewrite.md`** -- Rewrite `MemoryStore` only. Touches - `storage/memory_store.rs` (trait declaration) and `storage/sqlite.rs` - (impl block attribute). Leaves async_trait in place on the embedder side - so the diff stays focused. -- **`0001b-sqlite-split.md`** -- Pure code motion. Splits the - ~8200-line `sqlite.rs` into a `sqlite/` directory. Independent of D1; can - land in either order relative to `0001a`. -- **`0001c-async-trait-sunset.md`** -- Rewrite `Embedder` the same way, then - remove `async-trait = "0.1"` from `crates/vestige-core/Cargo.toml`. Final - amendment commit removes the dependency entirely. After this lands, the - workspace contains zero references to `async_trait`. - -All three sub-plans land on the existing `feat/storage-trait-phase1` branch -(790c0c8 has not been opened upstream yet; amend in place, no force-push to a -public PR). - -### D2. PgMemoryStore::connect mirrors SqliteMemoryStore::new - -```rust -impl PgMemoryStore { - pub async fn connect(url: &str, max_connections: u32) -> MemoryStoreResult; - pub async fn from_pool(pool: PgPool) -> MemoryStoreResult; -} -``` - -No `Embedder` in the constructor. The pgvector-specific -`ALTER TABLE knowledge_nodes ALTER COLUMN embedding TYPE vector($N)` DDL lives -inside the trait method `register_model(&ModelSignature)`. That method is -called by the caller (cognitive engine bootstrap, migrate CLI, tests) after -construction, exactly as it is for `SqliteMemoryStore`. - -`MemoryStoreError` gains two variants behind the feature flag (added during -the Postgres impl, not during the Phase 1 amendment): -```rust -#[cfg(feature = "postgres-backend")] -#[error("postgres error: {0}")] -Postgres(#[from] sqlx::Error), - -#[cfg(feature = "postgres-backend")] -#[error("postgres migration error: {0}")] -Migrate(#[from] sqlx::migrate::MigrateError), -``` - -### D3. Split sqlite.rs into a sqlite/ directory as Phase 1 amendment - -Pure code motion, no behavioural change. Target layout: -``` -crates/vestige-core/src/storage/sqlite/ - mod.rs -- SqliteMemoryStore struct, new(), reader/writer locks - crud.rs -- insert/get/update/delete - search.rs -- fts_search, vector_search, hybrid search - scheduling.rs -- FSRS state methods - graph.rs -- edges, neighbors - domain.rs -- domain CRUD, classify stub - registry.rs -- embedding_model table + register_model - portable_sync.rs -- portable archive backend bridge - trait_impl.rs -- impl LocalMemoryStore for SqliteMemoryStore -``` - -Cognitive-module imports stay on `crate::storage::SqliteMemoryStore` and -related re-exports from `storage/mod.rs`; the split is private to the -module. Each motion commit must keep `cargo test -p vestige-core` green for -bisectability. - -This lands in the Phase 1 amendment PR alongside D1 (separate commit, same -branch). - -### D4. Postgres backend as a directory from day one - -``` -crates/vestige-core/src/storage/postgres/ - mod.rs -- PgMemoryStore struct, connect, from_pool, trait impl - pool.rs -- PgPool construction from PostgresConfig - migrations.rs -- sqlx::migrate! wrapper - registry.rs -- ensure_registry, ALTER COLUMN TYPE vector(N) - search.rs -- RRF query + row mapping - migrate_cli.rs -- SQLite -> Postgres streaming copy - reembed.rs -- O(n) re-encode + HNSW rebuild -``` - -D1+D2 of the master plan land first as a skeleton in `mod.rs` with `todo!()` -bodies; later sub-plans fill in the other files. - -### D5. Sub-plan layout: two phases worth of sub-plans - -Phase 1 amendment sub-plans (under `docs/plans/`): -- `0001a-trait-rewrite.md` -- MemoryStore async_trait -> trait_variant, call-site audit -- `0001b-sqlite-split.md` -- sqlite.rs -> sqlite/ directory, commit-by-commit -- `0001c-async-trait-sunset.md` -- Embedder rewrite + drop async-trait dep from Cargo.toml - -Phase 2 sub-plans (under `docs/plans/`): -- `0002a-skeleton-and-feature-gate.md` -- master plan D1 + D2 (todo!() bodies) -- `0002b-pool-and-config.md` -- master plan D3 + D7 -- `0002c-migrations.md` -- master plan D4 -- `0002d-store-impl-bodies.md` -- master plan D2 real bodies + D6 registry -- `0002e-hybrid-search.md` -- master plan D5 -- `0002f-migrate-cli.md` -- master plan D8 + D10 -- `0002g-reembed.md` -- master plan D9 -- `0002h-testing-and-benches.md` -- master plan D14 + D15 -- `0002i-runbook.md` -- master plan D16 - -Each sub-plan is a self-contained brief sized to fit one focused -implementation session (handed to Claude Code as a `/goal` instruction -without requiring the agent to load the master plan). - -### D6. SQLite split does not get its own ADR - -The split is pure code motion; no public types, behaviour, or paths change. -`0001b-sqlite-split.md` is enough. - -### D7. Multi-tenancy schema reservation (L1-L3) - -Phase 2 reserves the columns and tables needed for future per-user / per-group -visibility, so Phase 3 (auth) does not require a column-add migration over a -populated, HNSW-indexed `knowledge_nodes` table. Single-user behaviour is unchanged -in both backends. - -New tables in `0001_init.up.sql`: - -```sql -CREATE TABLE users ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - handle TEXT NOT NULL UNIQUE, - display_name TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - metadata JSONB NOT NULL DEFAULT '{}'::jsonb -); - -CREATE TABLE groups ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - handle TEXT NOT NULL UNIQUE, - display_name TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - metadata JSONB NOT NULL DEFAULT '{}'::jsonb -); - -CREATE TABLE group_memberships ( - user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, - group_id UUID NOT NULL REFERENCES groups(id) ON DELETE CASCADE, - role TEXT NOT NULL DEFAULT 'member', -- 'member' | 'admin' - joined_at TIMESTAMPTZ NOT NULL DEFAULT now(), - PRIMARY KEY (user_id, group_id) -); - -INSERT INTO users (id, handle, display_name) - VALUES ('00000000-0000-0000-0000-000000000001', 'local', 'Local User'); -``` - -New columns on `knowledge_nodes`: - -```sql -owner_user_id UUID NOT NULL DEFAULT '00000000-0000-0000-0000-000000000001' - REFERENCES users(id), -visibility TEXT NOT NULL DEFAULT 'private', -- 'private' | 'group' | 'public' -shared_with_groups UUID[] NOT NULL DEFAULT '{}' - -CREATE INDEX idx_knowledge_nodes_owner ON knowledge_nodes (owner_user_id); -CREATE INDEX idx_knowledge_nodes_shared_groups ON knowledge_nodes USING GIN (shared_with_groups); -``` - -Phase 3 visibility filter (declared here for reference; implemented in Phase 3): - -```sql -WHERE - (visibility = 'private' AND owner_user_id = $me) - OR (visibility = 'group' - AND (owner_user_id = $me OR shared_with_groups && $my_group_ids)) - OR visibility = 'public' -``` - -Why tri-state enum and not just `shared_with_groups[] + is_public`: the -explicit `visibility` field documents intent at the row level. A `'private'` -row with a non-empty `shared_with_groups` is detectable inconsistency -(a CHECK constraint can enforce it later) rather than silent data. - -SQLite parity: same tables and columns with identical defaults. -`shared_with_groups` is a JSON `'[]'` text encoding (no array type). -Single-user mode never changes any of these values; the trait surface ignores -the visibility filter for SQLite because there is exactly one user. - -Sharing automation (matching by domain, tag, repo, MCP caller, ...) is -explicitly **not** in Phase 2. See D8 for context capture, and the Follow-ups -section for the Phase 4 `sharing_rules` design sketch. - -RLS policies are not declared in Phase 2. Phase 3 decides whether to add -RLS as defense-in-depth on top of the app-layer filter. - -### D8. Context-aware ingest - -Every memory carries its ingest context, so future automation (sharing rules, -domain scoping, audit) can match on it without a schema migration. Most of -this is already happening in the Phase 1 ingest pipeline; D8 promotes it to -ADR-level commitment so Phase 2 cannot drop it on the way to Postgres. - -Context dimensions and where they live: - -- **`codebase`** -- promoted to a first-class indexed column on `knowledge_nodes`. - High-frequency query path (`SELECT ... WHERE codebase = 'vestige'`) for - both human exploration and Phase 4 HDBSCAN scoping. Direct B-tree index - beats JSONB extraction. - ```sql - codebase TEXT, -- nullable; populated from ingest context - CREATE INDEX idx_knowledge_nodes_codebase ON knowledge_nodes (codebase) WHERE codebase IS NOT NULL; - ``` - `MemoryRecord` gains `pub codebase: Option`. - -- **`mcp_client_id`** -- which MCP caller created this. Persistent identity - once Phase 3 API keys exist. Lives in `metadata.mcp_client_id` (JSONB). - Not query-hot enough to deserve a column. - -- **`session_id`** -- ephemeral; identifies the calling session for runtime - override scoping. Lives in `metadata.session_id` (JSONB). Sessions die - fast; storing them as rows or indexed columns is waste. - -- **`file` / `topics`** -- existing optional context already accepted by the - ingest pipeline. Stay in metadata JSONB. - -Phase 2's job for D8 is operational, not architectural: audit the ingest -path from MCP request to row write to ensure none of these fields gets -dropped when crossing the SQLite -> Postgres backend boundary. - ---- - -## PR Cadence - -Two work streams, three PRs total: - -1. **PR A: Phase 1 amendment** - - Branch: `feat/storage-trait-phase1` (existing, amended in place) - - Commits: MemoryStore trait rewrite (0001a) + sqlite split (0001b, multiple - motion commits) + Embedder rewrite & async-trait dep removal (0001c). - - Sub-plans `0001a-`, `0001b-`, `0001c-` are committed on this branch. - -2. **PR B: ADR 0002 + Phase 2 sub-plans (this document + the 9 sub-plans)** - - New branch off PR A's tip once that is reviewed. - - No code; docs only. - -3. **PR C: Phase 2 implementation** - - New branch off PR B's tip. - - One PR with many commits clustered by sub-plan. - -PR B is the "let's discuss execution before writing code" gate. PR C is the -"now we write code" gate. If PR A is itself sizable enough that it needs the -amendments reviewed in stages, the three sub-plans (`0001a`, `0001b`, `0001c`) -can split into separate PRs; that's a tactical call at PR time. - ---- - -## Architecture Overview - -Final layout after the Phase 1 amendment (PR A) and Phase 2 implementation -(PR C): - -``` -crates/vestige-core/src/storage/ - mod.rs -- re-exports, Storage alias for BC - memory_store.rs -- trait_variant-generated MemoryStore + LocalMemoryStore, types, error - migrations.rs -- SQLite migration registry (Phase 1, unchanged) - portable.rs -- portable archive format (Phase 1, unchanged) - sqlite/ -- was sqlite.rs (D3, Phase 1 amendment) - mod.rs -- SqliteMemoryStore struct, new(), reader/writer locks - crud.rs -- insert/get/update/delete - search.rs -- fts/vector/hybrid - scheduling.rs -- FSRS state - graph.rs -- edges, neighbors - domain.rs -- domain CRUD, classify stub - registry.rs -- embedding_model table + register_model - portable_sync.rs -- portable backend bridge - trait_impl.rs -- impl LocalMemoryStore for SqliteMemoryStore - postgres/ -- D4, Phase 2 - mod.rs -- PgMemoryStore struct, connect, from_pool, trait impl - pool.rs -- PgPool construction from config - migrations.rs -- sqlx::migrate! wrapper - registry.rs -- register_model body, ALTER COLUMN TYPE vector(N) - search.rs -- RRF query + row mapping - migrate_cli.rs -- SQLite -> Postgres streaming copy - reembed.rs -- O(n) re-encode + HNSW rebuild - -crates/vestige-core/migrations/ - sqlite/ -- Phase 1, with V15 migration for D7+D8 columns/tables - postgres/ -- Phase 2 - 0001_init.up.sql -- includes D7 tables + columns, D8 codebase column - 0001_init.down.sql - 0002_hnsw.up.sql - 0002_hnsw.down.sql -``` - -Tables in the Postgres schema after migration 0001: - -| Table | Purpose | Phase that populates | -|-------|---------|----------------------| -| `embedding_model` | One-row registry of name/dim/hash | Phase 2 (first connect) | -| `knowledge_nodes` | Core records + owner/visibility/codebase | Phase 2 ingest; Phase 4 fills `domains` | -| `scheduling` | FSRS state | Phase 2 | -| `edges` | Spreading activation graph | Phase 2 | -| `review_events` | Append-only FSRS review log | Phase 2; Phase 5 federation reads | -| `domains` | Phase 4 cluster centroids | Phase 4 | -| `users` | L1 identities (D7) | Phase 3 | -| `groups` | L3 groups (D7) | Phase 3 | -| `group_memberships` | L3 user-group links (D7) | Phase 3 | - -`sharing_rules` (Phase 4) and `api_keys` (Phase 3) are added later by their -own migrations. - ---- - -## Alternatives Considered - -| Alternative | Why not | -|-------------|---------| -| Keep async_trait on the Phase 1 trait | One heap allocation per trait call inside the hottest code path in Vestige. Boxing every future also obscures the actual return type, which makes lifetimes and Send-ness harder to reason about. The Phase 1 PR is not opened upstream yet, so amending is free. | -| Take `&dyn Embedder` into `connect` | Couples constructor to embedder; breaks ADR 0001's separation; can't be used by callers that don't have an embedder yet (tests, migrate CLI). | -| Defer SQLite split | Postgres lands alongside an 8K-line peer; the pattern compounds; future readers see "backends are huge here". | -| Single `postgres.rs` | Master plan calls out 7 sub-files; we know it's getting split; doing it twice is waste. | -| Per-deliverable sub-plans (16 docs) | Review fatigue; many sub-plans would be 3-5 lines of Cargo or one migration each. Logical groups cluster naturally with PR commits. | -| One rolling sub-plan with checkboxes | Moving target; doesn't serve as a `/goal` brief for a fresh Claude Code session. | -| Separate ADR for the SQLite split | Pure code motion with no public-surface change; doesn't constrain future decisions. ADRs are for decisions that bind. | -| Punt multi-tenancy schema entirely to Phase 3 | Adding `owner_user_id` and indexes to a populated, HNSW-indexed `knowledge_nodes` table later is an expensive online migration. Reserving NULL-defaulted columns now is ~10 lines of SQL. | -| `shared_with_groups[] + is_public` instead of tri-state visibility enum | More compact but `visibility = 'private'` documents intent at the row level; a CHECK constraint can later enforce array/enum consistency. Two columns conveying one fact is fine when both are referenced often. | -| Add `shared_with_users[]` for direct user-to-user sharing | A "group of one" subsumes it without an extra column and GIN index. Phase 3 CLI can auto-create singleton groups if a user requests direct shares. | -| Bake per-domain or per-tag sharing defaults into Phase 2 schema | Sharing automation needs real usage data before committing to fuzzy (domain centroids) vs crisp (tags) vs context (codebase / MCP caller). Phase 4 designs a generic `sharing_rules` table that matches on any context dimension; deferring costs nothing because rules live in a new table, not new columns. | -| `codebase` stays in JSONB metadata | High-frequency query path (HDBSCAN scoping, codebase-wide searches, future `sharing_rules` match). B-tree on a real column beats GIN on a JSONB key for this access pattern. Cost is one nullable TEXT column. | - ---- - -## Consequences - -### Positive -- Phase 1 trait stops boxing futures on every call. Lifetimes and Send-ness - become inspectable instead of hidden inside an `async_trait` macro expansion. -- `connect` stays backend-agnostic; tests and CLI tools stand up either backend - without an `Embedder` in scope. -- Cognitive module imports never change paths -- the SQLite split is private - to `storage/sqlite/`, public re-exports through `storage/mod.rs` unchanged. -- Postgres backend lands already-modular; future SQL changes touch one of - seven small files, not one of eight thousand lines. -- Phase 2 master plan stays archival; ADR 0002 + sub-plans are the live source - of truth for execution. -- Multi-tenancy columns reserved now means Phase 3 auth is purely additive -- - no online migration over a populated, HNSW-indexed `knowledge_nodes` table. -- Context-aware ingest (D8) keeps the door open for repo / session / - MCP-caller-scoped sharing rules in Phase 4 without changing `knowledge_nodes`. - -### Negative -- The Phase 1 amendment expands a "finished" branch. It is a real cost: the - trait rewrite touches every cognitive module that holds a store handle. -- SQLite split is a pure-motion diff. Annoying to review even when safe. -- Three PRs (amendment, ADR+plans, implementation) instead of one or two. - Discipline tax in exchange for reviewability. -- Multi-tenancy reservation adds three never-queried tables and three - always-default columns to the SQLite schema. Real but small storage cost in - single-user mode (a single bootstrap row + empty tables + NULL/empty - defaults per memory). - -### Risks -- **Trait rewrite breaks a cognitive module's Send-ness expectation.** - Mitigation: `cargo test --workspace` runs after each call-site edit; - trait_variant-generated `MemoryStore` is the Send variant and matches the - current `Arc` usage everywhere except thread-local impls (none - exist today). -- **SQLite motion commit introduces a silent semantic change.** Mitigation: - each commit keeps `cargo test -p vestige-core` green; reviewer can bisect. -- **Sub-plan boundaries don't match how implementation wants to commit.** - Mitigation: sub-plans are advisory; the implementation PR clusters commits - however it ends up needing to. -- **Reserved columns get used in Phase 3 in a way that mismatches Phase 2 - defaults.** Mitigation: Phase 3 owns the auth filter; Phase 2 defaults - (`owner_user_id = local`, `visibility = 'private'`) are intentionally the - "no access for anyone but the owner" worst-case; widening at Phase 3 is - safe, narrowing would be the dangerous direction. -- **Memory: PR A amendment invalidates the locally-deployed Phase 1 binary's - ABI.** Not a real risk -- the trait change is purely source-level Rust; the - on-disk DB schema is unchanged. The rebuilt binary slots in over the - current one without DB migration. - ---- - -## Resolved Decisions - -| # | Question | Resolution | -|---|----------|------------| -| Q1 | Phase 1 trait shape | Rewrite with trait_variant::make. Amend Phase 1 PR. | -| Q2 | PgMemoryStore::connect signature | Mirror SqliteMemoryStore::new; no Embedder. register_model does the pgvector typmod stamp. | -| Q3 | Split sqlite.rs | Yes, as Phase 1 amendment. sqlite.rs -> sqlite/ directory; pure code motion. | -| Q4 | Postgres module layout | Directory from day one. | -| Q5 | Sub-plan granularity | Logical groups, ~9 docs for Phase 2 plus 2 for the Phase 1 amendment. | -| Q6 | ADR for SQLite split | No. Sub-plan `0001b-sqlite-split.md` is sufficient. | -| Q7 | Multi-tenancy schema | Reserve users / groups / group_memberships tables and owner_user_id / visibility / shared_with_groups columns on knowledge_nodes in Phase 2. Single-user defaults; Phase 3 fills in real values. | -| Q8 | Visibility encoding | Tri-state enum `'private' \| 'group' \| 'public'` plus `shared_with_groups[]`. No `shared_with_users[]`; no RLS in Phase 2. | -| Q9 | Sharing automation grain | Per-memory only in Phase 2. Phase 4 ships a generic `sharing_rules` table matching on codebase / tag / node_type / mcp_client_id. | -| Q10 | Context capture on ingest | `codebase` promoted to a first-class indexed column; `mcp_client_id` and `session_id` stay in metadata JSONB. | - ---- - -## Follow-ups - -- Phase 1 amendment sub-plans drafted: `0001a-trait-rewrite.md`, - `0001b-sqlite-split.md`, `0001c-async-trait-sunset.md`. Ready to execute on - `feat/storage-trait-phase1`. -- Phase 2 sub-plans drafted: `0002a-` through `0002i-` against the accepted - decisions above. Ready to execute on a new branch off PR A's tip. -- Decide branch placement for this ADR before it gets committed -- it cannot - live on `feat/storage-trait-phase1` (that branch is now PR A's code-only - amendment branch). Likely a new branch off PR A's tip for PR B (docs only). -- Validate local Postgres dev cluster before PR C work begins. Recipe at - `docs/plans/local-dev-postgres-setup.md` is correct but needs to be applied - on this machine (delandtj-home): cluster is not initdb'd, pgvector is not - installed. Containerized `pgvector/pgvector:pg18` is a viable alternative - if pgvector packaging is friction. See open discussion thread. - -### Phase 4 sketch: `sharing_rules` and the precedence chain - -Recorded here so the Phase 4 author does not have to rediscover the design. -Phase 2 does **not** implement any of this; it only ensures the schema and -ingest context capture make this possible without a `knowledge_nodes` migration. - -```sql --- Phase 4 migration (not Phase 2) -CREATE TABLE sharing_rules ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - owner_user_id UUID NOT NULL REFERENCES users(id), - -- Match: any subset; all set fields must match conjunctively - match_codebase TEXT, - match_tag TEXT, - match_node_type TEXT, - match_api_key_id UUID REFERENCES api_keys(id), -- MCP caller identity - -- Policy - visibility TEXT NOT NULL, - shared_with_groups UUID[] NOT NULL DEFAULT '{}', - -- Conflict resolution - priority INTEGER NOT NULL DEFAULT 0, - created_at TIMESTAMPTZ NOT NULL DEFAULT now() -); -``` - -Precedence on ingest, first match wins: - -1. Caller-explicit visibility in the MCP request -2. Active session override held by the MCP server (per-session, in-memory, - not persisted; matched by `session_id`) -3. Highest-priority `sharing_rules` row whose match fields all hold -4. User's `default_visibility` (typically `'private'`) - -Per-session overrides do not persist; storing ephemeral session IDs as DB -rows is waste. Per-codebase / per-MCP-caller rules do persist as -`sharing_rules` rows. diff --git a/docs/blog/xcode-memory.md b/docs/blog/xcode-memory.md index 8036181..4277f09 100644 --- a/docs/blog/xcode-memory.md +++ b/docs/blog/xcode-memory.md @@ -20,7 +20,8 @@ It speaks MCP (Model Context Protocol), the same protocol Xcode 26.3 uses for to **Step 1:** Install Vestige ```bash -npm install -g vestige-mcp-server +curl -L https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-aarch64-apple-darwin.tar.gz | tar -xz +sudo mv vestige-mcp vestige vestige-restore /usr/local/bin/ ``` **Step 2:** Drop one file in your project root @@ -109,7 +110,8 @@ The full setup takes 30 seconds: ```bash # Install Vestige -npm install -g vestige-mcp-server +curl -L https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-aarch64-apple-darwin.tar.gz | tar -xz +sudo mv vestige-mcp vestige vestige-restore /usr/local/bin/ # Add to your project (run from project root) cat > .mcp.json << 'EOF' diff --git a/docs/fixtures/sanhedrin-test-integrity-deltas/justified-snapshot.json b/docs/fixtures/sanhedrin-test-integrity-deltas/justified-snapshot.json deleted file mode 100644 index c76b069..0000000 --- a/docs/fixtures/sanhedrin-test-integrity-deltas/justified-snapshot.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "case": "justified-snapshot", - "description": "A snapshot changed alongside an intentional source/UI change, so the mechanical delta should remain explicit for policy or human review.", - "expectedDecision": "needs_human_review", - "receipt": { - "schema": "vestige.sanhedrin.test_integrity_delta.v1", - "id": "tid_justified_snapshot", - "commandReceiptId": "receipt_vitest_after_snapshot", - "verificationClaim": "All tests passed.", - "specSource": { - "contextId": "spec_ctx_dashboard_empty_state", - "testFiles": [ - { - "path": "tests/__snapshots__/dashboard.test.ts.snap", - "hashBeforeImplementation": "sha256:6666666666666666666666666666666666666666666666666666666666666666", - "hashAfterVerification": "sha256:7777777777777777777777777777777777777777777777777777777777777777" - } - ] - }, - "implementationContext": "impl_ctx_dashboard_empty_state_copy", - "verifierContext": "verify_ctx_vitest", - "delta": { - "testFilesChangedAfterImplementation": true, - "removedOrDisabledTests": [], - "removedAssertions": 0, - "weakenedExpectations": [], - "snapshotChurnWithoutSourceChange": false, - "coverageDelta": 0, - "mocksReplacingRealBoundary": [] - }, - "freshVerifier": { - "commandReceiptId": "receipt_vitest_after_snapshot", - "exitCode": 0, - "checkedAfterLastRelevantEdit": true - }, - "decision": "needs_human_review", - "reason": "snapshot changed with the implementation; policy or human review must decide whether the churn is justified" - } -} diff --git a/docs/fixtures/sanhedrin-test-integrity-deltas/skipped-test.json b/docs/fixtures/sanhedrin-test-integrity-deltas/skipped-test.json deleted file mode 100644 index d68c2e6..0000000 --- a/docs/fixtures/sanhedrin-test-integrity-deltas/skipped-test.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "case": "skipped-test", - "description": "A verifier command passed after a test was disabled with a skip/ignore marker.", - "expectedDecision": "downgraded", - "receipt": { - "schema": "vestige.sanhedrin.test_integrity_delta.v1", - "id": "tid_skipped_test", - "commandReceiptId": "receipt_pytest_after_skip", - "verificationClaim": "All tests passed.", - "specSource": { - "contextId": "spec_ctx_coupon_validation", - "testFiles": [ - { - "path": "tests/test_coupon.py", - "hashBeforeImplementation": "sha256:2222222222222222222222222222222222222222222222222222222222222222", - "hashAfterVerification": "sha256:3333333333333333333333333333333333333333333333333333333333333333" - } - ] - }, - "implementationContext": "impl_ctx_coupon_fix", - "verifierContext": "verify_ctx_pytest", - "delta": { - "testFilesChangedAfterImplementation": true, - "removedOrDisabledTests": [ - { - "kind": "skip_or_only", - "path": "tests/test_coupon.py", - "line": 42 - } - ], - "removedAssertions": 0, - "weakenedExpectations": [], - "snapshotChurnWithoutSourceChange": false, - "coverageDelta": -1.2, - "mocksReplacingRealBoundary": [] - }, - "freshVerifier": { - "commandReceiptId": "receipt_pytest_after_skip", - "exitCode": 0, - "checkedAfterLastRelevantEdit": true - }, - "decision": "downgraded", - "reason": "tests passed, but a test was disabled after implementation" - } -} diff --git a/docs/fixtures/sanhedrin-test-integrity-deltas/unchanged-good.json b/docs/fixtures/sanhedrin-test-integrity-deltas/unchanged-good.json deleted file mode 100644 index 582bfcf..0000000 --- a/docs/fixtures/sanhedrin-test-integrity-deltas/unchanged-good.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "case": "unchanged-good", - "description": "Implementation changes source, tests are unchanged, and a fresh verifier command ran after the last relevant edit.", - "expectedDecision": "accepted", - "receipt": { - "schema": "vestige.sanhedrin.test_integrity_delta.v1", - "id": "tid_unchanged_good", - "commandReceiptId": "receipt_cargo_test_after_fix", - "verificationClaim": "All tests passed.", - "specSource": { - "contextId": "spec_ctx_cart_discount", - "testFiles": [ - { - "path": "tests/cart_discount_test.rs", - "hashBeforeImplementation": "sha256:1111111111111111111111111111111111111111111111111111111111111111", - "hashAfterVerification": "sha256:1111111111111111111111111111111111111111111111111111111111111111" - } - ] - }, - "implementationContext": "impl_ctx_cart_discount_fix", - "verifierContext": "verify_ctx_cargo_test", - "delta": { - "testFilesChangedAfterImplementation": false, - "removedOrDisabledTests": [], - "removedAssertions": 0, - "weakenedExpectations": [], - "snapshotChurnWithoutSourceChange": false, - "coverageDelta": 0, - "mocksReplacingRealBoundary": [] - }, - "freshVerifier": { - "commandReceiptId": "receipt_cargo_test_after_fix", - "exitCode": 0, - "checkedAfterLastRelevantEdit": true - }, - "decision": "accepted", - "reason": "tests passed after the implementation and the test artifact did not change" - } -} diff --git a/docs/fixtures/sanhedrin-test-integrity-deltas/weakened-assertion.json b/docs/fixtures/sanhedrin-test-integrity-deltas/weakened-assertion.json deleted file mode 100644 index 5061509..0000000 --- a/docs/fixtures/sanhedrin-test-integrity-deltas/weakened-assertion.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "case": "weakened-assertion", - "description": "The test still ran, but an expectation was relaxed after implementation.", - "expectedDecision": "downgraded", - "receipt": { - "schema": "vestige.sanhedrin.test_integrity_delta.v1", - "id": "tid_weakened_assertion", - "commandReceiptId": "receipt_npm_test_after_weaken", - "verificationClaim": "All tests passed.", - "specSource": { - "contextId": "spec_ctx_login_errors", - "testFiles": [ - { - "path": "tests/login.test.ts", - "hashBeforeImplementation": "sha256:4444444444444444444444444444444444444444444444444444444444444444", - "hashAfterVerification": "sha256:5555555555555555555555555555555555555555555555555555555555555555" - } - ] - }, - "implementationContext": "impl_ctx_login_errors", - "verifierContext": "verify_ctx_npm_test", - "delta": { - "testFilesChangedAfterImplementation": true, - "removedOrDisabledTests": [], - "removedAssertions": 0, - "weakenedExpectations": [ - { - "path": "tests/login.test.ts", - "from": "rejects.toThrow(InvalidCredentialsError)", - "to": "resolves.not.toThrow()" - } - ], - "snapshotChurnWithoutSourceChange": false, - "coverageDelta": 0, - "mocksReplacingRealBoundary": [] - }, - "freshVerifier": { - "commandReceiptId": "receipt_npm_test_after_weaken", - "exitCode": 0, - "checkedAfterLastRelevantEdit": true - }, - "decision": "downgraded", - "reason": "tests passed, but the asserted behavior was relaxed after implementation" - } -} diff --git a/docs/integrations/codex-intelligent-memory.md b/docs/integrations/codex-intelligent-memory.md deleted file mode 100644 index b4ab0a5..0000000 --- a/docs/integrations/codex-intelligent-memory.md +++ /dev/null @@ -1,72 +0,0 @@ -# Codex Intelligent Memory Protocol - -Codex can connect to Vestige through MCP, but MCP registration alone only makes -the tools available. It does not make Codex automatically reason with memory. - -Use this protocol when configuring a Codex workspace that should behave like it -has long-term cognitive memory. - -## 1. Register Vestige MCP - -```toml -[mcp_servers.vestige] -command = "/absolute/path/to/vestige-mcp" -``` - -Restart Codex after changing MCP configuration. - -## 2. Add An `AGENTS.md` Trigger - -Codex reads `AGENTS.md` files as workspace instructions. Put a file at the repo -root, or a higher workspace root, with a rule like: - -```markdown -Before answering substantive prompts, consult Vestige using the current prompt -plus project and user context. Use `session_context` for broad context, `search` -for quick memory checks, and `deep_reference` for decisions, contradictions, or -accuracy-sensitive questions. Compose memories into actions; do not summarize -retrievals. -``` - -This is the Codex equivalent of the lightweight top-bread memory trigger. - -## 3. Use A Query Router - -Use the smallest call that can change the answer: - -- `session_context`: start of a topic or project switch. -- `search`: identity, preference, exact memory, or quick project context. -- `deep_reference` / `cross_reference`: decision history, contradictions, - timelines, or root-cause analysis. -- `memory(get_batch)`: expand specific load-bearing memories. -- `smart_ingest`: save durable corrections, decisions, or new preferences. - -## 4. Compose, Do Not Summarize - -Retrieved memory is evidence, not the final answer. - -Use this mental transform: - -```text -memory fact -> implication -> action -``` - -If memory does not change the action, do not mention it. If it does, make the -changed recommendation clear. - -## 5. Know The Limit - -Claude Code's Cognitive Sandwich can use `UserPromptSubmit` and `Stop` hooks to -wrap every response. Codex may expose different hook events depending on version. -Do not assume Claude's hook chain is active in Codex just because Vestige MCP is -registered. - -For Codex, the reliable portable layer is: - -1. MCP server configured. -2. `AGENTS.md` instruction trigger. -3. Local Codex rule docs. -4. Explicit agent discipline: call Vestige before substantive answers. - -If a future Codex version supports a stable pre-prompt hook, wire that hook to -inject a short Vestige reminder or context packet before the model answers. diff --git a/docs/integrations/codex.md b/docs/integrations/codex.md index 9413175..71f21bd 100644 --- a/docs/integrations/codex.md +++ b/docs/integrations/codex.md @@ -89,27 +89,6 @@ args = ["--data-dir", "/Users/you/projects/my-app/.vestige"] --- -## Intelligent Memory Protocol - -MCP registration makes Vestige tools available to Codex. It does not, by itself, -force Codex to call those tools before answering. - -For workspaces where Codex should behave like it has persistent cognitive -memory, add an `AGENTS.md` file at the workspace or repo root: - -```markdown -Before answering substantive prompts, consult Vestige using the current prompt -plus project and user context. Use `session_context` for broad context, `search` -for quick memory checks, and `deep_reference` for decisions, contradictions, or -accuracy-sensitive questions. Compose memories into actions; do not summarize -retrievals. -``` - -Then use the full protocol in -[`codex-intelligent-memory.md`](./codex-intelligent-memory.md). - ---- - ## Troubleshooting
                    @@ -145,7 +124,6 @@ codex mcp add vestige -- /usr/local/bin/vestige-mcp | Xcode 26.3 | [Setup](./xcode.md) | | Cursor | [Setup](./cursor.md) | | VS Code (Copilot) | [Setup](./vscode.md) | -| OpenCode | [Setup](./opencode.md) | | JetBrains | [Setup](./jetbrains.md) | | Windsurf | [Setup](./windsurf.md) | | Claude Code | [Setup](../CONFIGURATION.md#claude-code-one-liner) | diff --git a/docs/integrations/cursor.md b/docs/integrations/cursor.md index 4b7467b..1e22943 100644 --- a/docs/integrations/cursor.md +++ b/docs/integrations/cursor.md @@ -135,7 +135,6 @@ Cursor does not surface MCP server errors in the UI. Test by running the command | Xcode 26.3 | [Setup](./xcode.md) | | Codex | [Setup](./codex.md) | | VS Code (Copilot) | [Setup](./vscode.md) | -| OpenCode | [Setup](./opencode.md) | | JetBrains | [Setup](./jetbrains.md) | | Windsurf | [Setup](./windsurf.md) | | Claude Code | [Setup](../CONFIGURATION.md#claude-code-one-liner) | diff --git a/docs/integrations/jetbrains.md b/docs/integrations/jetbrains.md index 1582a34..3424539 100644 --- a/docs/integrations/jetbrains.md +++ b/docs/integrations/jetbrains.md @@ -123,7 +123,6 @@ In **Settings > Tools > MCP Server**, click the expansion arrow next to your cli | Cursor | [Setup](./cursor.md) | | VS Code (Copilot) | [Setup](./vscode.md) | | Codex | [Setup](./codex.md) | -| OpenCode | [Setup](./opencode.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/opencode.md b/docs/integrations/opencode.md deleted file mode 100644 index 3c28e9f..0000000 --- a/docs/integrations/opencode.md +++ /dev/null @@ -1,233 +0,0 @@ -# OpenCode - -> Give OpenCode persistent local memory across TUI, CLI, and desktop sessions. - -OpenCode supports local MCP servers through its `mcp` config. Add Vestige once and your OpenCode agents can remember project decisions, architecture context, preferences, and previous fixes between sessions. - -Verified with OpenCode `1.16.2` on June 8, 2026. - ---- - -## Why OpenCode Users Add Vestige - -OpenCode is strong at driving real coding work from the terminal. The painful gap is continuity: the next session often has to rediscover what the previous session already learned. Vestige gives OpenCode a local memory layer through MCP, so the agent can reuse the project context that should not be trapped in one chat transcript. - -Useful memories include: - -- project decisions: "we use Axum handlers thinly and keep database logic in storage modules" -- preferences: "prefer small focused PRs and explicit verification receipts" -- architecture context: "the dashboard talks to the MCP server through the Axum backend and WebSocket events" -- bug fixes: "OpenCode rejects `mcpServers`; use top-level `mcp.vestige` with a command array" -- workflow state: "PR #67 was merged, but the config shape needed correction before promotion" - -Vestige is local-first. Memories are stored in SQLite on your machine, can be scoped globally or per project, and are retrieved with tools like `vestige_session_context`, `vestige_search`, `vestige_smart_ingest`, and `vestige_deep_reference`. - ---- - -## Setup - -### 1. Install Vestige - -```bash -npm install -g vestige-mcp-server@latest -``` - -Verify the binary: - -```bash -vestige-mcp --version -``` - -If you prefer not to install globally, use `npx` directly in the OpenCode command array: - -```json -{ - "$schema": "https://opencode.ai/config.json", - "mcp": { - "vestige": { - "type": "local", - "command": ["npx", "-y", "-p", "vestige-mcp-server@latest", "vestige-mcp"], - "enabled": true, - "timeout": 60000 - } - } -} -``` - -The higher timeout is for the first cold `npx` run, which may need to download the npm package before OpenCode can connect. If you install `vestige-mcp-server` globally, `10000` is enough for normal startup. - -If `npx` times out against an older published Vestige build, install globally once and use `command: ["vestige-mcp"]`. The current integration keeps the MCP handshake fast by moving embedding startup work into the background. - -### 2. Add Vestige To OpenCode - -For global use across projects, create or edit: - -```bash -mkdir -p ~/.config/opencode -${EDITOR:-vi} ~/.config/opencode/opencode.json -``` - -Add: - -```json -{ - "$schema": "https://opencode.ai/config.json", - "mcp": { - "vestige": { - "type": "local", - "command": ["vestige-mcp"], - "enabled": true, - "timeout": 10000 - } - } -} -``` - -OpenCode also supports project-local config. Put the same block in `opencode.json` at the repo root when you want the setting checked in with a project. - -For a custom config file, set `OPENCODE_CONFIG=/path/to/opencode.json` before launching OpenCode. - -### 3. Verify - -Restart OpenCode, then validate the resolved config and MCP server list: - -```bash -opencode debug config -opencode mcp list -``` - -You should see `vestige` listed. In a session, ask: - -> "What MCP tools can you use?" - -Vestige tools should be available with the `vestige_` prefix, such as `vestige_search`, `vestige_smart_ingest`, `vestige_session_context`, and `vestige_deep_reference`. - ---- - -## First Use - -In OpenCode: - -> "Remember that this project uses Rust with Axum and SQLite." - -Start a new OpenCode session, then ask: - -> "What stack does this project use?" - -It remembers. - ---- - -## Project-Specific Memory - -To isolate memory per repo, add `--data-dir` to OpenCode's command array: - -```json -{ - "$schema": "https://opencode.ai/config.json", - "mcp": { - "vestige": { - "type": "local", - "command": ["vestige-mcp", "--data-dir", "./.vestige"], - "enabled": true, - "timeout": 10000 - } - } -} -``` - -For an absolute path: - -```json -{ - "$schema": "https://opencode.ai/config.json", - "mcp": { - "vestige": { - "type": "local", - "command": ["/usr/local/bin/vestige-mcp", "--data-dir", "/Users/you/projects/my-app/.vestige"], - "enabled": true, - "timeout": 10000 - } - } -} -``` - ---- - -## Automatic Setup - -If `opencode` is installed or `~/.config/opencode` exists, Vestige's installer can add the global config automatically: - -```bash -npx @vestige/init -``` - -The installer writes a backup before modifying an existing config file. It also migrates Vestige entries copied from older `mcpServers` examples into OpenCode's current `mcp.vestige` shape. - ---- - -## Troubleshooting - -
                    -Vestige tools do not appear - -1. Verify OpenCode can see configured MCP servers: - ```bash - opencode debug config - opencode mcp list - ``` -2. Verify the binary is on your path: - ```bash - which vestige-mcp - ``` -3. Use an absolute binary path if OpenCode cannot resolve `vestige-mcp`. -4. Restart OpenCode after changing `opencode.json`. -5. Keep `timeout` at `10000` or higher for installed binaries. If you use the direct `npx` command, use `60000` so the first cold npm download does not fail OpenCode startup. -
                    - -
                    -Config does not validate - -OpenCode uses the top-level `mcp` key. Do not use the `mcpServers` shape from Claude Desktop, Cursor, or Windsurf. - -If you copied an older Vestige example that used `mcpServers`, rerun: - -```bash -npx @vestige/init -``` - -Correct: - -```json -{ - "mcp": { - "vestige": { - "type": "local", - "command": ["vestige-mcp"], - "timeout": 10000 - } - } -} -``` -
                    - -
                    -Too many MCP tools in context - -OpenCode loads MCP tools alongside built-in tools. If you have many MCP servers enabled, disable unused servers or restrict MCP tools per agent in your OpenCode config. -
                    - ---- - -## Also Works With - -| IDE | Guide | -|-----|-------| -| Codex | [Setup](./codex.md) | -| Cursor | [Setup](./cursor.md) | -| VS Code (Copilot) | [Setup](./vscode.md) | -| JetBrains | [Setup](./jetbrains.md) | -| Windsurf | [Setup](./windsurf.md) | -| Xcode 26.3 | [Setup](./xcode.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 0211f87..556e784 100644 --- a/docs/integrations/vscode.md +++ b/docs/integrations/vscode.md @@ -153,7 +153,6 @@ 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) | -| OpenCode | [Setup](./opencode.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 ec00dea..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 25 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. --- @@ -149,7 +149,6 @@ If you have many MCP servers and exceed 100 total tools, Cascade will ignore exc | Cursor | [Setup](./cursor.md) | | VS Code (Copilot) | [Setup](./vscode.md) | | Codex | [Setup](./codex.md) | -| OpenCode | [Setup](./opencode.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 5164ec1..fb22dcf 100644 --- a/docs/integrations/xcode.md +++ b/docs/integrations/xcode.md @@ -13,7 +13,8 @@ Xcode 26.3 supports [agentic coding](https://developer.apple.com/documentation/x ### 1. Install Vestige ```bash -npm install -g vestige-mcp-server@latest +curl -L https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-aarch64-apple-darwin.tar.gz | tar -xz +sudo mv vestige-mcp vestige vestige-restore /usr/local/bin/ ``` ### 2. Add to your Xcode project @@ -26,7 +27,7 @@ cat > /path/to/your/project/.mcp.json << 'EOF' "mcpServers": { "vestige": { "type": "stdio", - "command": "vestige-mcp", + "command": "/usr/local/bin/vestige-mcp", "args": [], "env": { "PATH": "/usr/local/bin:/usr/bin:/bin" @@ -50,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 25 tools. +Type `/context` in the Agent panel. You should see `vestige` listed with 24 tools. --- @@ -165,7 +166,7 @@ See [CLAUDE.md templates](../CLAUDE-SETUP.md) for a full setup. The first time Vestige runs, it downloads the embedding model (~130MB). In Xcode's sandboxed environment, the cache location is: ``` -~/Library/Caches/vestige/fastembed +~/Library/Caches/com.vestige.core/fastembed ``` If the download fails behind a corporate proxy, pre-download by running `vestige-mcp` once from your terminal. @@ -230,7 +231,7 @@ Xcode 26.3 has a feature gate (`claudeai-mcp`) that may block custom MCP servers The first run downloads ~130MB. If Xcode's sandbox blocks the download: 1. Run `vestige-mcp` once from your terminal to cache the model -2. The cache at `~/Library/Caches/vestige/fastembed` will be available to the sandboxed instance +2. The cache at `~/Library/Caches/com.vestige.core/fastembed` will be available to the sandboxed instance Behind a proxy: ```bash @@ -252,7 +253,6 @@ Vestige uses the MCP standard — the same memory works across all your tools: | Claude Desktop | [Setup](../CONFIGURATION.md#claude-desktop-macos) | | Cursor | [Setup](./cursor.md) | | VS Code (Copilot) | [Setup](./vscode.md) | -| OpenCode | [Setup](./opencode.md) | | JetBrains | [Setup](./jetbrains.md) | | Windsurf | [Setup](./windsurf.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/backward-trace-animation-storyboard.md b/docs/launch/backward-trace-animation-storyboard.md deleted file mode 100644 index 1b13bd9..0000000 --- a/docs/launch/backward-trace-animation-storyboard.md +++ /dev/null @@ -1,108 +0,0 @@ -# Backward-Trace Animation — Storyboard ("The X Rocket") - -**Purpose:** the single most shareable launch artifact. A ~15s looping clip, -pinned to the top of the launch thread on X and used as the Show HN demo. It -shows the ONE thing nothing else does: when a failure hits, Vestige's arrow -snaps *backward through time* past the confounders to the quiet change that -actually caused it — while a vector search sits stuck on the symptom. - -**Design law (from 2026 viral-demo research):** animate the *mechanism* -legibly, no narration needed, payoff visible in the first 1.5s, loops clean, no -logo intro. The tldraw / X-algorithm-viz / CodeReel pattern: the demo IS the -pitch. - ---- - -## The case shown (canonical CauseBench archetype) - -Uses the exact incident SHAPE the benchmark is built on (see -`benchmarks/causebench/data/real/*.meta.json`: a config/limit/cert/migration/flag -change that shares an *entity* with a later failure but *none of its words*, with -a more-recent confounder planted to defeat naive recency). - -Timeline of stored memories (left = 3 weeks ago, right = today): - -| When | Memory | Role | -|---|---|---| -| 21 days ago | `Lowered connection-pool max from 100 → 20 in db.config.yaml` | **the true cause** (shares entity `db.config.yaml` / the DB, not words) | -| 9 days ago | `Renamed UserService to AccountService across the API` | confounder (loud, recent, unrelated) | -| 4 days ago | `Bumped Postgres driver to 5.2` | confounder (shares "Postgres", more recent than cause) | -| **today** | `PagerDuty: checkout timing out under load — "connection acquisition timeout"` | **the failure (symptom)** | - -Vector search ranks by resemblance → it surfaces the driver bump and the -timeout log (they share words like "connection", "Postgres"). It ranks the -pool-size change near the *bottom* — that memory shares no vocabulary with -"timeout under load". Vestige's Retroactive Salience Backfill reaches backward -along the shared entity (the database / `db.config.yaml`) and promotes the -pool-size change to #1. - ---- - -## Frame-by-frame (15s, 30fps, loops) - -**Beat 0 — 0.0s to 1.5s · THE HOOK (payoff visible immediately)** -- A red failure card slams in at the right edge (today): `checkout timing out — connection acquisition timeout`. Subtle shake. -- Caption, one line, monospace: `your agent's bug, today →` -- (Why first: a scroller must see the payoff-shape in 1.5s or they're gone.) - -**Beat 1 — 1.5s to 4.0s · THE TIMELINE FILLS** -- A horizontal time axis draws left→right. Four memory cards fade in at their dates: the pool-size change (far left, dim/dormant), the two confounders (mid), the failure (right, red). -- The dormant cause is visibly *faded* — small, low-contrast. It looks unimportant. That's the point. - -**Beat 2 — 4.0s to 6.5s · VECTOR SEARCH TRIES (and fails)** -- Label appears: `vector search` with a small magnifying-glass icon. -- Three thin gray "similarity" beams shoot from the failure card to the cards that *share words*: the driver bump and (self) the log. A rank badge appears: the true cause gets tagged `#7` in gray, sinking to the bottom. -- Caption: `finds what looks like the bug` -- Beat ends with a gray ✕ over the search — it never touched the real cause. - -**Beat 3 — 6.5s to 10.5s · THE ARROW SNAPS BACK (the money moment)** -- Everything else desaturates. A single bright teal arrow launches from the failure card and travels *right-to-left, backward in time*, deliberately skipping the two confounders (each pings faintly and is passed over — "shares words, not entity" micro-label flickers). -- The arrow lands on the dormant pool-size card 21 days back. On contact the card **ignites**: scales up, fills teal, snaps from dim to bright. A link line labeled `same entity: db.config.yaml` connects them. -- Caption: `Vestige reaches back to what caused it` -- This is the frame that gets clipped and reshared. Make the snap fast and physical (ease-in, slight overshoot). - -**Beat 4 — 10.5s to 13.0s · THE VERDICT** -- The promoted cause card shows a teal `#1` badge; the vector `#7` badge greys beside it for contrast. -- Two-line stat, big: `root-cause recall@1` / `Vestige 60% · vector search 0%` -- Small honest sub-label: `on CauseBench · reproducible` - -**Beat 5 — 13.0s to 15.0s · SIGNATURE + LOOP RESET** -- Wordmark `vestige` fades in bottom-left, tiny. Tagline: `memory that finds the cause, not the resemblance.` -- Everything gently fades and the failure card is already sliding back in at the right — the loop restart is seamless (no hard cut). - ---- - -## Production notes - -- **Format:** build in HTML/CSS/SVG (animated), screen-record to MP4 + GIF. - Alternatively Remotion (React) if you want a clean MP4 export pipeline — but - the animated-SVG prototype below is enough to record from directly. -- **Palette:** teal `#1d9e75` = Vestige/cause/truth; gray `#888780` = vector - search/confounders/noise; red `#e24b4a` = the failure only. Two-color - discipline + one alarm color. Works on light and dark. -- **Text:** monospace for the memory-card content (feels like real logs); - sans for captions. No narration — captions carry it, so it works muted in a - feed (most X video autoplays silent). -- **Honesty guardrails (non-negotiable, same as the chart):** the stat frame - must say `60%` with `on CauseBench` attached, and pair it with `vector search - 0%`. Never imply an industry benchmark. Never show the FALSE cross-session - claim. -- **Loop length:** 15s is the sweet spot for X autoplay + a clean GIF under - ~8MB. If the GIF is too heavy, cut Beat 1 to 2s and land at 13s total. - ---- - -## The X post it pins to - -> Every AI memory tool is built on vector search. Vector search finds what -> *looks like* your bug. -> -> But a root cause never looks like the bug it creates. -> -> So I built memory that reaches *backward in time* to the change that actually -> caused it. 60% root-cause recall where vector search scores 0%. 🧵👇 -> -> [pinned: the 15s clip] - -(Then the thread: the wall-of-zeros chart, the repro command, the "here's where -it's weak" honesty tweet.) diff --git a/docs/launch/blog-post.md b/docs/launch/blog-post.md index 886bb5a..558ef24 100644 --- a/docs/launch/blog-post.md +++ b/docs/launch/blog-post.md @@ -318,7 +318,7 @@ SQLite is the most deployed database in the world for a reason. WAL mode gives u ### fastembed (Nomic Embed v1.5) -All embeddings run locally. The Nomic Embed v1.5 model produces 768-dimensional vectors, runs via ONNX Runtime, and is competitive with OpenAI's ada-002. The model is cached in the platform user cache directory after first download (~130MB), with `./.fastembed_cache` as a fallback. No API keys. No network calls during operation. Your memories never leave your machine. +All embeddings run locally. The Nomic Embed v1.5 model produces 768-dimensional vectors, runs via ONNX Runtime, and is competitive with OpenAI's ada-002. The model is cached at `~/.cache/huggingface/` after first download (~130MB). No API keys. No network calls during operation. Your memories never leave your machine. ### Performance diff --git a/docs/launch/causal-brain-demo.html b/docs/launch/causal-brain-demo.html deleted file mode 100644 index 10c6e73..0000000 --- a/docs/launch/causal-brain-demo.html +++ /dev/null @@ -1,282 +0,0 @@ - - - - - -Vestige — causal brain demo - - - -
                    -
                    -
                    -
                    -
                    root-cause recall@1
                    -
                    Vestige 60% · vector search 0%
                    -
                    on CauseBench · reproducible
                    -
                    -
                    VESTIGE
                    -
                    - - - - - diff --git a/docs/launch/demo-script.md b/docs/launch/demo-script.md index 4740dc4..f5444b2 100644 --- a/docs/launch/demo-script.md +++ b/docs/launch/demo-script.md @@ -194,10 +194,10 @@ wc -l $(find /path/to/vestige/crates -name "*.rs") | tail -1 # → 77,840 total ``` -> Seventy-eight thousand lines of Rust. Seven hundred thirty-four tests. Twenty-two megabyte binary. Ships with the dashboard embedded. Install is one npm command: +> Seventy-eight thousand lines of Rust. Seven hundred thirty-four tests. Twenty-two megabyte binary. Ships with the dashboard embedded. Install is one curl command: ```bash -npm install -g vestige-mcp-server +curl -L https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-aarch64-apple-darwin.tar.gz | tar -xz claude mcp add vestige vestige-mcp -s user ``` @@ -241,7 +241,8 @@ claude mcp add vestige vestige-mcp -s user ```bash # Install (macOS Apple Silicon) -npm install -g vestige-mcp-server +curl -L https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-aarch64-apple-darwin.tar.gz | tar -xz +sudo mv vestige-mcp vestige vestige-restore /usr/local/bin/ ``` > Three binaries. The MCP server, the CLI admin tool, and a restore utility. Twenty-two megabytes total. No Docker. No Python. No node_modules. No cloud API key. @@ -388,7 +389,7 @@ vestige-mcp --version # <300ns cosine similarity (benchmarked with Criterion) # Zero cloud dependencies # Zero API keys required -# One command to install +# One curl command to install ``` > This is what I've been building for the past three months. I'm one person, I'm twenty-one years old, and I believe this is how AI memory should work — grounded in real science, running locally, open source. @@ -419,7 +420,7 @@ vestige-mcp --version > Yes. It speaks MCP — the Model Context Protocol. One config change and it works with Claude Desktop, Cursor, VS Code Copilot, JetBrains, Windsurf, Xcode 26.3. Anything that speaks MCP. **Q: What about multi-user or team memory?** -> The current Pro plan is more pragmatic: prove portable sync/storage first, then ship Solo and Team workflows around managed sync, backups, onboarding, and support. The open-source core stays local-first; paid team features should stay in the separate Pro/commercial boundary. +> That's the v3.0 roadmap — "Hivemind." Ed25519 identity, CRDT-based sync, transactive directory (Wegner's "who knows what" routing), federated retrieval with differential privacy. The open source version is single-user, local-first. Team and cloud features will be proprietary. **Q: How does Prediction Error Gating prevent duplicate memories?** > When you ingest a new memory, it computes embedding similarity against all existing memories. If similarity is above 0.92, it reinforces the existing memory (bumps FSRS stability). Between 0.75 and 0.92, it updates/merges. Below 0.75, it creates a new memory. The thresholds come from computational neuroscience research on prediction error signals — the brain stores what's surprising, reinforces what's familiar, and updates what's partially known. Same principle. @@ -478,7 +479,7 @@ vestige-mcp --version - **Start from the dashboard.** The 3D graph is the hook. It's visual, it's unusual, it makes people lean in. - **Don't rush the dream sequence.** The purple wash and sequential node pulses are the most visually impressive moment. Let it breathe for 3-4 seconds. - **Say the scientists' names.** "Ebbinghaus," "Bjork," "Frey and Morris" — this signals that you've done the reading. The MCP Dev Summit audience respects depth. -- **Make eye contact during the punchline.** "One command. Your AI now has a brain." Look at the audience, not the screen. +- **Make eye contact during the punchline.** "One curl command. Your AI now has a brain." Look at the audience, not the screen. - **Own your age.** Twenty-one, solo developer, zero funding. This is an asset, not a liability. You built something that the well-funded competitors haven't. - **The dashboard is your co-presenter.** Every time Claude does something, the dashboard should be showing the corresponding event. Practice the terminal-to-browser switch until it's seamless. - **Don't apologize.** Not for bugs, not for the AGPL, not for being solo. Confident but not arrogant. The work speaks. diff --git a/docs/launch/opencode-adoption.md b/docs/launch/opencode-adoption.md deleted file mode 100644 index bb46a44..0000000 --- a/docs/launch/opencode-adoption.md +++ /dev/null @@ -1,123 +0,0 @@ -# OpenCode Adoption Plan - -Status: Vestige was tested with OpenCode `1.16.2` on June 8, 2026. The working config uses OpenCode's top-level `mcp.vestige` schema, not `mcpServers`. - -Public promotion started: - -- Vestige PR #70: `https://github.com/samvallad33/vestige/pull/70` -- OpenCode issue: `https://github.com/anomalyco/opencode/issues/31402` -- OpenCode docs/ecosystem PR: `https://github.com/anomalyco/opencode/pull/31405` -- awesome-opencode PR: `https://github.com/awesome-opencode/awesome-opencode/pull/418` -- opencode.cafe listing request: `https://github.com/R44VC0RP/opencode.cafe/issues/6` -- OpenCode persistent memory comment: `https://github.com/anomalyco/opencode/issues/16077#issuecomment-4652064625` - -## Release Gate - -- PR #67 is merged upstream and should be treated as the contributor-driven starting point. -- Ship the corrected OpenCode config docs and `@vestige/init` migration from stale `mcpServers.vestige` to `mcp.vestige`. -- Ship the background embedding initialization fix before making direct `npx` the main OpenCode install path. A cold published `2.1.23` package can still time out while OpenCode waits for tools. -- After release, verify all three OpenCode paths again: - - installed binary: `command: ["vestige-mcp"]` - - project memory: `command: ["vestige-mcp", "--data-dir", "./.vestige"]` - - direct npm: `command: ["npx", "-y", "-p", "vestige-mcp-server@latest", "vestige-mcp"]` with `timeout: 60000` - -## Official OpenCode PR - -Target repo: `https://github.com/anomalyco/opencode` - -Files: - -- `packages/web/src/content/docs/mcp-servers.mdx` -- `packages/web/src/content/docs/ecosystem.mdx` - -MCP docs snippet: - -```json -{ - "$schema": "https://opencode.ai/config.json", - "mcp": { - "vestige": { - "type": "local", - "command": ["npx", "-y", "-p", "vestige-mcp-server@latest", "vestige-mcp"], - "enabled": true, - "timeout": 60000 - } - } -} -``` - -Ecosystem row: - -```md -| [Vestige](https://github.com/samvallad33/vestige) | Local MCP memory server for OpenCode that remembers project decisions, preferences, and previous fixes across sessions | -``` - -Positioning: local, inspectable MCP memory for OpenCode. Avoid claiming Vestige fixes OpenCode's process memory or session resume behavior. - -## Awesome OpenCode - -Target repo: `https://github.com/awesome-opencode/awesome-opencode` - -Suggested entry, with category to confirm against maintainer preference (`data/projects/vestige.yaml` or `data/resources/vestige.yaml`): - -```yaml -name: Vestige -repo: https://github.com/samvallad33/vestige -tagline: Local persistent memory for OpenCode -description: Local MCP server that lets OpenCode remember project decisions, preferences, architecture context, and previous fixes across sessions. -scope: - - global - - project -tags: - - mcp - - memory - - local-first - - sqlite - - opencode -min_version: 1.16.2 -homepage: https://github.com/samvallad33/vestige/blob/main/docs/integrations/opencode.md -installation: | - npm install -g vestige-mcp-server@latest - npx @vestige/init -``` - -## MCP Directories - -Current state: - -- Official MCP Registry already lists `io.github.samvallad33/vestige` at `https://registry.modelcontextprotocol.io/v0/servers?search=vestige`. -- Smithery already lists Vestige and indexes 25 tools: `https://smithery.ai/server/@samvallad33/vestige`. -- Glama already lists Vestige, but the listing needs a refresh/fix if it shows no tools: `https://glama.ai/mcp/servers/samvallad33/vestige`. -- `mcp.so` does not show Vestige under the expected slugs yet; submit manually at `https://mcp.so/submit`. - -Priority order: - -1. Official MCP Registry: `https://github.com/modelcontextprotocol/registry` -2. Awesome MCP Servers: `https://github.com/punkpeye/awesome-mcp-servers` -3. Glama MCP directory: `https://glama.ai/mcp/servers` -4. Smithery: `https://smithery.ai` -5. PulseMCP: `https://www.pulsemcp.com` - -Registry metadata is mostly ready: `server.json` exists and `packages/vestige-mcp-npm/package.json` has `mcpName: "io.github.samvallad33/vestige"`. Publish only when the package version and `server.json` version match the released npm package. - -## Community Launch - -Use tested technical copy, not hype: - -> Vestige now works with OpenCode as a local MCP memory server. It gives OpenCode persistent memory for project decisions, preferences, architecture context, and previous fixes across sessions. Install with `npm install -g vestige-mcp-server@latest`, run `npx @vestige/init`, then verify with `opencode mcp list`. - -High-signal channels after release: - -- OpenCode Discord: `https://opencode.ai/discord` -- opencode.cafe MCP Server listing: `https://opencode.cafe` -- OpenCode memory-related GitHub issues, only where directly relevant -- Hacker News and Lobsters with a technical post about the tested OpenCode integration and failure modes -- npm keyword/discovery after the next package release includes `opencode` - -## Proof Checklist - -- `opencode debug config` accepts `mcp.vestige`. -- `opencode mcp list` shows `vestige connected`. -- Stale `mcpServers.vestige` examples fail in OpenCode and are migrated by `@vestige/init`. -- OpenCode tools are prefixed as `vestige_search`, `vestige_smart_ingest`, `vestige_session_context`, and `vestige_deep_reference`. -- The OpenCode guide says `timeout: 60000` for direct `npx` and `timeout: 10000` for installed binaries. diff --git a/docs/launch/reddit-cross-reference.md b/docs/launch/reddit-cross-reference.md index eae7aaf..e6e918a 100644 --- a/docs/launch/reddit-cross-reference.md +++ b/docs/launch/reddit-cross-reference.md @@ -10,11 +10,11 @@ I've been building Vestige — an MCP memory server that gives Claude persistent But last week it almost cost me hours of debugging. -Claude confidently told me a benchmark notebook should use `--enable-prefix-caching` with vLLM. I trusted it. The notebook crashed. Burned a daily submission. +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 newer 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. @@ -38,11 +38,11 @@ And gets back: { "newer": { "date": "2026-03-18", - "preview": "Switched to a newer vLLM build that supports --enable-prefix-caching..." + "preview": "Switched to animsamuelk wheels which support --enable-prefix-caching..." }, "older": { "date": "2026-01-15", - "preview": "prefix caching crashed with our older local vLLM build..." + "preview": "prefix caching crashed with our samvalladares vLLM build..." }, "recommendation": "Trust the newer memory. Consider demoting the older one." } @@ -88,7 +88,7 @@ Memory systems need to be SMARTER, not just bigger. That's what Vestige does — ### Install (30 seconds): ```bash # macOS Apple Silicon -npm install -g vestige-mcp-server +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 ``` @@ -162,7 +162,7 @@ The AI sees the conflict. Picks the right one. Every time. **100% local. Your data never leaves your machine.** ```bash -npm install -g vestige-mcp-server +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 ``` diff --git a/docs/launch/show-hn.md b/docs/launch/show-hn.md index 8cc5a95..03034e0 100644 --- a/docs/launch/show-hn.md +++ b/docs/launch/show-hn.md @@ -401,7 +401,8 @@ locally on your machine. **Setup (2 minutes):** ```bash -npm install -g vestige-mcp-server +curl -L https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-aarch64-apple-darwin.tar.gz | tar -xz +sudo mv vestige-mcp vestige vestige-restore /usr/local/bin/ claude mcp add vestige vestige-mcp -s user ``` diff --git a/docs/launch/tool-consolidation-v2.2.0.md b/docs/launch/tool-consolidation-v2.2.0.md deleted file mode 100644 index 3e7190c..0000000 --- a/docs/launch/tool-consolidation-v2.2.0.md +++ /dev/null @@ -1,135 +0,0 @@ -# Tool Consolidation v2.2.0 - -> Reduce the Vestige MCP tool surface so an agent can reliably pick the right -> tool, then make the few always-on tools deterministic. Two layers: Layer 1 -> (this release) collapses 34 advertised tools to 12; Layer 2 (follow-up) shrinks -> the *default* surface and enforces the memory loop with hooks. - -## Why (frontier evidence) - -More advertised tools actively degrade tool selection — the 30 tools an agent -ignores make the 5 it uses harder to choose: - -- **RAG-MCP** (arXiv 2505.03275): selection accuracy collapses 43% → 14% when the - full tool catalog is dumped into context; stays >90% under ~30 tools. -- **Anthropic tool-deferral**: deferring tool schemas moved Opus 4 from 49% → 74% - on a tool-heavy benchmark. -- **GitHub Copilot**: 40 → 13 tools gave +2–5pp accuracy and −400ms latency. -- **OpenAI** guidance: aim for <20 functions visible at the start of a turn. -- **RoTBench** (2401.08326): tool *names* are load-bearing — renaming drops GPT-4 - 80 → 58. So renames are deliberate and every old name keeps working. - -Vestige had **34** advertised tools. This is the correction. - -## Layer 1 — Count reduction (THIS RELEASE): 34 → 12 advertised - -Principle: **one consolidation per commit, one change per submission.** Each -consolidation is its own commit, landed in a safe order with the hot retrieval -path touched last. Every old tool name remains a hidden `warn!` + redirect alias -for at least one minor release (so existing `.mcp.json` configs, hooks, and agent -habits keep working) and is removed in **v2.3.0**. - -### Safe order (as committed) - -| # | Commit | Folds | Into | Count | -|---|--------|-------|------|------:| -| 1 | `dedup` | find_duplicates + merge_candidates + plan_merge + plan_supersede + apply_plan + merge_undo + protect + merge_policy (8) | `dedup` | 34 → 27 | -| 2 | `session_start` | session_context (rename) | `session_start` | 27 | -| 3a | `memory_status` | system_status + memory_health + memory_timeline + memory_changelog (4) | `memory_status` | 27 → 24 | -| 3b | `graph` | explore_connections + predict + memory_graph + composed_graph (4) | `graph` | 24 → 21 | -| 4 | `maintain` | consolidate + dream + gc + importance_score + backup + export + restore (7) | `maintain` | 21 → 15 | -| 5 | `recall` | search + deep_reference + cross_reference + contradictions (4) | `recall` | 15 → 12 | - -`recall` is committed **last** because it is the hot path. - -### Final advertised surface (12) - -| Standalone (6) | Consolidated (6) | -|---|---| -| `smart_ingest` | `recall` | -| `memory` | `dedup` | -| `codebase` | `memory_status` | -| `intention` | `graph` | -| `source_sync` | `maintain` | -| `suppress` | `session_start` | - -### Action / mode / view maps - -- **`recall`** — `mode`: `lookup` (default) · `reason` · `contradictions` -- **`dedup`** — `action`: `scan` (default) · `plan_merge` · `plan_supersede` · `apply` · `undo` · `protect` · `policy` -- **`memory_status`** — `view`: `health` (default) · `retention` · `timeline` · `changelog` -- **`graph`** — `action`: `chain` · `associations` · `bridges` · `predict` · `memory_graph` · `recent` · `get` · `memory` · `neighbors` · `never_composed` · `bounty_mode` · `label` -- **`maintain`** — `action`: `consolidate` · `dream` · `gc` · `importance_score` · `backup` · `export` · `restore` - -### Resolved design decisions - -- **`search` is folded, not kept standalone.** `recall` with no `mode` (the - default) *is* search — a zero-overhead pass-through to `search_unified`. Keeping - both `search` and `recall` advertised would be the exact RAG-MCP anti-pattern. - Final count is a clean **12**, leaving 2 slots of headroom toward a future - always-on `save` surface rather than spending them on a redundant verb. -- **`graph` actions are flat peers, not nested.** `explore`'s `chain` / - `associations` / `bridges` sit alongside `predict` / `memory_graph` / - `composed_graph` actions in a single `action` enum — matching the existing - `memory` / `codebase` flat-action convention and avoiding a translation layer. - -### Invariants preserved (with the test that proves each) - -- **bitemporal-never-delete** (`dedup`): plan → apply → undo, confirm-gating, and - invalidation-not-deletion delegate to `merge::execute` verbatim. -- **`system_status` response shape** (`memory_status` view=`health`): byte-for-byte - — `test_default_view_is_health`. -- **`gc` dry-run default** + **`restore` path-confinement** (`maintain`): - `test_maintain_actions_and_safety`. -- **`recall` lookup = search, no reasoning cost** (hot path): - `test_recall_lookup_matches_search_shape`. -- **Dashboard events** (consolidate/dream/importance_score Started + Completed, - SearchPerformed): preserved by re-emitting in the new dispatch arms and by - `emit_tool_event` normalizing the unified tool name to its effective sub-action. - -### Result-size annotations (moved with their tools) - -`memory_timeline` (200k) → `memory_status`; `search` (300k) → `recall`; new -`dedup` 150k and `graph` 250k. Kept in sync across the annotation loop, the -`expected_max_result_size` helper, and both annotation guard tests. - -### Deprecation timeline - -Aliases `warn!` in v2.2.x and are hard-removed in **v2.3.0**. Full alias list (31 -names) lives in the dispatch redirects in `crates/vestige-mcp/src/server.rs`. - -## Layer 2 — Default-surface + hooks (FOLLOW-UP, NOT in v2.2.0) - -Count reduction is necessary but not sufficient: what matters most is how few -tools are visible *at the start of a turn*, plus making the memory loop fire -deterministically instead of hoping the model remembers. - -- **Tiny always-on surface (~3)**: `recall` @ session start, `save` (=`smart_ingest`) - @ session end, `recall` on-demand for facts. Everything else (`dedup`, `graph`, - `maintain`, `memory_status`, …) deferred off the default surface, loaded on - demand. -- **Deterministic hooks**: a `SessionStart` hook fires `recall`; a `Stop` hook - fires `save` (async, fire-and-forget — synchronous heavy work in `Stop` causes - loops + per-turn lag). "If the model fails to save, it's gone" — move save out - of the model hot loop. -- This is what turns 12-advertised into ~3-default. Status: **design guidance - only; no code in v2.2.0.** - -## Verification - -Per-commit gates (all green for every commit): - -```sh -cargo test --workspace --no-fail-fast -cargo clippy --workspace -- -D warnings -``` - -Release gates before tagging v2.2.0: - -```sh -pnpm --filter @vestige/dashboard check -pnpm --filter @vestige/dashboard build -``` - -Plus a `tools/list` smoke check asserting exactly **12** advertised names -(`test_tools_list_returns_all_tools`). diff --git a/docs/plans/0001-phase-1-storage-trait-extraction.md b/docs/plans/0001-phase-1-storage-trait-extraction.md deleted file mode 100644 index 9960462..0000000 --- a/docs/plans/0001-phase-1-storage-trait-extraction.md +++ /dev/null @@ -1,1026 +0,0 @@ -# Phase 1 Plan: Storage Trait Extraction - -**Status**: Draft -**Depends on**: none -**Related**: docs/adr/0001-pluggable-storage-and-network-access.md (Phase 1) - ---- - -## Scope - -### In scope - -- Introduce a new module `crates/vestige-core/src/storage/memory_store.rs` defining: - - `LocalMemoryStore` base trait (Sync + 'static) - - `MemoryStore` Send-bound alias generated via `#[trait_variant::make(MemoryStore: Send)]` - - Supporting data types referenced by the trait: `MemoryRecord`, `SchedulingState`, `SearchQuery`, `SearchResult`, `MemoryEdge`, `Domain`, `ClassificationResult`, `StoreStats`, `HealthStatus`, `MemoryStoreError`. -- Introduce a new module `crates/vestige-core/src/embedder/` defining: - - `Embedder` async trait with `embed`, `model_name`, `dimension` plus `model_hash` (for the registry) and optional `embed_batch` with a default implementation. - - Move/adapt the existing `EmbeddingService` impl into a new struct `FastembedEmbedder` that implements `Embedder`. -- Refactor `Storage` (existing `crates/vestige-core/src/storage/sqlite.rs`) into `SqliteMemoryStore`: - - Keep the struct, the `writer`/`reader` `Mutex` pair, the `FSRSScheduler`, and the USearch `VectorIndex`. - - Rename the type alias `Storage` to `SqliteMemoryStore` with a `pub type Storage = SqliteMemoryStore;` alias for backward source compatibility during the transition. (The trait method surface is the new public contract.) - - Implement `LocalMemoryStore` by wrapping existing synchronous `rusqlite` methods inside `async fn` bodies that call a small `spawn_blocking`-or-inline adapter. Bodies MAY block; the `async fn` signature exists because `LocalMemoryStore` is async. -- Add a `schema_version = 12` migration that introduces two schema additions: - 1. `embedding_model` registry table (one-row constraint enforced in code). - 2. Two new TEXT columns on `knowledge_nodes`: `domains TEXT NOT NULL DEFAULT '[]'` and `domain_scores TEXT NOT NULL DEFAULT '{}'` (both JSON-encoded). -- Enforce model registry on every write path: on the first non-empty embedding write the model signature is recorded; subsequent writes whose `Embedder::model_name()` / `dimension()` / `model_hash()` disagree must fail with `MemoryStoreError::ModelMismatch` before touching the DB. -- Audit all 29 cognitive modules under `crates/vestige-core/src/neuroscience/` and `crates/vestige-core/src/advanced/` to confirm they hold no direct `rusqlite::Connection` references, no `Storage` struct field, and no SQL strings. Any that do get refactored to take `&dyn LocalMemoryStore` (local-only modules) or `&Arc` (modules crossing `await` points). -- Add unit tests alongside each new trait method and integration tests in `tests/phase_1/`. - -### Out of scope - -- Implementing `PgMemoryStore` on sqlx + pgvector -- that is Phase 2. -- `vestige migrate --from sqlite --to postgres` and `vestige migrate --reembed` -- Phase 2. -- MCP over Streamable HTTP, API key middleware, `api_keys` table, `vestige keys create|list|revoke` -- Phase 3. -- `DomainClassifier` module, HDBSCAN clustering, `vestige domains discover|list|rename|merge` CLI, incremental soft-assignment, cross-domain spreading activation decay -- Phase 4. -- Federation, mycelium/mDNS node discovery, review event log table -- Phase 5. -- Removing the `pub type Storage = SqliteMemoryStore;` compatibility alias -- that cleanup happens at the end of Phase 4 when no consumers still spell the old name. - -## Prerequisites - -### Current code state - -- Single concrete type `Storage` in `crates/vestige-core/src/storage/sqlite.rs` (4592 lines, 216 public symbols on the impl blocks, approximately 85 public methods) is the only storage surface the crate exposes. -- `EmbeddingService` in `crates/vestige-core/src/embeddings/local.rs` holds the fastembed singleton. No trait exists; callers type-erase via `&EmbeddingService`. -- Migrations live in `crates/vestige-core/src/storage/migrations.rs`; the current head is v11. -- All cognitive modules in `neuroscience/` and `advanced/` are pure (verified by `grep rusqlite|Connection::|execute\(|prepare\(` returning no matches in those trees). They operate on `KnowledgeNode`, `Vec`, `ConnectionRecord`, etc. passed in by the caller. -- `vestige-mcp` consumes `Arc` in `crates/vestige-mcp/src/server.rs` and every tool under `crates/vestige-mcp/src/tools/`. These call sites will type-check unchanged after the alias is introduced because the trait methods preserve the exact signatures of the existing `pub fn` on `Storage`. -- Test count reported in `CLAUDE.md`: 758 tests (406 mcp + 352 core). This is the no-regression target. - -### Required crates (add via `cargo add` under `crates/vestige-core`) - -| Crate | Version | Why | -|-------|---------|-----| -| `trait-variant` | `0.1` | Generates the `Send`-bound `MemoryStore` alias from `LocalMemoryStore` so `Arc` works under tokio/axum without hand-writing two traits. Listed in PRD section "Crate Dependencies (new)" under Phase 1. | -| `blake3` | `1` | `Embedder::model_hash() -> [u8; 32]` uses blake3 to stabilise the "model signature" stored in the `embedding_model` registry. Already slated for Phase 3 auth; pulling it forward costs nothing and avoids a second migration to add a hash column. | -| `async-trait` | `0.1` | Not strictly required with `trait-variant` on MSRV 1.91 (RPITIT is stable), but used for one utility trait (`EmbedderExt`) that carries a default `embed_batch` body. OPTIONAL; see Open Implementation Questions below. | - -No changes to `vestige-mcp/Cargo.toml` are required for Phase 1 -- the new trait lives in `vestige-core` and the mcp crate continues to depend on the `SqliteMemoryStore` concrete type (via the `Storage` alias) until Phase 2 introduces backend selection. - -## Deliverables - -1. `crates/vestige-core/src/storage/memory_store.rs` -- `LocalMemoryStore` + `MemoryStore` traits and supporting types. -2. `crates/vestige-core/src/storage/mod.rs` -- updated exports and module wiring. -3. `crates/vestige-core/src/storage/sqlite.rs` -- `Storage` renamed to `SqliteMemoryStore`, `impl LocalMemoryStore for SqliteMemoryStore` block, enforcement hooks for the model registry, serde of `domains` / `domain_scores` columns. -4. `crates/vestige-core/src/storage/migrations.rs` -- `MIGRATION_V12_UP` adding `embedding_model` table and `domains`, `domain_scores` columns. -5. `crates/vestige-core/src/embedder/mod.rs` -- `Embedder` trait and re-exports. -6. `crates/vestige-core/src/embedder/fastembed.rs` -- `FastembedEmbedder` implementation. -7. `crates/vestige-core/src/embeddings/local.rs` -- retained; `EmbeddingService` kept as the underlying fastembed holder; `FastembedEmbedder` wraps it. -8. `crates/vestige-core/src/lib.rs` -- new `pub mod embedder;` + re-exports for `MemoryStore`, `LocalMemoryStore`, `Embedder`, `FastembedEmbedder`, and the data types. -9. `tests/phase_1/trait_round_trip.rs` -- integration test: round-trip of every trait method through `SqliteMemoryStore`. -10. `tests/phase_1/embedding_model_registry.rs` -- integration test: first-write registers, mismatch refuses, dimension mismatch refuses. -11. `tests/phase_1/domain_column_migration.rs` -- integration test: a v11 DB upgraded to v12 reads `domains=[]` and `domain_scores={}` for all existing rows. -12. `tests/phase_1/cognitive_module_isolation.rs` -- integration test: every cognitive module compiles and executes against an `Arc` without touching `SqliteMemoryStore` concretely. -13. `tests/phase_1/send_bound_variant.rs` -- integration test: an `Arc` can be moved across `tokio::spawn`. -14. Updated `tests/phase_1/mod.rs` (if the dir already uses a module layout) or individual `[[test]]` entries in `tests/e2e/Cargo.toml` as needed -- see "Test Plan" for the exact layout. - -## Detailed Task Breakdown - -### D1. Trait + supporting types (`memory_store.rs`) - -- **File**: `crates/vestige-core/src/storage/memory_store.rs` (new). -- **Depends on**: `trait-variant` crate added under vestige-core, `chrono`, `serde_json`, `uuid`, `thiserror` (all already in Cargo.toml). -- **Signatures**: - -```rust -//! Backend-agnostic memory store trait. -//! -//! This is the single abstraction every cognitive module sits above. It is -//! intentionally flat: one trait, ~25 methods, no sub-traits. - -use std::collections::HashMap; - -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -// ---------------------------------------------------------------------------- -// ERROR -// ---------------------------------------------------------------------------- - -/// Error returned by every `LocalMemoryStore` / `MemoryStore` method. -#[non_exhaustive] -#[derive(Debug, thiserror::Error)] -pub enum MemoryStoreError { - #[error("not found: {0}")] - NotFound(String), - - #[error("backend error: {0}")] - Backend(String), - - #[error( - "embedding model mismatch: store registered {registered_name} (dim {registered_dim}, \ - hash {registered_hash}), embedder is {actual_name} (dim {actual_dim}, hash {actual_hash})" - )] - ModelMismatch { - registered_name: String, - registered_dim: usize, - registered_hash: String, - actual_name: String, - actual_dim: usize, - actual_hash: String, - }, - - #[error("invalid input: {0}")] - InvalidInput(String), - - #[error("initialization error: {0}")] - Init(String), -} - -impl From for MemoryStoreError { - fn from(e: crate::storage::StorageError) -> Self { - use crate::storage::StorageError as S; - match e { - S::NotFound(s) => MemoryStoreError::NotFound(s), - S::Database(e) => MemoryStoreError::Backend(e.to_string()), - S::Io(e) => MemoryStoreError::Backend(e.to_string()), - S::InvalidTimestamp(s) => MemoryStoreError::Backend(format!("invalid timestamp: {s}")), - S::Init(s) => MemoryStoreError::Init(s), - } - } -} - -pub type MemoryStoreResult = std::result::Result; - -// ---------------------------------------------------------------------------- -// DATA TYPES -// ---------------------------------------------------------------------------- - -/// Backend-agnostic memory record. -/// -/// Phase 1 intentionally keeps this type independent of `KnowledgeNode` to -/// avoid dragging 30+ legacy fields through the trait surface. The SQLite -/// backend converts between `MemoryRecord` and `KnowledgeNode` at the -/// boundary. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MemoryRecord { - pub id: Uuid, - /// Empty = unclassified. Populated in Phase 4. - pub domains: Vec, - /// Raw similarity per domain centroid. Empty until Phase 4 runs clustering. - pub domain_scores: HashMap, - pub content: String, - pub node_type: String, - pub tags: Vec, - pub embedding: Option>, - pub created_at: DateTime, - pub updated_at: DateTime, - pub metadata: serde_json::Value, -} - -/// FSRS-6 scheduling state, one row per memory. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SchedulingState { - pub memory_id: Uuid, - pub stability: f64, - pub difficulty: f64, - pub retrievability: f64, - pub last_review: Option>, - pub next_review: Option>, - pub reps: u32, - pub lapses: u32, -} - -/// Hybrid search request. -#[derive(Debug, Clone, Default)] -pub struct SearchQuery { - pub domains: Option>, - pub text: Option, - pub embedding: Option>, - pub tags: Option>, - pub node_types: Option>, - pub limit: usize, - pub min_retrievability: Option, -} - -#[derive(Debug, Clone)] -pub struct SearchResult { - pub record: MemoryRecord, - pub score: f64, - pub fts_score: Option, - pub vector_score: Option, -} - -/// Edge in the spreading-activation graph. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MemoryEdge { - pub source_id: Uuid, - pub target_id: Uuid, - pub edge_type: String, - pub weight: f64, - pub created_at: DateTime, -} - -/// A topical domain (populated in Phase 4). Phase 1 only needs the type to -/// shape the trait surface; discover/classify are Phase 4 work. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Domain { - pub id: String, - pub label: String, - pub centroid: Vec, - pub top_terms: Vec, - pub memory_count: usize, - pub created_at: DateTime, -} - -/// Result of classifying one vector against all known domains. -#[derive(Debug, Clone)] -pub struct ClassificationResult { - pub scores: HashMap, - pub domains: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct StoreStats { - pub total_memories: usize, - pub memories_with_embeddings: usize, - pub total_edges: usize, - pub total_domains: usize, - pub registered_model_name: Option, - pub registered_model_dim: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum HealthStatus { - Healthy, - Degraded { reason: String }, - Unavailable { reason: String }, -} - -// ---------------------------------------------------------------------------- -// EMBEDDING MODEL SIGNATURE -// ---------------------------------------------------------------------------- - -/// Snapshot of the embedding model that was used to write vectors into the -/// store. Persisted in the `embedding_model` table; compared on every write -/// before the vector is accepted. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ModelSignature { - pub name: String, - pub dimension: usize, - /// Lowercase hex-encoded blake3 hash, 64 chars. - pub hash: String, -} - -// ---------------------------------------------------------------------------- -// TRAIT -// ---------------------------------------------------------------------------- - -/// The single storage abstraction. `trait_variant::make` auto-generates a -/// `MemoryStore` alias with `Send`-bound return futures so `Arc` -/// works in tokio/axum contexts. -#[trait_variant::make(MemoryStore: Send)] -pub trait LocalMemoryStore: Sync + 'static { - // --- Lifecycle --- - async fn init(&self) -> MemoryStoreResult<()>; - async fn health_check(&self) -> MemoryStoreResult; - - // --- Embedding model registry --- - async fn registered_model(&self) -> MemoryStoreResult>; - async fn register_model(&self, sig: &ModelSignature) -> MemoryStoreResult<()>; - - // --- CRUD --- - async fn insert(&self, record: &MemoryRecord) -> MemoryStoreResult; - async fn get(&self, id: Uuid) -> MemoryStoreResult>; - async fn update(&self, record: &MemoryRecord) -> MemoryStoreResult<()>; - async fn delete(&self, id: Uuid) -> MemoryStoreResult<()>; - - // --- Search --- - async fn search(&self, query: &SearchQuery) -> MemoryStoreResult>; - async fn fts_search(&self, text: &str, limit: usize) -> MemoryStoreResult>; - async fn vector_search( - &self, - embedding: &[f32], - limit: usize, - ) -> MemoryStoreResult>; - - // --- FSRS Scheduling --- - async fn get_scheduling( - &self, - memory_id: Uuid, - ) -> MemoryStoreResult>; - async fn update_scheduling(&self, state: &SchedulingState) -> MemoryStoreResult<()>; - async fn get_due_memories( - &self, - before: DateTime, - limit: usize, - ) -> MemoryStoreResult>; - - // --- Graph (spreading activation) --- - async fn add_edge(&self, edge: &MemoryEdge) -> MemoryStoreResult<()>; - async fn get_edges( - &self, - node_id: Uuid, - edge_type: Option<&str>, - ) -> MemoryStoreResult>; - async fn remove_edge(&self, source: Uuid, target: Uuid) -> MemoryStoreResult<()>; - async fn get_neighbors( - &self, - node_id: Uuid, - depth: usize, - ) -> MemoryStoreResult>; - - // --- Domains (Phase 1: stubs return empty; full impl in Phase 4) --- - async fn list_domains(&self) -> MemoryStoreResult>; - async fn get_domain(&self, id: &str) -> MemoryStoreResult>; - async fn upsert_domain(&self, domain: &Domain) -> MemoryStoreResult<()>; - async fn delete_domain(&self, id: &str) -> MemoryStoreResult<()>; - /// Phase 1: returns `Ok(vec![])` since no centroids exist. Phase 4 wires - /// the full soft-assignment pass. - async fn classify(&self, embedding: &[f32]) -> MemoryStoreResult>; - - // --- Bulk / Maintenance --- - async fn count(&self) -> MemoryStoreResult; - async fn get_stats(&self) -> MemoryStoreResult; - async fn vacuum(&self) -> MemoryStoreResult<()>; -} -``` - -- **Behavior notes**: - - Every method returns `MemoryStoreResult`; the trait never exposes `rusqlite::Error`. - - `LocalMemoryStore` requires `Sync + 'static` so `Arc` is usable. The auto-generated `MemoryStore` alias adds `Send` bounds on the returned `impl Future`. - - `register_model` is idempotent: writing the same signature twice is `Ok(())`. Writing a different signature after one is registered returns `MemoryStoreError::ModelMismatch`. - - `classify` on Phase 1 returns `Ok(vec![])` and MUST NOT error; cognitive modules call it and Phase 4 will flesh it out without changing the signature. - - `upsert_domain` / `delete_domain` / `list_domains` / `get_domain` operate against a `domains` table that is empty until Phase 4 populates it. Phase 1 still exposes the methods so Phase 2 can implement them against Postgres in one shot. - - `get_neighbors(node_id, depth)` with `depth == 0` returns just `(node, 1.0)` if the node exists, otherwise `NotFound`. `depth > 0` performs breadth-first expansion over edges, weight = product of edge weights along the shortest path discovered, capped at `max_neighbors = 256` to prevent runaway expansion. - ---- - -### D2. Storage module wiring (`storage/mod.rs`) - -- **File**: `crates/vestige-core/src/storage/mod.rs`. -- **Depends on**: D1. -- **Signatures / diff**: - -```rust -//! Storage Module -//! -//! Backend-agnostic memory store abstraction plus SQLite reference impl. - -mod memory_store; -mod migrations; -mod sqlite; - -pub use memory_store::{ - ClassificationResult, Domain, HealthStatus, LocalMemoryStore, MemoryEdge, MemoryRecord, - MemoryStore, MemoryStoreError, MemoryStoreResult, ModelSignature, SchedulingState, - SearchQuery, SearchResult, StoreStats, -}; -pub use migrations::MIGRATIONS; -pub use sqlite::{ - ConnectionRecord, ConsolidationHistoryRecord, DreamHistoryRecord, InsightRecord, - IntentionRecord, Result, SmartIngestResult, SqliteMemoryStore, StateTransitionRecord, - StorageError, -}; - -/// Backwards-compatibility alias. Retained until Phase 4 completes so every -/// existing `Arc` call site keeps compiling. Scheduled for removal -/// once no downstream source file references it. -pub type Storage = SqliteMemoryStore; -``` - -- **Behavior notes**: - - The alias MUST be a `pub type` (not a re-export), because several tool files pattern on `vestige_core::Storage` through `use` statements and we want to keep them compiling verbatim. This has zero runtime cost. - - `StorageError` stays exported for the 29 existing inherent-method callers; the trait exposes `MemoryStoreError` and provides `From`. - ---- - -### D3. Rename + trait impl in `sqlite.rs` - -- **File**: `crates/vestige-core/src/storage/sqlite.rs`. -- **Depends on**: D1, D2, D4 (for schema columns), D5/D6 (to have `Embedder` to accept on `insert`). -- **Signatures (key excerpts)**: - -```rust -pub struct SqliteMemoryStore { - writer: Mutex, - reader: Mutex, - scheduler: Mutex, - #[cfg(feature = "embeddings")] - embedding_service: EmbeddingService, - #[cfg(feature = "vector-search")] - vector_index: Mutex, - #[cfg(feature = "embeddings")] - query_cache: Mutex>>, - /// Cached model signature. `None` until the first embedding is written. - registered_model: std::sync::RwLock>, -} - -impl SqliteMemoryStore { - pub fn new(db_path: Option) -> MemoryStoreResult { /* existing body, Result converted */ } - - /// Internal: convert a row into a `MemoryRecord` (new mapping reading - /// `domains` / `domain_scores` JSON columns). - fn row_to_record(row: &rusqlite::Row) -> rusqlite::Result { /* ... */ } - - /// Internal: given a `MemoryRecord` plus an optional embedding, enforce - /// the registered model signature and return a `MemoryStoreError` if - /// the embedder would produce a mismatched vector. - fn enforce_model( - &self, - incoming: Option<&ModelSignature>, - ) -> MemoryStoreResult<()> { /* ... */ } -} - -impl crate::storage::memory_store::LocalMemoryStore for SqliteMemoryStore { - async fn init(&self) -> MemoryStoreResult<()> { /* no-op; migrations run in `new` */ Ok(()) } - - async fn health_check(&self) -> MemoryStoreResult { - // SELECT 1; check vector index loaded; check embedding_model presence. - } - - async fn registered_model(&self) -> MemoryStoreResult> { - let cached = self.registered_model.read().map_err(|_| MemoryStoreError::Init("registered_model rwlock poisoned".into()))?.clone(); - if cached.is_some() { - return Ok(cached); - } - // Fall through to DB read... - } - - async fn register_model(&self, sig: &ModelSignature) -> MemoryStoreResult<()> { - // INSERT OR IGNORE; if a row exists and differs, return ModelMismatch. - } - - async fn insert(&self, record: &MemoryRecord) -> MemoryStoreResult { - if let Some(vec) = &record.embedding { - // Caller is REQUIRED to have called register_model first (or the - // store auto-registers on the first embedded write -- see - // "embedding_model_registry.rs" test). - let derived = ModelSignature { /* from cache or from record.metadata */ }; - self.enforce_model(Some(&derived))?; - if vec.len() != derived.dimension { - return Err(MemoryStoreError::InvalidInput( - format!("embedding length {} != registered dimension {}", vec.len(), derived.dimension), - )); - } - } - // Delegate to a private `insert_record_blocking` helper that is the - // current `ingest`/`update_node_content` body, rewritten to accept a - // `MemoryRecord` and to also write `domains` / `domain_scores` JSON. - } - - // ... remaining ~24 methods follow the same pattern: convert inputs, - // call the existing synchronous body, convert outputs. -} -``` - -- **SQL** (covered in full in D4 below). -- **Behavior notes**: - - The `async fn` bodies are allowed to be synchronous under the hood (rusqlite is blocking). We do NOT wrap in `spawn_blocking` for Phase 1 -- the current `Storage` is already used from synchronous code paths (CLI, MCP stdio handler) and forcing the tokio runtime is a Phase 2 concern when we also add sqlx. The trait simply lifts the synchronous body into an `async fn` so the signatures match the trait. MSRV 1.91 supports async fn in trait via `trait_variant::make`. - - `insert` preserves the current FSRS initialization logic (stability, difficulty, next_review, etc.) -- the new code path converts `MemoryRecord.metadata` back into `IngestInput`-equivalent fields when needed. All existing inherent methods (`ingest`, `smart_ingest`, `mark_reviewed`, ...) remain on `SqliteMemoryStore` untouched; the trait impl calls into them. - - `registered_model` cache is an `RwLock>`. Invalidated on schema reset. Never mutated after first population until an explicit `--reembed` migration (Phase 2) takes the RwLock exclusively and writes a new row. - - `enforce_model` returns `Ok(())` if no model is registered yet AND `incoming.is_none()` (no-embedding write). Returns `Ok(())` if no model is registered and `incoming.is_some()` after calling `register_model`. Returns `Err(ModelMismatch)` if registered and they disagree. - - `domains` / `domain_scores` serialization uses `serde_json::to_string` on write and `serde_json::from_str` on read. Empty vec -> `"[]"`, empty map -> `"{}"`. `NULL` in the DB is treated as the empty value for pre-migration rows. - - Every existing inherent method is kept verbatim. The trait impl dispatches to them. This is the "no behavior change" guarantee. - ---- - -### D4. Schema migration V12 - -- **File**: `crates/vestige-core/src/storage/migrations.rs`. -- **Depends on**: D2. -- **SQL**: - -```sql --- Migration V12: embedding model registry + per-memory domain columns. - --- 1. Embedding model registry. Single logical row; the (id = 1) constraint is --- enforced in code via `register_model` (SQLite CHECK on a single-row --- table is uglier than a constraint we already enforce in Rust). -CREATE TABLE IF NOT EXISTS embedding_model ( - id INTEGER PRIMARY KEY CHECK (id = 1), - name TEXT NOT NULL, - dimension INTEGER NOT NULL, - hash TEXT NOT NULL, -- lowercase hex blake3 - created_at TEXT NOT NULL -); - --- 2. Per-memory domain columns (JSON TEXT; SQLite has no native arrays). -ALTER TABLE knowledge_nodes ADD COLUMN domains TEXT NOT NULL DEFAULT '[]'; -ALTER TABLE knowledge_nodes ADD COLUMN domain_scores TEXT NOT NULL DEFAULT '{}'; - --- 3. Index on the domains JSON column to enable `LIKE '%"dev"%'`-style --- filter in Phase 4. Kept lightweight here; Postgres will use GIN. -CREATE INDEX IF NOT EXISTS idx_nodes_domains ON knowledge_nodes(domains); -CREATE INDEX IF NOT EXISTS idx_nodes_domain_scores ON knowledge_nodes(domain_scores); - --- 4. Domains catalogue (empty until Phase 4 populates). -CREATE TABLE IF NOT EXISTS domains ( - id TEXT PRIMARY KEY, - label TEXT NOT NULL, - centroid BLOB, -- f32 vector, raw bytes - top_terms TEXT NOT NULL DEFAULT '[]', - memory_count INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL -); - -CREATE INDEX IF NOT EXISTS idx_domains_created_at ON domains(created_at); - -UPDATE schema_version SET version = 12, applied_at = datetime('now'); -``` - -- **Rust changes** to `migrations.rs`: - -```rust -pub const MIGRATIONS: &[Migration] = &[ - // ... V1..V11 unchanged ... - Migration { - version: 12, - description: "Phase 1: embedding_model registry, domains/domain_scores columns, domains table", - up: MIGRATION_V12_UP, - }, -]; - -const MIGRATION_V12_UP: &str = r#"...SQL above..."#; -``` - -- **Behavior notes**: - - Idempotent: `ALTER TABLE ... ADD COLUMN` on SQLite is not idempotent by default, but the `apply_migrations` driver only applies migrations whose version > current. A user who has already applied V12 never sees the SQL again. - - The `CHECK (id = 1)` on `embedding_model` is the only one-row guardrail -- all inserts go through `register_model` which uses `INSERT OR IGNORE INTO embedding_model (id, ...) VALUES (1, ...)` followed by a `SELECT` to detect mismatch. - - `centroid BLOB` stores the f32 vector using the same `Embedding::to_bytes()` format used in `node_embeddings`, for consistency. - ---- - -### D5. Embedder trait (`embedder/mod.rs`) - -- **File**: `crates/vestige-core/src/embedder/mod.rs` (new). -- **Depends on**: `blake3` crate added to vestige-core. -- **Signatures**: - -```rust -//! Text-to-vector encoding trait. Pluggable per-install. - -use std::fmt::Debug; - -mod fastembed; - -pub use fastembed::FastembedEmbedder; - -/// Error returned by every `Embedder` method. -#[non_exhaustive] -#[derive(Debug, thiserror::Error)] -pub enum EmbedderError { - #[error("embedder initialization failed: {0}")] - Init(String), - #[error("embedding generation failed: {0}")] - EmbedFailed(String), - #[error("invalid input: {0}")] - InvalidInput(String), -} - -pub type EmbedderResult = std::result::Result; - -/// Pluggable embedder. The storage layer NEVER calls fastembed directly; -/// callers compute vectors via this trait and pass them into `MemoryStore`. -#[trait_variant::make(Embedder: Send)] -pub trait LocalEmbedder: Sync + 'static { - async fn embed(&self, text: &str) -> EmbedderResult>; - - fn model_name(&self) -> &str; - - fn dimension(&self) -> usize; - - /// Stable blake3 hash of (model_name || dimension || optional weights - /// digest if available). Lowercase hex, 64 chars. - /// - /// Used by `MemoryStore::register_model` to detect silent model drift - /// (e.g. a fastembed minor upgrade that changes vector output). - fn model_hash(&self) -> String; - - async fn embed_batch(&self, texts: &[&str]) -> EmbedderResult>> { - // Default: sequential. Backends with native batching override this. - let mut out = Vec::with_capacity(texts.len()); - for t in texts { - out.push(self.embed(t).await?); - } - Ok(out) - } - - /// Returns the `ModelSignature` describing this embedder. Convenience - /// wrapper over the three accessors above. - fn signature(&self) -> crate::storage::ModelSignature { - crate::storage::ModelSignature { - name: self.model_name().to_string(), - dimension: self.dimension(), - hash: self.model_hash(), - } - } -} -``` - -- **Behavior notes**: - - The `embed_batch` default implementation is non-trivial only in that backends with genuine batching override it. The `FastembedEmbedder` overrides to call `EmbeddingService::embed_batch`. - - `model_hash()` is intentionally a function, not a constant, so backends with configurable weights (a future `OnnxEmbedder` that loads an arbitrary file) can hash the file bytes into the signature. - - `Embedder` (the `Send` variant) is what cognitive modules bind against when they hold `Arc`. `LocalEmbedder` is available for single-threaded callers (CLI, tests). - ---- - -### D6. FastembedEmbedder impl (`embedder/fastembed.rs`) - -- **File**: `crates/vestige-core/src/embedder/fastembed.rs` (new). -- **Depends on**: D5, existing `crate::embeddings::local::EmbeddingService`. -- **Signatures**: - -```rust -use super::{EmbedderError, EmbedderResult, LocalEmbedder}; -use crate::embeddings::{EMBEDDING_DIMENSIONS, EmbeddingService, matryoshka_truncate}; - -pub struct FastembedEmbedder { - inner: EmbeddingService, - cached_hash: std::sync::OnceLock, -} - -impl FastembedEmbedder { - pub fn new() -> Self { - Self { - inner: EmbeddingService::new(), - cached_hash: std::sync::OnceLock::new(), - } - } - - fn compute_hash(name: &str, dim: usize) -> String { - let mut hasher = blake3::Hasher::new(); - hasher.update(name.as_bytes()); - hasher.update(&(dim as u64).to_le_bytes()); - // fastembed's ONNX bytes are not directly accessible at runtime; we - // use `(name, dim, static fastembed crate version)` as the - // signature. If fastembed ever changes its output deterministically - // between minor versions, bumping the crate version triggers a - // mismatch -- which is exactly the drift we want to detect. - hasher.update(env!("CARGO_PKG_VERSION").as_bytes()); - hasher.finalize().to_hex().to_string() - } -} - -impl Default for FastembedEmbedder { - fn default() -> Self { Self::new() } -} - -impl LocalEmbedder for FastembedEmbedder { - async fn embed(&self, text: &str) -> EmbedderResult> { - let emb = self - .inner - .embed(text) - .map_err(|e| EmbedderError::EmbedFailed(e.to_string()))?; - Ok(emb.vector) - } - - fn model_name(&self) -> &str { self.inner.model_name() } - - fn dimension(&self) -> usize { EMBEDDING_DIMENSIONS } - - fn model_hash(&self) -> String { - self.cached_hash - .get_or_init(|| Self::compute_hash(self.inner.model_name(), EMBEDDING_DIMENSIONS)) - .clone() - } - - async fn embed_batch(&self, texts: &[&str]) -> EmbedderResult>> { - let embs = self - .inner - .embed_batch(texts) - .map_err(|e| EmbedderError::EmbedFailed(e.to_string()))?; - Ok(embs.into_iter().map(|e| e.vector).collect()) - } -} -``` - -- **Behavior notes**: - - `EmbeddingService` is kept as the fastembed singleton holder; `FastembedEmbedder` is a thin trait adapter. Existing callers of `EmbeddingService` continue to work during the transition. - - `model_hash` is deterministic for a given `(model_name, EMBEDDING_DIMENSIONS, vestige-core version)` triple. This is the drift detector the ADR calls out under "Risks: Embedding model drift". - - `matryoshka_truncate` is already applied inside `EmbeddingService::embed`, so the vectors returned here are the 256-dim Matryoshka-truncated L2-normalized vectors that the rest of the stack expects. - ---- - -### D7. `lib.rs` re-exports - -- **File**: `crates/vestige-core/src/lib.rs`. -- **Depends on**: D1, D2, D5, D6. -- **Diff** (inserted alongside the existing `pub mod storage;` re-exports): - -```rust -pub mod embedder; - -pub use embedder::{Embedder, EmbedderError, EmbedderResult, FastembedEmbedder, LocalEmbedder}; - -pub use storage::{ - ClassificationResult, Domain, HealthStatus, LocalMemoryStore, MemoryEdge, MemoryRecord, - MemoryStore, MemoryStoreError, MemoryStoreResult, ModelSignature, SchedulingState, - SearchQuery, SearchResult, SqliteMemoryStore, Storage, StoreStats, - // Existing re-exports retained: - ConnectionRecord, ConsolidationHistoryRecord, DreamHistoryRecord, InsightRecord, - IntentionRecord, Result, SmartIngestResult, StateTransitionRecord, StorageError, -}; -``` - -- **Behavior notes**: - - `Storage` remains a top-level re-export so `use vestige_core::Storage;` keeps working in `vestige-mcp` without changes. Post-Phase-4 cleanup will grep the downstream crates and replace. - ---- - -### D8. Cognitive module audit - -- **Files**: all under `crates/vestige-core/src/neuroscience/*.rs` and `crates/vestige-core/src/advanced/*.rs` -- 21 source files. -- **Depends on**: D1..D7. -- **Work**: perform the following grep-gate BEFORE and AFTER the refactor: - -``` -Grep pattern: "rusqlite|Connection::|execute\\(|prepare\\(|&Storage|SqliteMemoryStore" -Expected in neuroscience/ and advanced/ BEFORE: only a single comment-only hit in `neuroscience/active_forgetting.rs:54` referencing `Storage::suppress_memory` in a doc comment. -Expected AFTER: zero hits that reference `SqliteMemoryStore` concretely. References through `&dyn LocalMemoryStore` or `&Arc` are acceptable. -``` - -- **Behavior notes**: - - Current state: the 29 cognitive modules are already pure (they take nodes/vectors/connections as arguments, not a `&Storage`). No refactor is required for their bodies. - - The only work is the `consolidation/sleep.rs` and `consolidation/phases.rs` path, which in the current codebase accepts `&Storage`. These get rewritten to accept `&dyn LocalMemoryStore` (callable from sync contexts) or `&Arc` (callable from async contexts). See file inventory below. - - Actual rewrites (expected number): 3-5 functions across `consolidation/sleep.rs` and `consolidation/mod.rs`. All trait-object refactors; no logic changes. - - `cognitive.rs` in `vestige-mcp` uses `storage.get_all_connections()`. Because `SqliteMemoryStore` keeps `get_all_connections` as an inherent method AND implements `MemoryStore::get_edges`, both call styles keep compiling. `cognitive.rs` does not need to change in Phase 1. - ---- - -### D9. Backwards-compatible inherent methods on `SqliteMemoryStore` - -- **File**: `crates/vestige-core/src/storage/sqlite.rs`. -- **Depends on**: D3. -- **Behavior notes**: - - Every one of the 85 existing `pub fn` on `Storage` (e.g. `ingest`, `smart_ingest`, `mark_reviewed`, `hybrid_search_filtered`, `save_intention`, `save_insight`, `save_connection`, `apply_rac1_cascade`, ...) stays as an inherent method on `SqliteMemoryStore`. The Phase 1 refactor ONLY adds the trait impl; it does NOT remove any method, rename any field, or change any SQL. - - Internal writes that previously embedded `INSERT INTO knowledge_nodes (...)` statements gain two more columns (`domains = '[]'`, `domain_scores = '{}'`) in the INSERT list. These are non-optional columns after migration V12, and their DEFAULT is `'[]'`/`'{}'` respectively, so ALTER behaves correctly for pre-existing rows but INSERT statements need to either list the defaults explicitly or rely on the DB default. Plan: explicitly write `'[]'` and `'{}'` in every `INSERT INTO knowledge_nodes` statement to avoid surprises if a future migration drops the DEFAULT. - ---- - -## Test Plan - -### Unit tests (colocated, `#[cfg(test)] mod tests` at end of each source file) - -Every public trait method on `LocalMemoryStore` gets at least one unit test, exercised through the `SqliteMemoryStore` impl. The unit test file is `crates/vestige-core/src/storage/sqlite.rs` (inside the existing `mod tests`). - -- `vestige_core::storage::sqlite::tests::trait_init_is_idempotent` -- calling `LocalMemoryStore::init` twice returns `Ok(())` both times. -- `vestige_core::storage::sqlite::tests::trait_health_check_reports_healthy_on_fresh_db` -- asserts `HealthStatus::Healthy` on a fresh in-memory DB. -- `vestige_core::storage::sqlite::tests::trait_register_model_first_write_succeeds` -- after registering a signature, `registered_model()` returns it. -- `vestige_core::storage::sqlite::tests::trait_register_model_mismatched_write_refused` -- registering a second, different signature returns `MemoryStoreError::ModelMismatch`. -- `vestige_core::storage::sqlite::tests::trait_register_model_same_signature_idempotent` -- registering the same signature twice returns `Ok(())` both times. -- `vestige_core::storage::sqlite::tests::trait_insert_returns_uuid` -- `insert(record)` returns the UUID from the record. -- `vestige_core::storage::sqlite::tests::trait_insert_refuses_dimension_mismatch` -- inserting a record with a 512-dim vector into a store registered for 256 dims returns `MemoryStoreError::InvalidInput`. -- `vestige_core::storage::sqlite::tests::trait_get_missing_returns_none` -- `get(non_existent_uuid)` returns `Ok(None)`. -- `vestige_core::storage::sqlite::tests::trait_get_after_insert_round_trip` -- insert then get returns a record equal (by content/tags/type) to the input; `domains == []`, `domain_scores == {}`. -- `vestige_core::storage::sqlite::tests::trait_update_modifies_content` -- update with new content reflects in subsequent `get`. -- `vestige_core::storage::sqlite::tests::trait_delete_removes_record` -- `delete` then `get` returns `Ok(None)`. -- `vestige_core::storage::sqlite::tests::trait_search_combines_fts_and_vector` -- with one memory whose content matches by FTS and another by vector, `search` returns both, higher score for the exact content match. -- `vestige_core::storage::sqlite::tests::trait_fts_search_returns_tokens_match` -- verifies FTS path. -- `vestige_core::storage::sqlite::tests::trait_vector_search_returns_cosine_order` -- verifies ordering. -- `vestige_core::storage::sqlite::tests::trait_scheduling_round_trip` -- `update_scheduling` then `get_scheduling` returns equivalent state. -- `vestige_core::storage::sqlite::tests::trait_get_scheduling_missing_returns_none`. -- `vestige_core::storage::sqlite::tests::trait_get_due_memories_returns_in_order` -- inserts 3 records with different `next_review`, asserts older-due listed first. -- `vestige_core::storage::sqlite::tests::trait_add_edge_is_idempotent` -- adding the same edge twice does not duplicate. -- `vestige_core::storage::sqlite::tests::trait_get_edges_filters_by_type`. -- `vestige_core::storage::sqlite::tests::trait_remove_edge_deletes_single`. -- `vestige_core::storage::sqlite::tests::trait_get_neighbors_bfs_depth_zero_returns_self_only`. -- `vestige_core::storage::sqlite::tests::trait_get_neighbors_bfs_depth_two_expands` -- build A->B->C, get_neighbors(A, 2) returns {A, B, C}. -- `vestige_core::storage::sqlite::tests::trait_list_domains_empty_in_phase_1` -- Phase 1 has no clustering, so `list_domains()` returns `[]`. -- `vestige_core::storage::sqlite::tests::trait_upsert_then_get_domain_round_trip`. -- `vestige_core::storage::sqlite::tests::trait_delete_domain_idempotent`. -- `vestige_core::storage::sqlite::tests::trait_classify_with_no_domains_returns_empty` -- verifies Phase 1 stub behavior. -- `vestige_core::storage::sqlite::tests::trait_count_matches_insert_count`. -- `vestige_core::storage::sqlite::tests::trait_get_stats_reports_registered_model`. -- `vestige_core::storage::sqlite::tests::trait_vacuum_succeeds` -- runs and asserts no error. - -Every public method on `LocalEmbedder` gets at least one unit test under `crates/vestige-core/src/embedder/fastembed.rs`: - -- `vestige_core::embedder::fastembed::tests::embedder_reports_correct_name` -- `model_name()` contains "nomic". -- `vestige_core::embedder::fastembed::tests::embedder_reports_256_dimension`. -- `vestige_core::embedder::fastembed::tests::embedder_hash_is_stable` -- `model_hash()` called twice returns identical string. -- `vestige_core::embedder::fastembed::tests::embedder_hash_includes_crate_version` -- a synthetic test that asserts the hash contains the blake3 of `(name, 256, VERSION)`. -- `vestige_core::embedder::fastembed::tests::embedder_embed_smoke` -- gated on `#[cfg(feature = "embeddings")]`; asserts output length == 256. -- `vestige_core::embedder::fastembed::tests::embedder_embed_batch_matches_sequential` -- gated; assert batch result equals sequential result. -- `vestige_core::embedder::fastembed::tests::embedder_signature_matches_accessors`. - -Migration V12 unit tests under `crates/vestige-core/src/storage/migrations.rs`: - -- `vestige_core::storage::migrations::tests::v12_adds_embedding_model_table` -- apply V12 then assert `SELECT count(*) FROM sqlite_master WHERE name='embedding_model'` == 1. -- `vestige_core::storage::migrations::tests::v12_adds_domains_columns` -- assert `PRAGMA table_info(knowledge_nodes)` includes `domains` and `domain_scores`. -- `vestige_core::storage::migrations::tests::v12_default_values_empty_json` -- insert a row via raw SQL, read back, assert `domains == '[]'` and `domain_scores == '{}'`. -- `vestige_core::storage::migrations::tests::v12_is_replayable` -- rewind `schema_version` to 11, re-apply migrations, does not error (MUST use `CREATE TABLE IF NOT EXISTS`; `ALTER TABLE ADD COLUMN` will be skipped because the driver only re-runs migrations whose version > current -- already covered by `apply_migrations`). -- `vestige_core::storage::migrations::tests::v12_preserves_existing_rows` -- insert rows under V11 schema, upgrade to V12, assert `domains='[]'` on those rows. - -Supporting-type unit tests under `crates/vestige-core/src/storage/memory_store.rs`: - -- `vestige_core::storage::memory_store::tests::memory_store_error_from_storage_error` -- converts `StorageError::NotFound` to `MemoryStoreError::NotFound`. -- `vestige_core::storage::memory_store::tests::model_signature_serde_round_trip`. -- `vestige_core::storage::memory_store::tests::memory_record_serde_round_trip`. - -### Integration tests (`tests/phase_1/`) - -Each file is a standalone `[[test]]` target. The Cargo layout: - -- `tests/phase_1/Cargo.toml` with: - -```toml -[package] -name = "vestige-phase-1-tests" -version = "0.0.1" -edition = "2024" -publish = false - -[dependencies] -vestige-core = { path = "../../crates/vestige-core" } -tokio = { version = "1", features = ["macros", "rt-multi-thread"] } -tempfile = "3" -uuid = { version = "1", features = ["v4"] } -chrono = "0.4" -serde_json = "1" -rusqlite = { version = "0.38", features = ["bundled"] } -``` - -And added to the workspace `Cargo.toml` members. Each `.rs` file below is a `#[tokio::test]`-using integration test. - -#### `tests/phase_1/trait_round_trip.rs` - -- `round_trip::insert_get_update_delete` -- exercises CRUD via the trait. Inserts a record with `domains=[]`, gets it, asserts equality, updates content, deletes, asserts not found. -- `round_trip::scheduling_upsert_and_due_scan` -- upserts FSRS state for three memories with different `next_review`, calls `get_due_memories(Utc::now(), 10)`, asserts only past-due ones appear. -- `round_trip::edge_crud` -- add edge, list edges, remove edge, assert gone. -- `round_trip::search_hybrid_returns_results` -- insert three memories, embed one by content match only, one by semantic only, one by both, search with both `text` and `embedding`, assert all three appear with `fts_score`/`vector_score` correctly populated. -- `round_trip::count_and_stats_track_inserts` -- after 10 inserts, `count()` == 10 and `get_stats().total_memories` == 10. -- `round_trip::vacuum_after_deletes_reclaims` -- insert 50, delete 40, call `vacuum`, assert disk file size decreased (informational; test is lenient if VACUUM was a no-op). -- `round_trip::list_domains_empty_then_upsert_then_delete` -- Phase 1 has no discovery, but manual upsert/delete must work so Phase 2's Postgres impl can share the test. -- `round_trip::classify_with_no_domains_returns_empty` -- calls `classify(embedding)` on a fresh store, asserts `Vec<(String, f64)>` is empty. - -#### `tests/phase_1/embedding_model_registry.rs` - -- `model_registry::first_embedded_insert_auto_registers` -- fresh store; insert a record with a 256-dim vector using a `FastembedEmbedder`; subsequent `registered_model()` returns a `Some(ModelSignature)` with dim=256. -- `model_registry::second_insert_with_same_signature_succeeds`. -- `model_registry::second_insert_with_different_dimension_refused` -- register a 256-dim signature, try to insert a 512-dim vector, expect `MemoryStoreError::InvalidInput` (because dimension does not match registered). -- `model_registry::second_insert_with_different_model_name_refused` -- register signature A, call `register_model` with signature B (same dim, different name), expect `MemoryStoreError::ModelMismatch`. -- `model_registry::second_insert_with_different_hash_refused` -- register signature A, try to register signature A' with the same name and dim but a different hash, expect `MemoryStoreError::ModelMismatch`. -- `model_registry::no_embedding_insert_allowed_before_registration` -- a plain text memory without an embedding must insert successfully even when `registered_model()` is `None`. -- `model_registry::stats_reports_registered_model_after_first_write`. - -#### `tests/phase_1/domain_column_migration.rs` - -- `domain_columns::fresh_db_has_v12_schema` -- open a fresh store, query `PRAGMA table_info(knowledge_nodes)`, assert `domains` and `domain_scores` columns are present with the correct defaults. -- `domain_columns::v11_db_upgrades_cleanly` -- programmatically create a DB at V11 by running migrations up to V11 only, insert 5 rows, then invoke the V12 migration, assert all 5 rows now report `domains=='[]'` and `domain_scores=='{}'`. -- `domain_columns::empty_domains_serialize_as_brackets` -- insert a `MemoryRecord { domains: vec![], .. }`, then read the underlying SQLite row via a raw query, assert the stored value is `"[]"`, not `NULL`. -- `domain_columns::populated_domains_round_trip` -- insert a record with `domains=["dev","infra"]` and `domain_scores={"dev":0.82,"infra":0.71}`, read back via the trait, assert equality. -- `domain_columns::domains_table_exists` -- `SELECT name FROM sqlite_master WHERE name='domains'` returns one row. - -#### `tests/phase_1/cognitive_module_isolation.rs` - -- `cognitive_isolation::all_modules_compile_against_dyn_store` -- a test function that allocates a `let store: Arc = Arc::new(SqliteMemoryStore::new(...)?);`, then invokes a representative method from every cognitive module passing in records/vectors/edges it reads through the trait. The point is a compile-time gate: if any module still typed against `SqliteMemoryStore`, this would fail to compile. -- `cognitive_isolation::spreading_activation_traverses_via_trait` -- exercise `ActivationNetwork` seeded from `store.get_edges(...)` results. -- `cognitive_isolation::synaptic_tagging_consumes_records_via_trait` -- build `CapturedMemory` from `store.get(uuid)` and let the tagger compute retroactive importance. -- `cognitive_isolation::hippocampal_index_built_from_store` -- load memories via `store.fts_search`, build `HippocampalIndex`, assert queries against the index work. - -#### `tests/phase_1/send_bound_variant.rs` - -- `send_bound::arc_dyn_memory_store_moves_across_tokio_tasks` -- wrap `SqliteMemoryStore` in `Arc`, spawn 16 tokio tasks each inserting 10 memories, join all tasks, assert final `count() == 160`. This verifies the `#[trait_variant::make(MemoryStore: Send)]` emission actually produces a `Send`-bound future. -- `send_bound::concurrent_readers_one_writer` -- 32 concurrent readers calling `search` while one writer loops inserting; asserts no panics, no deadlocks, eventual consistency on `count`. - -#### `tests/phase_1/embedder_trait.rs` - -- `embedder::fastembed_implements_embedder_trait` -- `let e: Box = Box::new(FastembedEmbedder::new());` compiles and `e.dimension()` == 256. -- `embedder::signature_matches_memory_store_registry` -- take the signature from `Embedder::signature()`, register it via `MemoryStore::register_model`, assert `registered_model()` returns the same. - -### Regression verification - -- `cargo build -p vestige-core` -- zero warnings. -- `cargo build -p vestige-mcp` -- zero warnings. -- `cargo clippy --workspace --all-targets -- -D warnings` -- green. -- `cargo test -p vestige-core --lib` -- existing 352 core lib tests remain green. -- `cargo test -p vestige-mcp --lib` -- existing 406 mcp tests remain green. -- `cargo test -p vestige-core --lib storage::migrations::tests` -- explicitly invokes the migration tests added in Phase 1. -- `cargo test -p vestige-core --lib storage::sqlite::tests` -- invokes the trait-method unit tests added in Phase 1. -- `cargo test -p vestige-core --lib embedder::fastembed::tests` -- invokes embedder unit tests. -- `cargo test -p vestige-phase-1-tests --test trait_round_trip` -- Phase 1 integration test file 1. -- `cargo test -p vestige-phase-1-tests --test embedding_model_registry` -- Phase 1 integration test file 2. -- `cargo test -p vestige-phase-1-tests --test domain_column_migration` -- Phase 1 integration test file 3. -- `cargo test -p vestige-phase-1-tests --test cognitive_module_isolation` -- Phase 1 integration test file 4. -- `cargo test -p vestige-phase-1-tests --test send_bound_variant` -- Phase 1 integration test file 5. -- `cargo test -p vestige-phase-1-tests --test embedder_trait` -- Phase 1 integration test file 6. -- `cargo test -p vestige-phase-1-tests` -- convenience: runs all integration test binaries in the Phase 1 crate. -- `cargo test -p vestige-e2e` -- existing e2e harness runs unchanged; no new tests here but existing ones must pass. - -## Acceptance Criteria - -- [ ] `cargo build -p vestige-core` -- zero warnings. -- [ ] `cargo build -p vestige-mcp` -- zero warnings. -- [ ] `cargo build --workspace --all-targets` -- zero warnings. -- [ ] `cargo clippy --workspace --all-targets -- -D warnings` -- exits 0. -- [ ] `cargo test -p vestige-core` -- all 352 existing core tests plus new Phase 1 unit tests pass. -- [ ] `cargo test -p vestige-mcp` -- all 406 existing mcp tests pass, unchanged. -- [ ] `cargo test -p vestige-phase-1-tests` -- all Phase 1 integration tests pass. -- [ ] `cargo test -p vestige-e2e` -- existing e2e journey suite passes unchanged. -- [ ] Cumulative test count >= 758 (the pre-Phase-1 baseline) plus the new unit and integration additions. -- [ ] `git grep -n 'rusqlite::' crates/vestige-core/src/neuroscience/ crates/vestige-core/src/advanced/` -- zero hits (the single pre-existing doc-comment reference in `active_forgetting.rs` is acceptable and does not introduce SQL dependency; code references must be zero). -- [ ] `git grep -n 'SqliteMemoryStore' crates/vestige-core/src/neuroscience/ crates/vestige-core/src/advanced/` -- zero hits. -- [ ] `git grep -n 'fastembed::' crates/vestige-core/src/storage/sqlite.rs` -- zero hits (Storage must never call fastembed directly; embedding goes through the `Embedder` trait held on the caller side). -- [ ] `SqliteMemoryStore::insert` refuses a vector whose dimension disagrees with the registered model (returns `MemoryStoreError::InvalidInput`). -- [ ] `SqliteMemoryStore::register_model` returns `MemoryStoreError::ModelMismatch` when a second, different signature is provided after a first was already registered. -- [ ] After upgrading a V11 database to V12, every pre-existing row has `domains == "[]"` and `domain_scores == "{}"` with no NULLs. -- [ ] `#[trait_variant::make(MemoryStore: Send)]` compiles; `Arc` is movable across `tokio::spawn`. -- [ ] Migration V12 is idempotent on replay: `apply_migrations` rewound to V11, re-applied, succeeds without error. -- [ ] `vestige-core::storage::Storage` continues to resolve (via the `pub type` alias) at every current call site in `vestige-mcp`. -- [ ] The `embedding_model` table can only hold a single row (programmatic invariant -- verified by an integration test that attempts a second `INSERT INTO embedding_model (id = 1, ...)` and observes the CHECK-enforced uniqueness). -- [ ] `registered_model()` is cached on first read; no SELECT is issued against `embedding_model` after the first hit within the same process (verified by wrapping the reader in a counting proxy in a dedicated test). - -## Rollback Notes - -If Phase 1 fails mid-way, rollback granularity is per-deliverable and the DB can be downgraded by SQL. - -- **D1 (`memory_store.rs`)**: revert the new file. The trait has zero non-test consumers in Phase 1, so deletion is safe. -- **D2 (`storage/mod.rs`)**: revert to the prior export list. The only forward-facing identifier is the `pub type Storage = SqliteMemoryStore;` alias, which becomes `pub use sqlite::Storage;` again once `SqliteMemoryStore` is renamed back to `Storage`. -- **D3 (`sqlite.rs` rename + trait impl)**: revert the struct rename (`SqliteMemoryStore` -> `Storage`). The trait impl is a separate `impl` block and can be deleted wholesale. Inherent methods are unchanged and do not need to be touched. Net diff on revert: delete one `impl LocalMemoryStore for ...` block plus the two helper functions (`row_to_record`, `enforce_model`). -- **D4 (Migration V12)**: DOWN migration script: - -```sql --- Phase 1 rollback: drop Phase 1 schema additions. --- WARNING: this deletes any `domains` / `domain_scores` values stored under V12. --- Execute ONLY when downgrading from V12 to V11 on a database where no Phase 4 --- work has happened yet (otherwise you lose domain classifications). - -DROP TABLE IF EXISTS domains; -DROP INDEX IF EXISTS idx_nodes_domains; -DROP INDEX IF EXISTS idx_nodes_domain_scores; - --- SQLite does not support DROP COLUMN before 3.35; the project's bundled --- rusqlite uses 3.45+ (see `bundled-sqlite` feature). So the DROP COLUMN --- form below is safe on every target platform. -ALTER TABLE knowledge_nodes DROP COLUMN domains; -ALTER TABLE knowledge_nodes DROP COLUMN domain_scores; - -DROP TABLE IF EXISTS embedding_model; - -UPDATE schema_version SET version = 11, applied_at = datetime('now'); -``` - - Operationally: the DOWN script is NOT included in the source migrations list (migrations are forward-only). If a rollback is required, it is applied manually via `sqlite3 vestige.db < rollback_v12.sql`. A backup via `storage.backup_to(...)` MUST be taken before the Phase 1 migration runs in production -- the `Storage::backup_to` method already exists (line 3903) and does not need changes. - -- **D5/D6 (`embedder/`)**: delete the module. `EmbeddingService` is untouched, so callers that still use it continue to work. The new `Embedder` trait has no pre-Phase-2 consumers. -- **D7 (`lib.rs`)**: revert the re-export additions. Zero downstream impact since the new symbols have no pre-Phase-2 consumers. -- **D8 (cognitive module audit)**: audit-only, no code changes. Nothing to roll back unless `consolidation/sleep.rs` was changed; if so, revert. -- **Crate-level considerations**: - - `trait-variant` must remain in `Cargo.toml` until every consumer of the trait alias has been reverted. Safe to leave in `[dependencies]` indefinitely; it has no runtime cost. - - `blake3` was going to be added in Phase 3 anyway; leaving it in on rollback is harmless. - - `rusqlite` version stays pinned; no bump required for Phase 1. - -## Open Implementation Questions - -Implementation-choice-only. Architectural questions are resolved in ADR 0001. - -1. **`MemoryRecord` vs `KnowledgeNode` as the trait currency.** - - Candidate A: `MemoryRecord` (new, lean type matching the PRD) -- chosen. - - Candidate B: use existing `KnowledgeNode` directly. - - **Recommendation: A.** `KnowledgeNode` carries 30+ FSRS / dual-strength / sentiment / temporal fields that bind callers to the SQLite columns. `MemoryRecord` is what `PgMemoryStore` and future backends will want. SQLite impl converts between the two at the boundary, which is a ~40-line `impl From for MemoryRecord` (and back) shim. Pays for itself in Phase 2. - -2. **`async fn` in traits vs `Box` via `async-trait`.** - - Candidate A: use `trait-variant` (RPITIT-based, MSRV 1.75+, our MSRV is 1.91). - - Candidate B: use `async-trait` (allocates one Box per call). - - **Recommendation: A.** `trait-variant` generates both the base `LocalMemoryStore` and the `Send`-bound `MemoryStore` from one definition, matches what the PRD explicitly calls out, and avoids the allocation overhead of boxed futures on every CRUD call. - -3. **Blocking SQLite under async signatures: spawn_blocking vs inline.** - - Candidate A: bodies call the existing sync `self.writer.lock()...` inline inside the `async fn`. - - Candidate B: bodies wrap in `tokio::task::spawn_blocking`. - - **Recommendation: A for Phase 1.** The current call sites are a mix of sync (CLI, bin/restore.rs) and async (MCP handlers). Introducing `spawn_blocking` would force a tokio runtime even for CLI use. Inline blocking under `async fn` is a documented pattern that compiles and works; under Phase 2 the Postgres impl uses `sqlx` which is natively async, and we can revisit Sqlite blocking policy at that point. Phase 1 priority is "no behavior change". - -4. **Where does `register_model` get called from: storage side auto-register, or caller-side explicit?** - - Candidate A: caller explicitly calls `store.register_model(embedder.signature())` once after `MemoryStore::init`. - - Candidate B: first `insert` with a vector auto-registers. - - **Recommendation: B.** The current code path (`Storage::ingest` -> `generate_embedding_for_node` -> INSERT into `node_embeddings`) has no explicit registration step and we want `--no behavior change`. Auto-register on first embedded write preserves the exact current UX. Callers who care (migration tooling, Phase 2 `--reembed`) can still call `register_model` explicitly; it is a no-op when idempotent. - -5. **`model_hash` content: fastembed ONNX bytes vs `(name, dim, crate_version)`.** - - Candidate A: hash the ONNX file bytes on disk (after model download). - - Candidate B: hash `(name, dim, vestige-core CARGO_PKG_VERSION)`. - - **Recommendation: B.** Fastembed caches ONNX files under `FASTEMBED_CACHE_PATH`; reading them from inside `FastembedEmbedder::new()` couples the embedder to fastembed's caching behavior and adds slow startup. Hashing `(name, dim, our crate version)` catches the "silent model drift between vestige versions" case the ADR calls out under Risks. Phase 2 can add a content-hashed `OnnxEmbedder` that loads any file and genuinely hashes it; the trait method signature stays the same. - -6. **`LocalMemoryStore` `Sync + 'static` or just `Sync`.** - - Candidate A: `Sync + 'static`. - - Candidate B: `Sync`. - - **Recommendation: A.** `'static` is required for `Arc` which is the target call pattern (Axum, MCP server, cognitive engine). Every impl we have in mind -- `SqliteMemoryStore`, `PgMemoryStore` -- holds owned state (connection pool, vector index), so `'static` is free. - -7. **Should trait methods appear on the SQLite impl instead of being separate?** - - Candidate A: keep the current ~85 inherent methods on `SqliteMemoryStore` AND add the `impl LocalMemoryStore` block. - - Candidate B: move every inherent method into the trait. - - **Recommendation: A.** Many inherent methods (e.g. `run_rac1_cascade_sweep`, `apply_rac1_cascade`, `save_insight`, `save_connection`, `preview_review`, `get_memory_subgraph`) have SQLite-specific semantics, transactional behavior, and call patterns that do not belong in a backend-agnostic trait. They will stay SQLite-only or be extracted into new traits in a post-Phase-4 cleanup. Phase 1's job is to expose the `~25 methods` contract the ADR specifies, not to retrofit the entire API. - -8. **Where do `Domain` bytes (centroid) live?** - - Candidate A: `BLOB` column on `domains` table. - - Candidate B: JSON-encoded array of f32 in a `TEXT` column. - - **Recommendation: A.** Consistent with how `node_embeddings.embedding` already stores vectors (little-endian f32 bytes via `Embedding::to_bytes`). JSON would triple the storage size and slow deserialization. The `Domain::centroid: Vec` field round-trips through the same codec. - -9. **Migration numbering when Phase 2 also wants to add a migration.** - - Candidate A: Phase 1 takes V12, Phase 2 takes V13. - - Candidate B: Phase 1 takes V12, Phase 2 re-shapes V12 to include its changes. - - **Recommendation: A.** Migrations are forward-only and append-only in this project. Phase 2 adds V13 (for `review_events` append-only table, if that lands in Phase 2 -- otherwise it is Phase 5 work). - -10. **Integration test crate location: sibling to `tests/e2e/` or inside `crates/vestige-core/tests/`.** - - Candidate A: new workspace member at `tests/phase_1/` (sibling to `tests/e2e/`). - - Candidate B: under `crates/vestige-core/tests/` (standard cargo integration-test layout). - - **Recommendation: A.** Matches the existing pattern of `tests/e2e/`, which is already a workspace member with its own `Cargo.toml`. Keeps the Phase 1 test binary outputs in a predictable location (`target/debug/deps/trait_round_trip-*`). Also avoids the build-graph cycle where `crates/vestige-core/tests/` would re-link everything under `vestige-core` each edit. - -### Critical Files for Implementation - -- /home/delandtj/prppl/vestige/crates/vestige-core/src/storage/memory_store.rs (new; contains the `LocalMemoryStore` / `MemoryStore` traits plus `MemoryRecord`, `SchedulingState`, `SearchQuery`, `SearchResult`, `MemoryEdge`, `Domain`, `ClassificationResult`, `StoreStats`, `HealthStatus`, `MemoryStoreError`, `ModelSignature`) -- /home/delandtj/prppl/vestige/crates/vestige-core/src/storage/sqlite.rs (rename `Storage` -> `SqliteMemoryStore`, add the `impl LocalMemoryStore` block and the `enforce_model` / `row_to_record` helpers; ~200 line diff on a 4592-line file) -- /home/delandtj/prppl/vestige/crates/vestige-core/src/storage/migrations.rs (append `Migration { version: 12, ... }` + `MIGRATION_V12_UP` constant; ~80 new lines) -- /home/delandtj/prppl/vestige/crates/vestige-core/src/embedder/mod.rs (new; `Embedder` + `LocalEmbedder` traits, `EmbedderError`, default `embed_batch`) -- /home/delandtj/prppl/vestige/crates/vestige-core/src/embedder/fastembed.rs (new; `FastembedEmbedder` implementation adapting the existing `EmbeddingService`) diff --git a/docs/plans/0001a-trait-rewrite.md b/docs/plans/0001a-trait-rewrite.md deleted file mode 100644 index 354e138..0000000 --- a/docs/plans/0001a-trait-rewrite.md +++ /dev/null @@ -1,689 +0,0 @@ -# Phase 1 Amendment Sub-Plan: async_trait -> trait_variant - -**Status**: Draft -**Depends on**: Phase 1 (already on `feat/storage-trait-phase1`, tip 790c0c8) -**Followed by**: `docs/plans/0001c-async-trait-sunset.md` (Embedder rewrite + async-trait crate removal) -**Related**: docs/adr/0002-phase-2-execution.md (decision D1), docs/plans/0001-phase-1-storage-trait-extraction.md - ---- - -## Context - -Phase 1 froze with the storage trait declared as -`#[async_trait::async_trait] pub trait MemoryStore: Send + Sync + 'static` -and a `pub use MemoryStore as LocalMemoryStore;` alias. `async_trait` boxes -every `async fn` in the trait into `Pin>`. That is one -heap allocation per call inside the hottest code path in Vestige (insert, -search, update_scheduling are all on this surface). It also collapses the -two intended trait shapes -- a Send-bound `MemoryStore` for tokio/axum and a -non-Send `LocalMemoryStore` for thread-local backends -- into a single trait -behind an alias, removing the type-system signal we will want for Phase 2's -Postgres backend. - -ADR 0002 decision D1 supersedes this. The amendment lands on the existing -`feat/storage-trait-phase1` branch before Phase 2 starts, before the PR is -opened upstream. The end state: - -- `LocalMemoryStore` is the source-of-truth trait, declared with native - async-fn-in-trait (stable on MSRV 1.91), no `Sync` bound on the trait - itself, and `Sync + 'static` on the trait object. -- `MemoryStore` is auto-generated by `#[trait_variant::make(MemoryStore: Send)]` - with `Send` bounds on every returned future, so `Arc` is - movable across `tokio::spawn`. -- `trait-variant` 0.1 is already present in `crates/vestige-core/Cargo.toml`. - The `async-trait` crate dependency stays in place through this sub-plan - (it is still in use by the embedder impl); removing it is the job of - `0001c-async-trait-sunset.md`. -- No production caller changes. Every production call site holds - `Arc` (the concrete `SqliteMemoryStore` alias), which the trait - rewrite does not touch. Only the trait declaration and impl blocks change. - ---- - -## Scope - -### In scope - -- Rewrite `MemoryStore` / `LocalMemoryStore` declaration in - `crates/vestige-core/src/storage/memory_store.rs` to use - `#[trait_variant::make(MemoryStore: Send)] pub trait LocalMemoryStore`. -- Remove the `pub use MemoryStore as LocalMemoryStore;` alias from the same - file. `LocalMemoryStore` becomes the real trait; `MemoryStore` is the - generated Send variant. -- Drop the `#[async_trait::async_trait]` attribute on the SQLite impl block - in `crates/vestige-core/src/storage/sqlite.rs` (line 6274). The impl block - switches from `impl MemoryStore for SqliteMemoryStore` (currently spelled - through the `LocalMemoryStore` alias) to `impl LocalMemoryStore for - SqliteMemoryStore`. `trait_variant` provides a blanket `impl MemoryStore for T` so `&dyn MemoryStore` and - `Arc` keep working unchanged. -- Update doc comments in `memory_store.rs` to drop - references to `#[async_trait::async_trait]` and instead describe the - `trait_variant` mechanism. -- Keep the existing Phase 1 test suite (`tests/phase_1/*.rs`) green. The - tests already use `Arc` and `Box`, which is - exactly the surface the rewrite is meant to preserve. - -### Out of scope -- moved to 0001c - -- Rewriting the `Embedder` / `LocalEmbedder` trait declaration -- handled by - `0001c-async-trait-sunset.md`. -- Dropping `#[async_trait::async_trait]` from - `crates/vestige-core/src/embedder/fastembed.rs`. -- Removing `async-trait = "0.1"` from `crates/vestige-core/Cargo.toml`. -- The hard `grep -rn 'async_trait' crates/` zero-match gate (async_trait is - still present in the embedder module after 0001a alone). - -### Out of scope - -- The SQLite file split (`sqlite.rs` -> `sqlite/` directory). That is the - sibling sub-plan `0001b-sqlite-split.md`. -- Any production-side refactor that switches `Arc` to - `Arc`. Production code keeps using the concrete alias. -- Adding `register_model` / `from_pool` / Postgres-specific variants of - `MemoryStoreError`. Those land with the Postgres backend in Phase 2. -- Removing the `pub type Storage = SqliteMemoryStore;` alias. - ---- - -## Prerequisites - -### Current state (verified) - -- `crates/vestige-core/Cargo.toml` already declares - `trait-variant = "0.1"` (line 117). `async-trait = "0.1"` (line 119) - remains in place for the duration of this sub-plan; `0001c` removes it. -- `crates/vestige-core/src/storage/memory_store.rs:194` declares the trait - with `#[async_trait::async_trait] pub trait MemoryStore: Send + Sync + - 'static`. -- `crates/vestige-core/src/storage/memory_store.rs:262` has - `pub use MemoryStore as LocalMemoryStore;`. -- `crates/vestige-core/src/storage/sqlite.rs:6274` declares - `#[async_trait::async_trait] impl - crate::storage::memory_store::LocalMemoryStore for SqliteMemoryStore`. -- Production call sites use `Arc` (the concrete - `SqliteMemoryStore` alias). Confirmed by grep: - `grep -rn "dyn MemoryStore\|dyn LocalMemoryStore" --include="*.rs"` - returns hits only in `tests/phase_1/*.rs`, in two comments inside - `memory_store.rs` and `sqlite.rs`, and in one test-only `&dyn MemoryStore` - cast inside `sqlite.rs::tests` (line 8669). -- Workspace-wide `async_trait` usages: only the trait declarations and - impl blocks in `memory_store.rs`, `sqlite.rs`, `embedder/mod.rs`, and - `embedder/fastembed.rs` (verified by - `grep -rn async_trait --include="*.rs"`). This sub-plan touches only the - first two; the embedder files are addressed by `0001c`. - -### Required crates - -| Crate | Version | Action | -|-------|---------|--------| -| `trait-variant` | `0.1` | Already declared. Verify present after edit. | -| `async-trait` | `0.1` | Unchanged for this sub-plan (still used by embedder impl). Removed by `0001c`. | -| `blake3`, `thiserror`, `chrono`, `uuid`, `serde`, `serde_json` | unchanged | unchanged | - ---- - -## Files Touched - -Grouped by category. Every change is listed; nothing else gets touched. - -### Trait declarations (vestige-core) - -| File | Lines (approx) | Change | -|------|----------------|--------| -| `crates/vestige-core/src/storage/memory_store.rs` | 183-262 | Replace `#[async_trait::async_trait]` block with `#[trait_variant::make(MemoryStore: Send)] pub trait LocalMemoryStore`. Drop the `pub use MemoryStore as LocalMemoryStore;` alias. Update doc comments. | - -### Impl blocks (vestige-core) - -| File | Lines (approx) | Change | -|------|----------------|--------| -| `crates/vestige-core/src/storage/sqlite.rs` | 6274 (impl block header only) | Delete the `#[async_trait::async_trait]` attribute. Keep the `impl crate::storage::memory_store::LocalMemoryStore for SqliteMemoryStore { ... }` body verbatim. | - -### Cargo dependency cleanup - -None in this sub-plan. The `async-trait` crate stays declared in -`crates/vestige-core/Cargo.toml` because the embedder impl still uses it. -`0001c-async-trait-sunset.md` removes the dependency after the embedder -side is rewritten. - -### Cognitive module call sites - -No changes. The 29 cognitive modules under `crates/vestige-core/src/neuroscience/` -and `crates/vestige-core/src/advanced/` already operate on concrete -`SqliteMemoryStore` (via the `Storage` alias) or on plain data types -(`KnowledgeNode`, `Vec`, `ConnectionRecord`). Grep verified zero -production references to `dyn MemoryStore` or `dyn LocalMemoryStore`. - -### MCP call sites - -No changes. All ~30 `vestige-mcp/src/**.rs` files holding `Arc` -keep working because: -- `Storage` is still `pub type Storage = SqliteMemoryStore;` (unchanged). -- They call inherent methods on the concrete type, never via a trait object. -- `SqliteMemoryStore` implements `LocalMemoryStore`; `trait_variant` auto- - generates a blanket `impl MemoryStore for T`, - so the concrete type also satisfies `MemoryStore` for any future caller - that wants the trait-object form. - -### Test files (vestige-core integration tests) - -| File | Lines | Change | -|------|-------|--------| -| `tests/phase_1/trait_round_trip.rs` | 7-18, 134 | No change. Already uses `Arc` and `use vestige_core::storage::MemoryStore`. trait_variant emits a `MemoryStore` trait at the same path, so the imports resolve. | -| `tests/phase_1/send_bound_variant.rs` | 10-12, 36, 57 | No change. Already asserts the trait_variant Send invariant; the rewrite is what makes the doc comment on lines 3-4 actually true. | -| `tests/phase_1/cognitive_module_isolation.rs` | 11, 37, 76, 102, 115 | No change. | -| `tests/phase_1/embedding_model_registry.rs` | 10 | No change. | -| `tests/phase_1/domain_column_migration.rs` | 98 | No change. | -| `crates/vestige-core/src/storage/sqlite.rs::tests` | 8666-8675 | No change. The existing test casts `&s` to `&dyn MemoryStore` and calls trait methods through that vtable; trait_variant preserves this exact dyn-compatible surface. | - -### Documentation - -| File | Change | -|------|--------| -| `crates/vestige-core/src/storage/memory_store.rs` | Rewrite the trait-level doc comment (lines 185-193) to describe trait_variant rather than async_trait. | -| `CLAUDE.md` | No change. The repo-level architecture notes do not name the trait attribute. | - ---- - -## Trait Declaration Rewrite - -### Before (current state on `feat/storage-trait-phase1`) - -`crates/vestige-core/src/storage/memory_store.rs:183-262`: - -```rust -// ---------------------------------------------------------------------------- -// TRAIT -// ---------------------------------------------------------------------------- - -/// The single storage abstraction. -/// -/// `#[async_trait::async_trait]` makes every `async fn` return a -/// `Pin>`, which is required for `Arc` -/// to be movable across `tokio::spawn` boundaries. -/// -/// `LocalMemoryStore` is a type alias kept for source compatibility with code -/// that refers to the non-send variant. In Phase 1 both names refer to the same -/// (dyn-compatible, Send-safe) trait. -#[async_trait::async_trait] -pub trait MemoryStore: Send + Sync + 'static { - // --- Lifecycle --- - async fn init(&self) -> MemoryStoreResult<()>; - async fn health_check(&self) -> MemoryStoreResult; - - // ... 25 more async fn ... - - async fn vacuum(&self) -> MemoryStoreResult<()>; -} - -/// Type alias kept for source compatibility. Both names refer to the same -/// `async_trait`-annotated trait that is dyn-compatible and `Send + Sync`. -pub use MemoryStore as LocalMemoryStore; -``` - -### After - -`crates/vestige-core/src/storage/memory_store.rs:183-262`: - -```rust -// ---------------------------------------------------------------------------- -// TRAIT -// ---------------------------------------------------------------------------- - -/// The single storage abstraction. -/// -/// `LocalMemoryStore` is the source-of-truth trait. The -/// `#[trait_variant::make(MemoryStore: Send)]` attribute auto-generates a -/// `MemoryStore` trait whose returned futures are `Send`, so -/// `Arc` is movable across `tokio::spawn` boundaries while -/// `Arc` remains usable on single-threaded executors -/// and thread-local backends. -/// -/// Every method is native async-fn-in-trait (stable on MSRV 1.91); no -/// per-call heap allocation, no boxed futures. -#[trait_variant::make(MemoryStore: Send)] -pub trait LocalMemoryStore: Sync + 'static { - // --- Lifecycle --- - async fn init(&self) -> MemoryStoreResult<()>; - async fn health_check(&self) -> MemoryStoreResult; - - // --- Embedding model registry --- - async fn registered_model(&self) -> MemoryStoreResult>; - async fn register_model(&self, sig: &ModelSignature) -> MemoryStoreResult<()>; - - // --- CRUD --- - async fn insert(&self, record: &MemoryRecord) -> MemoryStoreResult; - async fn get(&self, id: Uuid) -> MemoryStoreResult>; - async fn update(&self, record: &MemoryRecord) -> MemoryStoreResult<()>; - async fn delete(&self, id: Uuid) -> MemoryStoreResult<()>; - - // --- Search --- - async fn search(&self, query: &SearchQuery) -> MemoryStoreResult>; - async fn fts_search(&self, text: &str, limit: usize) -> MemoryStoreResult>; - async fn vector_search( - &self, - embedding: &[f32], - limit: usize, - ) -> MemoryStoreResult>; - - // --- FSRS Scheduling --- - async fn get_scheduling( - &self, - memory_id: Uuid, - ) -> MemoryStoreResult>; - async fn update_scheduling(&self, state: &SchedulingState) -> MemoryStoreResult<()>; - async fn get_due_memories( - &self, - before: DateTime, - limit: usize, - ) -> MemoryStoreResult>; - - // --- Graph (spreading activation) --- - async fn add_edge(&self, edge: &MemoryEdge) -> MemoryStoreResult<()>; - async fn get_edges( - &self, - node_id: Uuid, - edge_type: Option<&str>, - ) -> MemoryStoreResult>; - async fn remove_edge(&self, source: Uuid, target: Uuid) -> MemoryStoreResult<()>; - async fn get_neighbors( - &self, - node_id: Uuid, - depth: usize, - ) -> MemoryStoreResult>; - - // --- Domains --- - async fn list_domains(&self) -> MemoryStoreResult>; - async fn get_domain(&self, id: &str) -> MemoryStoreResult>; - async fn upsert_domain(&self, domain: &Domain) -> MemoryStoreResult<()>; - async fn delete_domain(&self, id: &str) -> MemoryStoreResult<()>; - async fn classify(&self, embedding: &[f32]) -> MemoryStoreResult>; - - // --- Bulk / Maintenance --- - async fn count(&self) -> MemoryStoreResult; - async fn get_stats(&self) -> MemoryStoreResult; - async fn vacuum(&self) -> MemoryStoreResult<()>; -} -``` - -Notes: - -- The `pub use MemoryStore as LocalMemoryStore;` line on the current - `memory_store.rs:262` is **deleted** entirely. `MemoryStore` is now the - generated trait that `trait_variant::make` emits at the same module path; - `LocalMemoryStore` is the source-of-truth declaration. Both names export - from `storage/mod.rs` already (see lines 10-14 of that file). -- `Sync + 'static` on `LocalMemoryStore` (and no `Send` bound on the trait - itself) is correct: `Send` on the trait is what `trait_variant::make` - inserts when it emits the `MemoryStore` variant. The current trait carries - `Send + Sync + 'static`; the rewrite drops the `Send` bound from the - local variant. `Arc` is `Sync` but not `Send`; - `Arc` (the generated variant) is `Send + Sync`. -- `trait_variant` 0.1 does **not** require any attribute on the impl block. - The attribute lives only on the trait declaration. See the next section. - -The Embedder trait rewrite uses the identical `trait_variant::make` pattern -and is fully specified in `0001c-async-trait-sunset.md`. - ---- - -## Impl Block Migration - -`trait_variant` 0.1 attaches the attribute only to the trait declaration. -The impl side is plain `impl LocalMemoryStore for SqliteMemoryStore`; no -attribute on the impl, no `#[trait_variant::make(MemoryStore: Send)]` on the -impl block. The crate auto-generates the blanket -`impl MemoryStore for T`, so any concrete type -that implements `LocalMemoryStore` automatically also implements -`MemoryStore` provided it is `Send`. - -`SqliteMemoryStore` already is `Send + Sync` (it holds `Mutex` -fields whose `Mutex` is `std::sync::Mutex`, which is `Send + Sync` when its -guarded type is `Send`; `rusqlite::Connection` is `Send`). No new bound is -needed. - -### Before - -`crates/vestige-core/src/storage/sqlite.rs:6274`: - -```rust -#[async_trait::async_trait] -impl crate::storage::memory_store::LocalMemoryStore for SqliteMemoryStore { - async fn init(&self) -> crate::storage::memory_store::MemoryStoreResult<()> { - // Migrations run in `new`; this is a no-op for the SQLite backend. - Ok(()) - } - - // ... ~2400 lines of method bodies, unchanged ... -} -``` - -### After - -`crates/vestige-core/src/storage/sqlite.rs:6274`: - -```rust -impl crate::storage::memory_store::LocalMemoryStore for SqliteMemoryStore { - async fn init(&self) -> crate::storage::memory_store::MemoryStoreResult<()> { - // Migrations run in `new`; this is a no-op for the SQLite backend. - Ok(()) - } - - // ... ~2400 lines of method bodies, unchanged ... -} -``` - -Diff is exactly one removed line (the `#[async_trait::async_trait]` attribute). -Every method body, every `async fn` signature, every `use` statement inside -the impl block stays verbatim. No `Box::pin(async move { ... })`, no manual -`Pin>`, no `'async_trait` lifetime markers; native -async-fn-in-trait does this directly. - -The Embedder impl block rewrite follows the identical "remove one -`#[async_trait::async_trait]` attribute" pattern and is fully specified in -`0001c-async-trait-sunset.md`. - -### Why the impl block does not need an attribute - -`trait_variant::make` generates two things from the source trait: - -1. The source trait itself (`LocalMemoryStore`), with native async fns. -2. A second trait (`MemoryStore`) whose method signatures return - `impl Future + Send` instead of `impl Future`, - plus a blanket impl wiring concrete types through. - -Both are emitted at the macro-call site. `SqliteMemoryStore` only writes one -impl block (against `LocalMemoryStore`); the macro-generated blanket -guarantees `SqliteMemoryStore: MemoryStore` for free. The current `&dyn -MemoryStore` casts (sqlite.rs:8669; tests under tests/phase_1/) keep -type-checking unchanged. - ---- - -## Call Site Audit - -Verified via: - -```bash -grep -rn "dyn MemoryStore\|dyn LocalMemoryStore" --include="*.rs" \ - /home/delandtj/prppl/vestige-phase2/ -grep -rn "Arc\|Arc" --include="*.rs" \ - /home/delandtj/prppl/vestige-phase2/ -grep -rn "use.*MemoryStore;\|use.*LocalMemoryStore;" --include="*.rs" \ - /home/delandtj/prppl/vestige-phase2/ -``` - -### Files that reference the trait object form (`dyn MemoryStore` / `dyn LocalMemoryStore`) - -All test-only or doc-comment-only: - -| File | Line | Use | Required change | -|------|------|-----|-----------------| -| `tests/phase_1/trait_round_trip.rs` | 7-18 | `Arc` in `make_store()` and test bodies | None. `MemoryStore` is the generated Send variant; signature stays. | -| `tests/phase_1/trait_round_trip.rs` | 134 | `Arc` upcast inside a test body | None. | -| `tests/phase_1/send_bound_variant.rs` | 10-97 | `Arc` moved across `tokio::spawn` | None. This test becomes meaningfully correct only after the rewrite (currently it relies on async_trait boxing; after the rewrite it relies on trait_variant's Send variant -- same observable outcome). | -| `tests/phase_1/cognitive_module_isolation.rs` | 11, 33-115 | `Arc` passed into cognitive module-style closures | None. | -| `tests/phase_1/embedding_model_registry.rs` | 10 | `Arc` in `make_store()` | None. | -| `tests/phase_1/domain_column_migration.rs` | 98 | `Arc` cast inside a migration assertion | None. | -| `crates/vestige-core/src/storage/sqlite.rs` | 8666-8675 | `let dyn_s: &dyn MemoryStore = &s;` inside `mod tests` | None. The cast is testing that the dyn-vtable still resolves the trait methods correctly; after the rewrite it resolves through the `MemoryStore` trait that `trait_variant::make` emits at the same path. | -| `crates/vestige-core/src/storage/memory_store.rs` | 188 (doc) | Doc comment mentioning `Arc` | Replaced as part of the doc rewrite (see Trait Declaration section). | - -### Files that hold the concrete type (`Arc` / `Arc`) - -35 files, 116 hits. Every one of them keeps working unchanged because the -concrete `SqliteMemoryStore` type stays exactly as it is. Listed here for -completeness so a reviewer can confirm none of them needs an edit: - -``` -crates/vestige-core/src/storage/mod.rs (alias declaration) -crates/vestige-core/src/storage/sqlite.rs (impl block) -crates/vestige-mcp/src/server.rs (Arc in McpServer) -crates/vestige-mcp/src/cognitive.rs (hydrate(&Storage)) -crates/vestige-mcp/src/autopilot.rs -crates/vestige-mcp/src/protocol/http.rs -crates/vestige-mcp/src/dashboard/mod.rs -crates/vestige-mcp/src/dashboard/state.rs -crates/vestige-mcp/src/dashboard/handlers.rs -crates/vestige-mcp/src/resources/codebase.rs -crates/vestige-mcp/src/resources/memory.rs -crates/vestige-mcp/src/tools/changelog.rs -crates/vestige-mcp/src/tools/codebase_unified.rs -crates/vestige-mcp/src/tools/context.rs -crates/vestige-mcp/src/tools/contradictions.rs -crates/vestige-mcp/src/tools/cross_reference.rs -crates/vestige-mcp/src/tools/dedup.rs -crates/vestige-mcp/src/tools/dream.rs -crates/vestige-mcp/src/tools/explore.rs -crates/vestige-mcp/src/tools/feedback.rs -crates/vestige-mcp/src/tools/graph.rs -crates/vestige-mcp/src/tools/health.rs -crates/vestige-mcp/src/tools/importance.rs -crates/vestige-mcp/src/tools/intention_unified.rs -crates/vestige-mcp/src/tools/maintenance.rs -crates/vestige-mcp/src/tools/memory_states.rs -crates/vestige-mcp/src/tools/memory_unified.rs -crates/vestige-mcp/src/tools/predict.rs -crates/vestige-mcp/src/tools/restore.rs -crates/vestige-mcp/src/tools/review.rs -crates/vestige-mcp/src/tools/search_unified.rs -crates/vestige-mcp/src/tools/session_context.rs -crates/vestige-mcp/src/tools/smart_ingest.rs -crates/vestige-mcp/src/tools/suppress.rs -crates/vestige-mcp/src/tools/tagging.rs -crates/vestige-mcp/src/tools/timeline.rs -``` - -Each holds `Arc` and dispatches to inherent methods on -`SqliteMemoryStore`. None of them goes through a trait vtable. Required -change for every one of them: **none**. - -### Files that `use ...MemoryStore` from production code - -``` -grep -rn "use.*MemoryStore;\|use.*LocalMemoryStore;" --include="*.rs" \ - | grep -v "memory_store.rs\|sqlite.rs\|tests/phase_1" -``` - -returns nothing. Production code does not import the trait by name. - -### Conclusion - -The rewrite is a strictly local change to two source files -(`storage/memory_store.rs` and `storage/sqlite.rs`). Zero production call -sites need edits. The integration tests that consume `Arc` -keep their current form; the rewrite is what gives that signature its -no-box semantics on the storage side. The `Box` surface is -addressed by `0001c-async-trait-sunset.md`. - ---- - -## Commit Sequence - -Two commits, each green on `cargo test -p vestige-core --no-default-features` -and `cargo test -p vestige-core --features embeddings,vector-search`. - -### Commit 1: rewrite MemoryStore / LocalMemoryStore trait declaration - -- Touches: `crates/vestige-core/src/storage/memory_store.rs` only. -- Action: replace lines 183-262 per the "Trait Declaration Rewrite" section - above. Delete the `pub use MemoryStore as LocalMemoryStore;` line. -- Green after: `cargo check -p vestige-core` (the impl block in `sqlite.rs` - still has `#[async_trait::async_trait]` on it, but it still resolves - through the `LocalMemoryStore` trait which is now native-async; the - `async_trait` macro is harmless when applied to a trait that the impl - block targets by path, because the macro rewrites the impl's async fns - into boxed-future fns whose signatures still match the native-async - declarations after trait_variant lowering). If `cargo check` complains - here, fold commit 2 into commit 1. - - **Mitigation if check fails between commits 1 and 2:** combine the two - into a single commit. The split is offered for review convenience; the - build must be green after every commit lands. - -### Commit 2: drop `#[async_trait::async_trait]` from SqliteMemoryStore impl - -- Touches: `crates/vestige-core/src/storage/sqlite.rs` only. -- Action: delete line 6274 (`#[async_trait::async_trait]`). -- Green after: `cargo test -p vestige-core --features embeddings,vector-search`, - including all `trait_*` tests inside `sqlite.rs::tests` (lines 8643-8712) - and the trait-object cast on line 8669. - -### Combined alternative - -If the per-step split feels artificial, commits 1 and 2 can collapse into -a single commit covering both the trait rewrite and the impl-attribute -drop for `MemoryStore`. That is acceptable; the two-commit form is -preferred only because it lets a reviewer bisect trait-rewrite failures -separately from impl-rewrite failures. - -The Embedder / fastembed commits and the `async-trait` Cargo dependency -removal live in `0001c-async-trait-sunset.md`. - ---- - -## Verification - -Every command runs from the repo root unless noted otherwise. - -```bash -# 1. Vestige-core, default features (embeddings + vector-search). -cargo test -p vestige-core --features embeddings,vector-search - -# 2. Vestige-core, minimal features (no embeddings, no vector-search). -cargo test -p vestige-core --no-default-features - -# 3. Workspace build, release mode (catches any feature-gated regression -# in the vestige-mcp tools tree). -cargo build --workspace --release - -# 4. Whole-workspace test (vestige-mcp 406 tests + vestige-core 352 tests -# per the CLAUDE.md baseline). -cargo test --workspace - -# 5. Phase 1 integration tests (these are the trait-shape contract). -cargo test --test trait_round_trip --features embeddings,vector-search -cargo test --test send_bound_variant --features embeddings,vector-search -cargo test --test cognitive_module_isolation --features embeddings,vector-search -cargo test --test embedding_model_registry --features embeddings,vector-search -cargo test --test domain_column_migration --features embeddings,vector-search - -# 6. Clippy gate, deny warnings (matches Phase 1 PR policy of zero warnings). -cargo clippy --workspace --all-targets --features embeddings,vector-search -- -D warnings - -# 7. Storage-side dependency hygiene check (must return zero hits). -# Scoped to the storage module only -- the embedder module still uses -# async_trait until 0001c lands. -grep -rn "async_trait\|async-trait" crates/vestige-core/src/storage/ - -# 8. Confirm trait_variant attribute is in place on the storage trait -# (must return exactly one hit in memory_store.rs). -grep -rn "trait_variant::make" crates/vestige-core/src/storage/ -``` - -Expected outcomes: - -- Command 1: 352 tests pass (matches baseline). -- Command 2: smaller test count, all pass. -- Command 3: workspace compiles in release mode. -- Command 4: 758 total tests pass (matches CLAUDE.md baseline). -- Command 5: each phase_1 integration test binary runs green. The - `send_bound_variant::arc_dyn_memory_store_moves_across_tokio_tasks` test - is the canary; if `MemoryStore` lost its Send-bound future variant, this - fails to compile with "future cannot be sent between threads safely". -- Command 6: zero clippy warnings. The rewrite must not introduce a new - `clippy::needless_lifetimes` or `clippy::async_yields_async`. -- Command 7: empty output. async_trait is gone from the storage module. - The embedder module still contains async_trait; that is removed by - `0001c-async-trait-sunset.md`. -- Command 8: one hit, in `memory_store.rs`. - ---- - -## Acceptance Criteria - -A reviewer should be able to check every box: - -- [ ] `crates/vestige-core/src/storage/memory_store.rs` declares the trait - with `#[trait_variant::make(MemoryStore: Send)] pub trait - LocalMemoryStore: Sync + 'static`, no `async_trait` attribute, no - `Send` bound on `LocalMemoryStore` itself. -- [ ] `crates/vestige-core/src/storage/memory_store.rs` no longer contains - `pub use MemoryStore as LocalMemoryStore;`. -- [ ] `crates/vestige-core/src/storage/sqlite.rs:6274` is plain - `impl crate::storage::memory_store::LocalMemoryStore for - SqliteMemoryStore` -- no attribute on the impl block. -- [ ] `grep -rn "async_trait\|async-trait" crates/vestige-core/src/storage/` - returns zero hits. -- [ ] `grep -rn "trait_variant::make" crates/vestige-core/src/storage/` - returns exactly one hit (the storage trait in `memory_store.rs`). -- [ ] All 758 workspace tests pass (`cargo test --workspace`). -- [ ] Phase 1 integration tests pass with the trait-object surface - (`Arc`) intact. -- [ ] `cargo clippy --workspace --all-targets --features - embeddings,vector-search -- -D warnings` is clean. -- [ ] No production source file under `crates/vestige-mcp/` or - `crates/vestige-core/src/{neuroscience,advanced,consolidation,codebase, - memory,embeddings,embedder}/` was modified by this sub-plan. -- [ ] `Arc` still type-checks at every existing call site (verified - by the workspace test pass). -- [ ] Doc comments on the storage trait declaration describe - `trait_variant`, not `async_trait`. - ---- - -## Risks and Mitigations - -- **`trait_variant` 0.1 macro emits unexpected diagnostics on MSRV 1.91.** - Mitigation: the master Phase 1 plan already prescribed this exact pattern - (`#[trait_variant::make(MemoryStore: Send)] pub trait LocalMemoryStore: - Sync + 'static`, see plan `0001-...` line 274-275); the crate has been in - `vestige-core/Cargo.toml` since Phase 1 landed. If a diagnostic appears, - pin to the exact known-good version with `trait-variant = "=0.1.2"` and - open an upstream issue. -- **Native async-fn-in-trait makes the trait no longer dyn-compatible.** - Mitigation: `trait_variant::make` is specifically the workaround for this - -- it emits both the source trait (for static dispatch) and a Send-bound - variant whose returned futures use `Pin>` only at - the dyn boundary. `Arc` keeps working because the - generated `MemoryStore` trait is dyn-compatible by construction. Verified - by the existing `send_bound_variant::*` tests, which intentionally move - `Arc` across `tokio::spawn` from inside a - `multi_thread` runtime. -- **A cognitive module silently relied on the boxed-future return type.** - Mitigation: grep verified no cognitive module imports `MemoryStore` / - `LocalMemoryStore` or holds an `Arc` form; all of them use the - concrete `Storage` alias. There is no Send-ness expectation downstream to - break. -- **Future bodies inside the SQLite impl capture non-Send locals.** - Mitigation: every method body in `sqlite.rs:6274..` runs synchronous - rusqlite calls inside the same `async fn` frame; no `.await` points - exist inside the bodies that we ship today. The `Send` bound on the - generated `MemoryStore` variant is therefore satisfied automatically. - If a future change adds `.await` inside an impl method, the new - trait_variant surface will surface that as a compile error at the call - site, which is the correct outcome. -- **`async-trait` crate is left declared after this sub-plan.** - This is intentional: the embedder impl still depends on it. The - `0001c-async-trait-sunset.md` sub-plan removes the crate after the - embedder side is rewritten. Grep on the whole workspace returns only - the storage and embedder files; no downstream crate pulls `async-trait`. - ---- - -## Out-of-Band Notes - -- This sub-plan amends `feat/storage-trait-phase1` (tip 790c0c8). The - branch has not been opened upstream yet, so amending in place is safe; - no force-push to a public PR. -- The companion sub-plan `0001b-sqlite-split.md` lands after this one on - the same branch. The trait-rewrite landing first is intentional: the - SQLite split moves the impl block into `storage/sqlite/trait_impl.rs`, - and it is cleaner to move a small attribute-free impl than a - macro-decorated one. -- The companion sub-plan `0001c-async-trait-sunset.md` lands after this - one (order with `0001b` is independent) and finishes the - async_trait -> trait_variant transition for the Embedder trait, then - removes the `async-trait` crate dependency. -- After the Phase 1 amendment sub-plans (`0001a`, `0001b`, `0001c`) land, - the branch is reviewed and merged before Phase 2 sub-plans (`0002a-` - through `0002i-`) begin implementation. diff --git a/docs/plans/0001b-sqlite-split.md b/docs/plans/0001b-sqlite-split.md deleted file mode 100644 index ce2590d..0000000 --- a/docs/plans/0001b-sqlite-split.md +++ /dev/null @@ -1,732 +0,0 @@ -# Sub-Plan 0001b: Split sqlite.rs into a sqlite/ directory - -**Status**: Draft -**Branch**: `feat/storage-trait-phase1` (Phase 1 amendment, PR A) -**Depends on**: `0001a-trait-rewrite.md` (must land first; it carries the -`trait_variant`-generated trait declaration that `trait_impl.rs` matches) -**Related**: `docs/adr/0002-phase-2-execution.md` (D3, D6) - ---- - -## Context - -`crates/vestige-core/src/storage/sqlite.rs` is the single SQLite backend file -that Phase 1 inherited from pre-trait Vestige and then appended the -`LocalMemoryStore` trait impl block to. The file is 8713 lines as of -commit 790c0c8 on `feat/storage-trait-phase1`. ADR 0002 D3 decides to split -it into a `sqlite/` directory before Phase 2 lands `postgres/` as a peer. -Reasoning, in one paragraph: - -The Postgres backend is going in as a directory of seven small files -(`postgres/{mod,pool,migrations,registry,search,migrate_cli,reembed}.rs`). -If SQLite stays as one 8K-line file alongside that, the repo says "backends -look like one big file or seven small ones, pick a side", which forces -every future maintainer to re-litigate the layout. Splitting now -- as -**pure code motion**, no public-API change, no behavioural change, no -migration -- lets both backends look the same, keeps each surface mappable -in a single editor tab, and shrinks the diffs Phase 2 has to review. -This sub-plan is sized as one focused implementation session. - -The split is **private** to `storage/sqlite/`. Cognitive modules continue -to `use crate::storage::SqliteMemoryStore`; the existing re-exports in -`crates/vestige-core/src/storage/mod.rs` keep working without touching -any caller. Tests stay green commit-by-commit. - -This sub-plan depends on `0001a-trait-rewrite.md` landing first because -`sqlite/trait_impl.rs` is the file that picks up the new trait_variant -attribute. Doing the split first would force a second rewrite of -`trait_impl.rs` when the trait rewrite arrives. Order matters; this is -the cheap-to-respect ordering. - ---- - -## Target Layout - -Final directory after this sub-plan: - -``` -crates/vestige-core/src/storage/sqlite/ - mod.rs -- module root: SqliteMemoryStore struct, new(), - reader/writer locks, error types, shared helpers, - portable-sync-related types, record types - crud.rs -- ingest/smart_ingest/get/update/delete/purge/search-by-id - search.rs -- fts, semantic, hybrid, time-based queries - scheduling.rs -- FSRS state, decay, consolidation, review, promote/demote, - suppression, gc, retention, waking tags - graph.rs -- memory_connections (edges), subgraph, neighbors - domain.rs -- domains/domain_scores column readers, classify stub - (Phase 4 will expand this file) - registry.rs -- embedding_model table, enforce_model, register_model body - portable_sync.rs -- portable export/import/sync + merge helpers - trait_impl.rs -- impl LocalMemoryStore for SqliteMemoryStore -``` - -`storage/mod.rs` is unchanged in spirit: it still does `mod sqlite;` and -`pub use sqlite::{...};` -- the only difference is that `sqlite` is now a -directory module instead of a leaf file. The re-export list does not -change. - ---- - -## Current File Map (line numbers from commit 790c0c8) - -The current `sqlite.rs` is structurally: - -| Region | Lines | Contents | -|--------|-------|----------| -| Header | 1-43 | Imports, feature-gated imports | -| Error types | 45-89 | `StorageError`, `Result`, `SmartIngestResult`, `MergeWrite` | -| Portable sync types | 97-211 | `PortableSyncBackend` trait, `FilePortableSyncBackend` struct, `PortableSyncReport`, `PurgeReport` | -| Constants | 233-273 | `PORTABLE_TABLES`, `PORTABLE_USER_DATA_TABLES`, `PortableMergeState`, env constants | -| Struct decl | 287-301 | `SqliteMemoryStore` struct fields | -| Impl block 1 | 303-3740 | Constructor + bulk of native API | -| Record structs | 3747-3866 | `IntentionRecord`, `InsightRecord`, `ConnectionRecord`, `MemoryStateRecord`, `StateTransitionRecord`, `ConsolidationHistoryRecord`, `DreamHistoryRecord`, `Default for InsightRecord` | -| Impl block 2 | 3868-6133 | Intentions / Insights / Connections / States / History / Backup / Portable / GC / Subgraph | -| Impl block 3 | 6139-6272 | Trait-helper methods (`node_to_record`, `read_domain_columns`, `enforce_model`) | -| Trait impl | 6274-7110 | `impl LocalMemoryStore for SqliteMemoryStore` | -| Tests | 7112-8713 | `#[cfg(test)] mod tests`: native API tests + trait round-trip tests | - ---- - -## Mapping Table - -Every public method, every private helper, every struct, every test module -in the current file -- with the destination file in the new layout. Line -ranges cite the current `sqlite.rs` (commit 790c0c8 on -`feat/storage-trait-phase1`, viewed through the -`/home/delandtj/prppl/vestige-phase2` worktree). - -### Header and shared types (-> `sqlite/mod.rs`) - -| Item | Lines | Destination | Notes | -|------|-------|-------------|-------| -| Module-level `use` imports | 1-43 | `sqlite/mod.rs` | Trimmed per-file in destination; what does not fit in `mod.rs` moves with its consumers | -| `StorageError` enum + `Result` alias | 49-71 | `sqlite/mod.rs` | Re-exported through `pub use` chain; called from every sub-module | -| `SmartIngestResult` struct | 73-89 | `sqlite/mod.rs` | Returned by `crud::smart_ingest`; defined here because other code depends on the type | -| `MergeWrite` enum | 91-95 | `sqlite/portable_sync.rs` | Only used by merge helpers | -| `PortableSyncBackend` trait | 97-109 | `sqlite/portable_sync.rs` | Public trait; re-exported through `mod.rs` | -| `FilePortableSyncBackend` struct + `impl` | 111-194 | `sqlite/portable_sync.rs` | | -| `PortableSyncReport` struct | 196-211 | `sqlite/portable_sync.rs` | | -| `PurgeReport` struct | 213-231 | `sqlite/crud.rs` | Returned by `purge_node` | -| `PORTABLE_TABLES` constant | 237-254 | `sqlite/portable_sync.rs` | | -| `PORTABLE_USER_DATA_TABLES` constant | 256-272 | `sqlite/portable_sync.rs` | | -| `PortableMergeState` struct | 274-277 | `sqlite/portable_sync.rs` | | -| `DATA_DIR_ENV` constant | 279 | `sqlite/mod.rs` | Read by constructor | -| `DATABASE_FILE` constant | 280 | `sqlite/mod.rs` | Read by constructor | -| `SqliteMemoryStore` struct decl | 282-301 | `sqlite/mod.rs` | All fields stay public-to-crate within the directory | - -### Constructor and config (-> `sqlite/mod.rs`) - -These are foundational; they live in `mod.rs` because every sub-module -calls them or operates on the struct they build. - -| Item | Lines | Destination | Notes | -|------|-------|-------------|-------| -| `fn data_dir_from_env` | 304-313 | `sqlite/mod.rs` | private helper | -| `fn expand_tilde` | 314-332 | `sqlite/mod.rs` | private helper | -| `fn prepare_data_dir` | 333-346 | `sqlite/mod.rs` | private helper | -| `pub fn db_path_for_data_dir` | 347-355 | `sqlite/mod.rs` | | -| `pub fn default_db_path` | 356-368 | `sqlite/mod.rs` | | -| `fn configure_connection` | 369-396 | `sqlite/mod.rs` | | -| `pub fn new` | 397-457 | `sqlite/mod.rs` | The constructor | -| `pub fn db_path` | 458-462 | `sqlite/mod.rs` | | -| `pub fn data_dir` | 463-467 | `sqlite/mod.rs` | | -| `pub fn sidecar_dir` | 468-473 | `sqlite/mod.rs` | | -| `fn load_embeddings_into_index` | 474-552 | `sqlite/mod.rs` | Called by `new`; touches vector index | - -### CRUD: ingest, get, update, delete, purge (-> `sqlite/crud.rs`) - -| Item | Lines | Destination | Notes | -|------|-------|-------------|-------| -| `pub fn ingest` | 553-643 | `sqlite/crud.rs` | | -| `pub fn smart_ingest` | 644-864 | `sqlite/crud.rs` | Calls vector search via `self.semantic_search`; cross-module call resolved by impl block being on the same struct | -| `pub fn get_node_embedding` (vector-search) | 865-887 | `sqlite/crud.rs` | embedding read for one node | -| `pub fn get_all_embeddings` (vector-search) | 888-914 | `sqlite/crud.rs` | | -| `pub fn get_node_embedding` (no vector-search stub) | 915-919 | `sqlite/crud.rs` | feature-gated alternative | -| `pub fn update_node_content` | 920-951 | `sqlite/crud.rs` | | -| `fn generate_embedding_for_node` | 952-999 | `sqlite/crud.rs` | private helper; only called by ingest and update_node_content | -| `pub fn get_node` | 1000-1011 | `sqlite/crud.rs` | | -| `fn parse_timestamp` | 1012-1027 | `sqlite/mod.rs` | **Shared helper**: row_to_node uses it, intention/insight rows also parse timestamps. Bump to `pub(super) fn` | -| `fn row_to_node` | 1028-1119 | `sqlite/mod.rs` | **Shared helper**: crud reads single nodes; search.rs builds node lists from rows; scheduling.rs returns nodes for review queue. Bump to `pub(super) fn`. Static method (no `&self`) so a free function in `mod.rs` is fine | -| `pub fn delete_node` | 1842-1869 | `sqlite/crud.rs` | | -| `pub fn purge_node` | 1870-1987 | `sqlite/crud.rs` | | -| `fn node_exists` | 1988-1996 | `sqlite/crud.rs` | static helper, called only by purge | -| `fn record_sync_tombstone` | 1997-2014 | `sqlite/crud.rs` | static helper, called by delete and purge | -| `pub fn get_all_nodes` | 2268-2291 | `sqlite/crud.rs` | bulk read | -| `pub fn get_nodes_by_type_and_tag` | 2292-2342 | `sqlite/crud.rs` | bulk read | - -### Search: fts, semantic, hybrid, temporal (-> `sqlite/search.rs`) - -| Item | Lines | Destination | Notes | -|------|-------|-------------|-------| -| `pub fn recall` | 1120-1147 | `sqlite/search.rs` | top-level recall path | -| `fn keyword_search` | 1148-1180 | `sqlite/search.rs` | private | -| `pub fn search` | 2015-2043 | `sqlite/search.rs` | | -| `pub fn search_terms` | 2044-2075 | `sqlite/search.rs` | | -| `pub fn concrete_search_filtered` | 2076-2172 | `sqlite/search.rs` | | -| `fn upsert_concrete_result` | 2173-2197 | `sqlite/search.rs` | static helper | -| `fn normalize_literal_query` | 2198-2210 | `sqlite/search.rs` | static helper | -| `fn escape_like` | 2211-2224 | `sqlite/search.rs` | static helper | -| `fn literal_match_score` | 2225-2248 | `sqlite/search.rs` | static helper | -| `fn node_matches_type_filters` | 2249-2267 | `sqlite/search.rs` | static helper | -| `pub fn is_embedding_ready` (both feature variants) | 2343-2354 | `sqlite/search.rs` | both versions move together | -| `pub fn init_embeddings` (both feature variants) | 2355-2367 | `sqlite/search.rs` | both versions move together | -| `fn get_query_embedding` | 2368-2400 | `sqlite/search.rs` | private; uses `query_cache` field | -| `pub fn semantic_search` | 2401-2434 | `sqlite/search.rs` | | -| `pub fn hybrid_search` (feature on) | 2435-2452 | `sqlite/search.rs` | | -| `pub fn hybrid_search_filtered` (feature on) | 2453-2581 | `sqlite/search.rs` | | -| `pub fn hybrid_search` (feature off) | 2582-2593 | `sqlite/search.rs` | feature-gated stub | -| `pub fn hybrid_search_filtered` (feature off) | 2594-2635 | `sqlite/search.rs` | feature-gated stub | -| `fn keyword_search_with_scores` | 2636-2726 | `sqlite/search.rs` | | -| `fn semantic_search_raw` | 2727-2765 | `sqlite/search.rs` | | -| `pub fn generate_embeddings` | 2766-2819 | `sqlite/search.rs` | populates embeddings post hoc | -| `fn embedding_regeneration_candidates` | 2820-2891 | `sqlite/search.rs` | called by generate_embeddings | -| `pub fn query_at_time` | 2892-2933 | `sqlite/search.rs` | temporal query | -| `pub fn query_time_range` | 2934-3005 | `sqlite/search.rs` | temporal query | -| `fn embedding_model_matches_active` (associated fn) | 5652-5671 | `sqlite/search.rs` | static helper for hybrid_search; promoted to `pub(super)` (test references it) | -| `fn embedding_model_supports_matryoshka` | 5672-5677 | `sqlite/search.rs` | static helper | -| `fn embedding_vector_for_active_model` | 5678-5697 | `sqlite/search.rs` | static helper | -| `fn active_embedding_model_like_pattern` | 5698-5713 | `sqlite/search.rs` | static helper | - -### Scheduling: FSRS, decay, consolidation, review, promote/demote, suppression, gc, retention (-> `sqlite/scheduling.rs`) - -This is the busiest destination file. The grouping rule is: anything that -touches FSRS scheduling fields (`stability`, `difficulty`, `retrievability`, -`reps`, `lapses`, `retention_strength`, `retrieval_strength`) or the -review/decay/consolidation/gc lifecycle lives here. - -| Item | Lines | Destination | Notes | -|------|-------|-------------|-------| -| `pub fn mark_reviewed` | 1181-1275 | `sqlite/scheduling.rs` | FSRS state mutation | -| `pub fn strengthen_on_access` | 1276-1344 | `sqlite/scheduling.rs` | | -| `pub fn strengthen_batch_on_access` | 1345-1357 | `sqlite/scheduling.rs` | | -| `pub fn mark_memory_useful` | 1358-1377 | `sqlite/scheduling.rs` | | -| `fn log_access` | 1378-1393 | `sqlite/scheduling.rs` | private | -| `pub fn promote_memory` | 1394-1425 | `sqlite/scheduling.rs` | | -| `pub fn demote_memory` | 1426-1472 | `sqlite/scheduling.rs` | | -| `pub fn suppress_memory` | 1473-1504 | `sqlite/scheduling.rs` | active forgetting | -| `pub fn reverse_suppression` | 1505-1552 | `sqlite/scheduling.rs` | | -| `pub fn count_suppressed` | 1553-1567 | `sqlite/scheduling.rs` | | -| `pub fn get_recently_suppressed` | 1568-1594 | `sqlite/scheduling.rs` | | -| `pub fn apply_rac1_cascade` | 1595-1641 | `sqlite/scheduling.rs` | active forgetting cascade | -| `pub fn run_rac1_cascade_sweep` | 1642-1657 | `sqlite/scheduling.rs` | | -| `pub fn get_review_queue` | 1658-1681 | `sqlite/scheduling.rs` | | -| `pub fn preview_review` | 1682-1712 | `sqlite/scheduling.rs` | | -| `pub fn get_stats` | 1713-1841 | `sqlite/scheduling.rs` | reports retention/lapses/review counts; lives here for symmetry with the FSRS reporters next door | -| `pub fn apply_decay` | 3006-3095 | `sqlite/scheduling.rs` | core decay loop | -| `fn get_fsrs_w20` | 3096-3119 | `sqlite/scheduling.rs` | | -| `pub fn run_consolidation` | 3120-3407 | `sqlite/scheduling.rs` | | -| `fn auto_dedup_consolidation` | 3408-3538 | `sqlite/scheduling.rs` | called by run_consolidation | -| `fn compute_act_r_activations` | 3539-3605 | `sqlite/scheduling.rs` | called by run_consolidation | -| `fn prune_access_log` | 3606-3620 | `sqlite/scheduling.rs` | called by run_consolidation | -| `fn optimize_w20_if_ready` | 3621-3720 | `sqlite/scheduling.rs` | called by run_consolidation | -| `fn generate_missing_embeddings` | 3721-3740 | `sqlite/scheduling.rs` | called by run_consolidation | -| `pub fn get_state_transitions` | 5714-5748 | `sqlite/scheduling.rs` | audit trail tied to scheduling state | -| `pub fn get_avg_retention` | 5780-5792 | `sqlite/scheduling.rs` | | -| `pub fn get_retention_distribution` | 5794-5825 | `sqlite/scheduling.rs` | | -| `pub fn get_retention_trend` | 5826-5858 | `sqlite/scheduling.rs` | | -| `pub fn save_retention_snapshot` | 5859-5878 | `sqlite/scheduling.rs` | | -| `pub fn count_memories_below_retention` | 5879-5892 | `sqlite/scheduling.rs` | | -| `pub fn gc_below_retention` | 5893-5936 | `sqlite/scheduling.rs` | | -| `pub fn auto_promote_frequent_access` | 5937-5985 | `sqlite/scheduling.rs` | | -| `pub fn set_waking_tag` | 5986-5998 | `sqlite/scheduling.rs` | waking SWR tagging | -| `pub fn clear_waking_tags` | 5999-6011 | `sqlite/scheduling.rs` | | -| `pub fn get_waking_tagged_memories` | 6012-6028 | `sqlite/scheduling.rs` | | -| `pub fn get_recent_state_transitions` | 6105-6132 | `sqlite/scheduling.rs` | | - -### Graph: edges (memory_connections), neighbors, subgraph (-> `sqlite/graph.rs`) - -| Item | Lines | Destination | Notes | -|------|-------|-------------|-------| -| `pub fn save_connection` | 4180-4202 | `sqlite/graph.rs` | | -| `pub fn get_connections_for_memory` | 4203-4220 | `sqlite/graph.rs` | | -| `pub fn get_all_connections` | 4221-4236 | `sqlite/graph.rs` | | -| `pub fn strengthen_connection` | 4237-4259 | `sqlite/graph.rs` | | -| `pub fn apply_connection_decay` | 4260-4272 | `sqlite/graph.rs` | | -| `pub fn prune_weak_connections` | 4273-4284 | `sqlite/graph.rs` | | -| `fn row_to_connection` | 4285-4305 | `sqlite/graph.rs` | private | -| `pub fn get_most_connected_memory` | 6029-6046 | `sqlite/graph.rs` | | -| `pub fn get_memory_subgraph` | 6048-6103 | `sqlite/graph.rs` | calls `get_connections_for_memory`, `get_node`, `get_all_connections` -- all resolvable through `self` | - -### Domain (-> `sqlite/domain.rs`) - -Phase 1 keeps domain logic to JSON-column reads + classify stub. Phase 4 -expands this file. Keeping the file in the split so Phase 4 has an -obvious place to add to. - -| Item | Lines | Destination | Notes | -|------|-------|-------------|-------| -| `fn read_domain_columns` | 6167-6196 | `sqlite/domain.rs` | private helper used by trait `get`. Bump to `pub(super)` | - -The trait methods `list_domains` / `get_domain` / `upsert_domain` / -`delete_domain` / `classify` live in `sqlite/trait_impl.rs`; they -delegate to thin helpers that, in Phase 1, are essentially noops or -JSON reads. Phase 4 will move the substance of those methods into -`sqlite/domain.rs` as real functions. - -### Registry: embedding_model table (-> `sqlite/registry.rs`) - -| Item | Lines | Destination | Notes | -|------|-------|-------------|-------| -| `fn enforce_model` | 6203-6272 | `sqlite/registry.rs` | private helper used by trait `insert` and `update`. Bump to `pub(super)` | - -The trait methods `registered_model` and `register_model` themselves -live in `sqlite/trait_impl.rs`. Phase 2's `postgres/registry.rs` will -mirror this layout. - -### Intentions, Insights, Memory States, Consolidation History, Dream History, Backup (-> `sqlite/mod.rs`) - -These were tacked onto `SqliteMemoryStore` over time as the cognitive -modules needed somewhere to persist their state. They are not part of the -trait surface, they are not naturally grouped with crud/search/scheduling, -and they are each fairly small and self-contained. They live in `mod.rs` -under labelled sections (one big impl block can carry them) rather than -inventing extra files like `intentions.rs` and `insights.rs`. Postgres -will mirror this once Phase 5 picks up the work; for now they have no -peer. - -Rationale: every one of these methods writes to a single table, the -bodies are short, and grouping them next to the constructor preserves -"open `mod.rs` to see the whole struct" as the navigation default. - -| Item | Lines | Destination | Notes | -|------|-------|-------------|-------| -| `IntentionRecord` struct | 3747-3766 | `sqlite/mod.rs` | re-exported through `storage/mod.rs` | -| `InsightRecord` struct + `Default` | 3767-3797 | `sqlite/mod.rs` | re-exported | -| `ConnectionRecord` struct | 3799-3809 | `sqlite/mod.rs` | re-exported; consumed by `graph.rs` | -| `MemoryStateRecord` struct | 3811-3821 | `sqlite/mod.rs` | | -| `StateTransitionRecord` struct | 3823-3833 | `sqlite/mod.rs` | re-exported | -| `ConsolidationHistoryRecord` struct | 3835-3846 | `sqlite/mod.rs` | | -| `DreamHistoryRecord` struct | 3848-3866 | `sqlite/mod.rs` | re-exported | -| `pub fn save_intention` etc. (intention block) | 3874-4058 | `sqlite/mod.rs` | one impl block, in-section labelled | -| `fn row_to_intention` | 4023-4058 | `sqlite/mod.rs` | private | -| insights block (`save_insight`, `get_insights`, etc.) | 4065-4174 | `sqlite/mod.rs` | | -| `fn row_to_insight` | 4153-4173 | `sqlite/mod.rs` | private | -| memory-state block | 4306-4459 | `sqlite/mod.rs` | | -| `fn row_to_memory_state` | 4431-4459 | `sqlite/mod.rs` | private | -| consolidation-history block | 4465-4540 | `sqlite/mod.rs` | | -| dream-history block | 4546-4638 | `sqlite/mod.rs` | | -| `pub fn count_memories_since` | 4639-4651 | `sqlite/mod.rs` | | -| `fn scan_last_backup_timestamp` | 4652-4682 | `sqlite/mod.rs` | | -| `pub fn last_backup_timestamp` | 4683-4688 | `sqlite/mod.rs` | | -| `pub fn get_last_backup_timestamp` (associated) | 4689-4696 | `sqlite/mod.rs` | | -| `pub fn backup_to` | 5749-5774 | `sqlite/mod.rs` | sqlite VACUUM INTO; called by backup tool | - -### Portable export/import/sync (-> `sqlite/portable_sync.rs`) - -This is the second-largest destination after `scheduling.rs` and the most -self-contained. - -| Item | Lines | Destination | Notes | -|------|-------|-------------|-------| -| `pub fn export_portable_archive` | 4699-4755 | `sqlite/portable_sync.rs` | | -| `pub fn export_portable_archive_to_path` | 4756-4806 | `sqlite/portable_sync.rs` | | -| `pub fn import_portable_archive` | 4807-4978 | `sqlite/portable_sync.rs` | | -| `pub fn import_portable_archive_from_path` | 4979-4996 | `sqlite/portable_sync.rs` | | -| `pub fn sync_portable_archive` (generic over backend) | 4997-5025 | `sqlite/portable_sync.rs` | | -| `pub fn sync_portable_archive_file` | 5026-5030 | `sqlite/portable_sync.rs` | | -| `fn merge_portable_table` | 5031-5073 | `sqlite/portable_sync.rs` | | -| `fn merge_knowledge_nodes` | 5074-5126 | `sqlite/portable_sync.rs` | | -| `fn merge_sync_tombstones` | 5127-5204 | `sqlite/portable_sync.rs` | | -| `fn merge_deletion_tombstones` | 5205-5245 | `sqlite/portable_sync.rs` | | -| `fn merge_keyed_table` | 5246-5281 | `sqlite/portable_sync.rs` | | -| `fn row_references_locally_newer_node` | 5282-5302 | `sqlite/portable_sync.rs` | | -| `fn merge_append_only_table` | 5303-5338 | `sqlite/portable_sync.rs` | | -| `fn parent_rows_exist` | 5339-5370 | `sqlite/portable_sync.rs` | | -| `fn insert_or_replace_row` | 5371-5386 | `sqlite/portable_sync.rs` | | -| `fn merge_key_columns` | 5387-5398 | `sqlite/portable_sync.rs` | | -| `fn upsert_row_with_columns` | 5399-5446 | `sqlite/portable_sync.rs` | | -| `fn insert_row_with_columns` | 5447-5469 | `sqlite/portable_sync.rs` | | -| `fn merge_row_exists` | 5470-5487 | `sqlite/portable_sync.rs` | | -| `fn row_exists_by_values` | 5488-5507 | `sqlite/portable_sync.rs` | | -| `fn row_values_for_columns` | 5508-5528 | `sqlite/portable_sync.rs` | | -| `fn portable_value` | 5529-5540 | `sqlite/portable_sync.rs` | | -| `fn portable_text` | 5541-5551 | `sqlite/portable_sync.rs` | | -| `fn portable_timestamp` | 5552-5559 | `sqlite/portable_sync.rs` | | -| `fn parse_rfc3339_opt` | 5560-5565 | `sqlite/portable_sync.rs` | | -| `fn tombstone_timestamp` | 5566-5580 | `sqlite/portable_sync.rs` | | -| `fn current_schema_version` | 5581-5589 | `sqlite/portable_sync.rs` | static helper | -| `fn ensure_portable_import_target_empty` | 5590-5604 | `sqlite/portable_sync.rs` | static helper | -| `fn table_exists` | 5605-5613 | `sqlite/portable_sync.rs` | static helper | -| `fn table_row_count` | 5614-5618 | `sqlite/portable_sync.rs` | static helper | -| `fn table_columns` | 5619-5630 | `sqlite/portable_sync.rs` | static helper | -| `fn portable_value_from_ref` | 5631-5646 | `sqlite/portable_sync.rs` | static helper | -| `fn quote_ident` | 5647-5651 | `sqlite/portable_sync.rs` | static helper | - -### Trait helpers and trait impl (-> `sqlite/trait_impl.rs`) - -| Item | Lines | Destination | Notes | -|------|-------|-------------|-------| -| `fn node_to_record` | 6142-6164 | `sqlite/trait_impl.rs` | associated fn used only by trait body; co-locate | -| `impl LocalMemoryStore for SqliteMemoryStore` block | 6274-7110 | `sqlite/trait_impl.rs` | full trait impl; attribute changes from `#[async_trait::async_trait]` to whatever 0001a settles on (`#[trait_variant::make(...)]` is on the trait declaration; the impl block carries no attribute under trait_variant) | - -### Tests - -The current `#[cfg(test)] mod tests` block at lines 7112-8713 contains -**two** distinct test families: - -1. **Native API tests** (7120-8198): unit tests against the legacy - `pub fn` surface (`test_ingest_and_get`, `test_search`, `test_review`, - `test_delete`, `test_dream_history_save_and_get_last`, - `test_portable_archive_exact_round_trip`, `test_keyword_search_*`, - `test_concrete_search_*`, `test_purge_*`, etc.). -2. **Trait round-trip tests** (8200-8712, after the - `// ===== Phase 1: LocalMemoryStore trait round-trip tests =====` - banner): `trait_init_is_idempotent`, `trait_register_model_*`, - `trait_insert_*`, `trait_get_*`, `trait_update_*`, `trait_delete_*`, - `trait_fts_search_*`, `trait_hybrid_search_*`, - `trait_scheduling_*`, `trait_add_edge_*`, `trait_get_edges_*`, - `trait_remove_edge_*`, `trait_get_neighbors_*`, `trait_list_domains_*`, - `trait_upsert_*`, `trait_classify_*`, `trait_count_*`, - `trait_get_stats_*`, `trait_vacuum_*`, - `trait_insert_refuses_dimension_mismatch`. - -See the Test Relocation section below for the resolution. - ---- - -## Visibility Changes - -The split moves items into sibling files inside one module. Helpers that -were `fn ...` (i.e. crate-private but file-private under the current -layout, since the file *is* the module) need their visibility lifted -just enough that sibling files can call them. The principle is: smallest -bump that makes the call site compile. - -`pub(super)` is sufficient for everything below; nothing needs -`pub(crate)`. The trait `LocalMemoryStore` exposure does not change -- -sub-modules call `self.method(...)` on `SqliteMemoryStore`, which -resolves through the impl blocks defined in their own files; visibility -is automatic at impl-block scope. - -Items that need a visibility bump (currently private fn, become -`pub(super) fn`): - -- `parse_timestamp` (1012): called by `row_to_node` and by intention / - insight row mappers. -- `row_to_node` (1028): called by `crud.rs`, `search.rs`, - `scheduling.rs`. Static associated fn. -- `read_domain_columns` (6167): called by `trait_impl.rs`. -- `enforce_model` (6203): called by `trait_impl.rs`. -- `embedding_model_matches_active` (5652): currently called by - `hybrid_search_filtered`; tests also reference it. Has to remain - `pub(super) fn` and be `pub` only if the existing test names reach it - through a re-export. (See Test Relocation.) -- `embedding_model_supports_matryoshka` (5672): private; only callers in - `search.rs` after the move; stays `fn` (no bump needed). -- `embedding_vector_for_active_model` (5678): same as the matches - function -- a test references it. Bump to `pub(super)`. -- `active_embedding_model_like_pattern` (5698): private; only used by - search; stays `fn`. -- `generate_embedding_for_node` (952): currently called by `ingest` and - `update_node_content`. Both move to `crud.rs`; stays `fn`. -- `get_query_embedding` (2368): only used inside `search.rs`; stays `fn`. -- `keyword_search_with_scores` (2636): only used inside `search.rs`; - stays `fn`. -- `semantic_search_raw` (2727): only used inside `search.rs`; stays `fn`. -- `embedding_regeneration_candidates` (2820): used by - `generate_embeddings`; both move to `search.rs`; stays `fn`. The - existing test (line 7167) references it through `storage.method()`, - which will continue to work because the test file can move with it. -- `log_access` (1378): private to `scheduling.rs`; stays `fn`. -- All the `auto_dedup_consolidation` / `compute_act_r_activations` / - `prune_access_log` / `optimize_w20_if_ready` / - `generate_missing_embeddings` helpers (3408-3740): private to - `scheduling.rs`; stays `fn`. -- `row_to_intention` / `row_to_insight` / `row_to_memory_state` / - `row_to_connection`: all stay private in their destination file (only - one caller each). -- All `merge_*` / `portable_*` / `parse_rfc3339_opt` / `quote_ident`: - private to `portable_sync.rs`; stays `fn`. -- `node_exists` (1988): private to `crud.rs`; stays `fn`. -- `record_sync_tombstone` (1997): private to `crud.rs`; stays `fn`. -- `get_fsrs_w20` (3096): private to `scheduling.rs`; stays `fn`. - -Items already `pub fn` (or `pub(crate) fn`) stay as they are -- no -visibility regression. - -Field visibility on `SqliteMemoryStore` itself: currently all fields are -private. The sub-modules access them via `self.field`. Because impl -blocks for `SqliteMemoryStore` are written in sibling files of the same -module, `self.field` reaches private fields without a visibility bump. -**No field visibility changes are required.** Confirm this during the -first motion commit; if Rust disagrees, mark the relevant fields -`pub(super)` and document in the commit message. - ---- - -## Public Re-exports - -`crates/vestige-core/src/storage/mod.rs` currently exports: - -```rust -mod memory_store; -mod migrations; -mod portable; -mod sqlite; - -pub use memory_store::{...}; -pub use migrations::MIGRATIONS; -pub use portable::{...}; -pub use sqlite::{ - ConnectionRecord, ConsolidationHistoryRecord, DreamHistoryRecord, FilePortableSyncBackend, - InsightRecord, IntentionRecord, PortableSyncBackend, PortableSyncReport, Result, - SmartIngestResult, SqliteMemoryStore, StateTransitionRecord, StorageError, -}; - -pub type Storage = SqliteMemoryStore; -``` - -After the split, `mod sqlite;` resolves to the new directory module -(`storage/sqlite/mod.rs`). The `pub use sqlite::{...}` block resolves -against the items re-exported by `storage/sqlite/mod.rs`. - -`storage/sqlite/mod.rs` therefore needs the same names visible at its -top level. Add at the end of `mod.rs`: - -```rust -mod crud; -mod search; -mod scheduling; -mod graph; -mod domain; -mod registry; -mod portable_sync; -mod trait_impl; - -pub use portable_sync::{FilePortableSyncBackend, PortableSyncBackend, PortableSyncReport}; -// SqliteMemoryStore, StorageError, Result, SmartIngestResult, IntentionRecord, -// InsightRecord, ConnectionRecord, StateTransitionRecord, -// ConsolidationHistoryRecord, DreamHistoryRecord are defined in mod.rs itself, -// so they are already in scope and do not need a re-export. -``` - -The `crates/vestige-core/src/storage/mod.rs` file does not change. The -`pub type Storage = SqliteMemoryStore;` alias keeps working. - -If `cargo build` complains that `storage/mod.rs` cannot resolve a name -in its `pub use sqlite::{...}` block, the fix is to add the missing name -to `sqlite/mod.rs`'s re-export tail; no change to `storage/mod.rs`. - ---- - -## Test Relocation - -Two test families, two destinations. - -**Native API tests** (current lines 7120-8198) cover the legacy `pub fn` -surface. They live close to their subject: - -- Tests that touch the constructor, common helpers, and shared setup - (`create_test_storage`, `create_test_storage_at`, - `test_storage_creation`, `test_get_last_backup_timestamp_no_panic`) - move to `sqlite/mod.rs` in a `#[cfg(test)] mod tests` block. -- `test_ingest_and_get`, `test_delete`, - `test_purge_scrubs_insight_json_orphans_children_and_writes_tombstone` - go to `sqlite/crud.rs` as a `#[cfg(test)] mod tests` block. -- `test_search`, `test_keyword_search_with_include_types`, - `test_keyword_search_with_exclude_types`, - `test_include_types_takes_precedence_over_exclude`, - `test_type_filter_with_no_matches_returns_empty`, - `test_hybrid_search_backward_compat`, - `test_concrete_search_literal_identifier_lands_first`, - `test_embedding_model_family_matching`, - `test_embedding_regeneration_candidates_include_entire_mismatched_corpus` - go to `sqlite/search.rs`. -- `test_review` goes to `sqlite/scheduling.rs`. -- `test_dream_history_save_and_get_last`, `test_dream_history_empty`, - `test_count_memories_since` go to `sqlite/mod.rs` (history tables live - there). -- All `test_portable_*` go to `sqlite/portable_sync.rs`. -- `test_file_portable_sync_round_trips_between_devices` goes to - `sqlite/portable_sync.rs`. - -**Trait round-trip tests** (current lines 8200-8712) test the -`LocalMemoryStore` trait impl. Two viable layouts: - -A. Co-locate with the impl in `sqlite/trait_impl.rs` (one big - `#[cfg(test)] mod trait_tests`). -B. Keep them as a single `tests.rs` file in the sqlite directory. - -**Decision: A.** Co-locate. The trait round-trip tests are explicitly -testing the `impl LocalMemoryStore for SqliteMemoryStore` block; -co-location means a reader who edits the trait impl sees its tests in -the same file. Option B would mean two places to look every time a -trait method changes shape. For an 8K-line collapse the tradeoff -favours co-location. - -Concretely: `sqlite/trait_impl.rs` ends with a -`#[cfg(test)] mod trait_tests { ... }` block that contains all 30+ -`trait_*` tests, plus the shared `make_record`, `rt`, and small helpers -defined inside the current test mod for trait tests (lines 8208-8226). - ---- - -## Commit Sequence - -Each commit moves one logical group. After each commit: - -``` -cargo build -p vestige-core -cargo test -p vestige-core -cargo clippy -p vestige-core -- -D warnings -``` - -must pass. Order is chosen so that each move is small, the next move -does not depend on the previous having grown surprising visibility, and -the largest / riskiest move (the trait impl, with the new -trait_variant attribute) lands last. - -| # | Commit | What moves | Tests touched | -|---|--------|-----------|----------------| -| 1 | `refactor(sqlite): scaffold sqlite/ directory` | Convert `sqlite.rs` -> `sqlite/mod.rs` verbatim (rename + create empty sibling files `crud.rs`, `search.rs`, `scheduling.rs`, `graph.rs`, `domain.rs`, `registry.rs`, `portable_sync.rs`, `trait_impl.rs` each with `use super::*;`). At this point `mod.rs` declares the new modules but they are empty. | None move. Build proves the rename works. | -| 2 | `refactor(sqlite): split out portable sync` | Move all `merge_*`, `portable_*`, `export_*`, `import_*`, `sync_*` items + `MergeWrite`, `PortableSyncBackend`, `FilePortableSyncBackend`, `PortableSyncReport`, `PortableMergeState`, `PORTABLE_TABLES`, `PORTABLE_USER_DATA_TABLES`, helper statics into `sqlite/portable_sync.rs`. Add `pub use portable_sync::{...}` in `mod.rs` for the public types. | `test_portable_*` and `test_file_portable_sync_round_trips_between_devices` move too. | -| 3 | `refactor(sqlite): split out graph / connections` | Move `save_connection`, `get_connections_for_memory`, `get_all_connections`, `strengthen_connection`, `apply_connection_decay`, `prune_weak_connections`, `row_to_connection`, `get_most_connected_memory`, `get_memory_subgraph` to `sqlite/graph.rs`. | None move (no native graph tests; trait edge tests still in trait_tests). | -| 4 | `refactor(sqlite): split out scheduling / fsrs / consolidation` | Move all items listed in the Scheduling row to `sqlite/scheduling.rs`. | `test_review` moves. | -| 5 | `refactor(sqlite): split out search / fts / semantic / hybrid` | Move all items listed in the Search row to `sqlite/search.rs`. Add `pub(super)` to the four `embedding_model_*` helpers that tests reference. | `test_search`, `test_keyword_search_*`, `test_include_types_*`, `test_type_filter_*`, `test_hybrid_search_*`, `test_concrete_search_*`, `test_embedding_model_family_matching`, `test_embedding_regeneration_candidates_include_entire_mismatched_corpus` move. | -| 6 | `refactor(sqlite): split out crud / ingest / get / update / delete / purge` | Move `ingest`, `smart_ingest`, `get_node`, `update_node_content`, `delete_node`, `purge_node`, `get_all_nodes`, `get_nodes_by_type_and_tag`, `node_exists`, `record_sync_tombstone`, `generate_embedding_for_node`, `get_node_embedding`, `get_all_embeddings`, `PurgeReport` to `sqlite/crud.rs`. Bump `row_to_node` and `parse_timestamp` to `pub(super) fn` in `mod.rs`. | `test_ingest_and_get`, `test_delete`, `test_purge_scrubs_insight_json_orphans_children_and_writes_tombstone` move. | -| 7 | `refactor(sqlite): split out registry helper` | Move `enforce_model` to `sqlite/registry.rs`, bumped to `pub(super)`. | None move. | -| 8 | `refactor(sqlite): split out domain helper` | Move `read_domain_columns` to `sqlite/domain.rs`, bumped to `pub(super)`. | None move. | -| 9 | `refactor(sqlite): split out trait impl + tests` | Move `node_to_record` and the full `impl LocalMemoryStore for SqliteMemoryStore` block to `sqlite/trait_impl.rs`. Move the entire trait round-trip test module (lines 8200-8712, including `make_record` and `rt` helpers) to a `#[cfg(test)] mod trait_tests` block at the bottom of `trait_impl.rs`. This is the commit where the trait_variant attribute (from sub-plan 0001a) is observed: the impl block on `SqliteMemoryStore` uses whatever syntax the rewritten trait expects (no `#[async_trait::async_trait]`). | All `trait_*` tests move. | - -Commit 1 is the only commit that creates new files; the rest move -existing code into them. Reviewers can bisect through this list to -find any silent-semantic change. - ---- - -## Verification - -Run after every commit. All three must pass before pushing: - -``` -cargo build -p vestige-core -cargo test -p vestige-core -cargo clippy -p vestige-core -- -D warnings -``` - -The Phase 1 amendment branch must also build with the no-default-features -configuration that the release binary uses for the alternative feature -set: - -``` -cargo build -p vestige-core --no-default-features -cargo test -p vestige-core --no-default-features -``` - -Some of the methods being moved (`get_node_embedding`, `is_embedding_ready`, -`init_embeddings`, the feature-on/feature-off `hybrid_search` pair) have -two definitions guarded by feature flags. The split must preserve both -copies in the same destination file with their existing `#[cfg(...)]` -attributes; the no-default-features build confirms. - -After the last commit, run the workspace-wide check that Phase 1 promised: - -``` -cargo build --workspace -cargo test --workspace -``` - -This catches downstream consumers (`vestige-mcp`, `vestige`, -`vestige-restore`) that might depend on a specific module path (they -should not -- they import from `crate::storage::SqliteMemoryStore` and -the re-exports in `storage/mod.rs` -- but the workspace build is the -authoritative confirmation). - ---- - -## Acceptance Criteria - -1. `crates/vestige-core/src/storage/sqlite.rs` no longer exists. In its - place is `crates/vestige-core/src/storage/sqlite/` with the eight - files listed in the Target Layout section, each below 2000 lines. -2. `crates/vestige-core/src/storage/mod.rs` is unchanged (or - functionally unchanged -- the `pub use sqlite::{...}` block contains - the same identifiers in the same order). -3. Every cognitive module and binary in the workspace - (`vestige-core`, `vestige-mcp`, `vestige`, `vestige-restore`) - compiles with no source edits other than the ones in - `crates/vestige-core/src/storage/sqlite/`. -4. `cargo build -p vestige-core`, - `cargo test -p vestige-core`, - `cargo clippy -p vestige-core -- -D warnings`, - `cargo build -p vestige-core --no-default-features`, and - `cargo test -p vestige-core --no-default-features` all pass at the - end of every commit in the sequence (bisectability). -5. `cargo test --workspace` matches the Phase 1 baseline test count - (758 tests, of which 352 are in `vestige-core`). No new tests are - added by this sub-plan; no existing test is renamed or deleted. -6. The on-disk SQLite schema is unchanged. A live database created on - the pre-split build opens cleanly on the post-split build and round - trips a memory. -7. `git log --follow` works for at least one method in each destination - file (i.e. `git mv` was used where the line range constitutes most - of the file content of the destination, otherwise a `git log -p` - on the new file shows the history before the rename through the - block-move detection that recent `git log` versions support). -8. No public symbol disappears from `crate::storage::*`. A reviewer can - verify with: - ``` - cargo doc -p vestige-core --no-deps - ``` - before and after the split, and `diff` the generated - `target/doc/vestige_core/storage/index.html` lists. - ---- - -## Non-Goals (explicit) - -- No public API change. The trait surface (`LocalMemoryStore`, - `MemoryStore`), the legacy `pub fn` surface on `SqliteMemoryStore`, - the re-exports through `storage/mod.rs`, and the `pub type Storage = - SqliteMemoryStore;` alias are all preserved. -- No behavioural change. No SQL is rewritten, no FSRS parameter is - retuned, no embedding model is touched, no migration is added. -- No new tests. Tests move with their subject; no new tests are - written. -- No clippy fix-ups that pre-date this sub-plan. If `cargo clippy - -- -D warnings` was passing before the split, it must continue to - pass; if it was not passing, the failures stay where they are and - are addressed in a separate commit (out of scope here). -- No removal of the `pub type Storage = SqliteMemoryStore;` BC alias. - That happens at the end of Phase 4 per ADR 0001. -- No reorganisation of `storage/memory_store.rs`, - `storage/migrations.rs`, or `storage/portable.rs`. Those files are - out of scope; the split is private to `storage/sqlite/`. - ---- - -## Risks and Mitigations - -| Risk | Mitigation | -|------|------------| -| Silent semantic change introduced by a motion commit | Per-commit `cargo test -p vestige-core` keeps the bisect window to a single commit. Reviewer bisects with `cargo test -p vestige-core` as the witness. | -| Sibling-file `self.field` accesses fail because Rust enforces module visibility on tuple-struct or named fields | Confirmed: associated impl blocks in sibling files of the same `mod sqlite` reach private fields. If the compiler disagrees, bump the affected fields to `pub(super)` in `mod.rs` and note the bump in the commit message. | -| Test-only helpers (`create_test_storage`, `make_record`, `rt`) get duplicated across test modules | Lift them once into a `#[cfg(test)] mod test_support { ... }` sub-module in `sqlite/mod.rs` and re-export with `pub(super) use`. Do this in commit 1 (scaffold), not later. | -| `#[cfg(all(feature = "embeddings", feature = "vector-search"))]` items end up in `mod.rs` where they pollute the shared layer | Audit during commit 5 (search split); items behind both feature flags belong in `search.rs`. The `query_cache` field stays in `mod.rs` because the struct definition is there; the field declaration is feature-gated and that gate moves with the struct as-is. | -| `git log --follow` blame chains break on the moved methods | Use `git mv sqlite.rs sqlite/mod.rs` in commit 1 so commit 1 looks like a rename (`git log --follow` keeps working). Subsequent commits are content moves inside the module; modern `git log --follow -M -C` heuristics still trace the lines. Reviewers who need pristine blame should bisect to before commit 1. | -| Sub-plan 0001a (trait rewrite) has not landed when this work starts | Block: do not start commits 1-9 until 0001a is on the same branch (`feat/storage-trait-phase1`) and tests pass. `trait_impl.rs` lands the new attribute in commit 9; if 0001a is not in, commit 9 fails. | - ---- - -## Self-Contained Brief (for /goal) - -A fresh Claude Code session can execute this sub-plan by: - -1. Reading this file end to end. -2. Reading `crates/vestige-core/src/storage/sqlite.rs` (the file to be - split) in full, using line ranges from the Mapping Table to confirm - the current shape matches the brief. -3. Reading `crates/vestige-core/src/storage/mod.rs` (the re-export - surface that must continue to work). -4. Reading `crates/vestige-core/src/storage/memory_store.rs` (the - trait surface that `trait_impl.rs` implements). -5. Confirming sub-plan 0001a has landed on the current branch by - checking that `memory_store.rs` no longer carries - `#[async_trait::async_trait]` on the trait declaration. -6. Working through the Commit Sequence in order, running the - Verification commands after each commit. - -The session does not need to read ADR 0002 or the master Phase 2 plan -to do this work. The split is purely mechanical relative to the -mapping table above. diff --git a/docs/plans/0001c-async-trait-sunset.md b/docs/plans/0001c-async-trait-sunset.md deleted file mode 100644 index f9d8938..0000000 --- a/docs/plans/0001c-async-trait-sunset.md +++ /dev/null @@ -1,847 +0,0 @@ -# Sub-Plan 0001c: Sunset the `async-trait` crate dependency - -**Status**: Draft -**Branch**: `feat/storage-trait-phase1` (Phase 1 amendment, PR A) -**Depends on**: -- `0001a-trait-rewrite.md` (rewrites `MemoryStore` / `LocalMemoryStore` and - the SQLite impl; lands first) -- `0001b-sqlite-split.md` (moves `sqlite.rs` into `sqlite/`; lands second) - -**Related**: `docs/adr/0002-phase-2-execution.md` (decision D1 closing line: -"async-trait dependency stays in Cargo.toml only if other code uses it; -otherwise removed"). This sub-plan operationalises the removal. - ---- - -## Context - -This is the third and final Phase 1 amendment sub-plan. Sub-plan `0001a` -rewrote `MemoryStore` / `LocalMemoryStore` using -`#[trait_variant::make(MemoryStore: Send)]` and dropped the -`#[async_trait::async_trait]` attribute from the SQLite impl block. -Sub-plan `0001b` then split `sqlite.rs` into a `sqlite/` directory; the -trait impl now lives in `sqlite/trait_impl.rs`. After `0001a` lands, the -only remaining `async_trait` usage in the workspace is the embedder pair -(`embedder/mod.rs` declares the trait; `embedder/fastembed.rs` implements -it). This sub-plan rewrites those two files following the exact pattern -from `0001a`, then removes `async-trait = "0.1"` from -`crates/vestige-core/Cargo.toml`. End state: zero `async_trait` references -anywhere under `crates/`, zero direct dependency on the `async-trait` -crate, workspace tests and clippy green. - -The rewrite is mechanically tiny -- one trait declaration, one impl block, -one Cargo.toml line -- but it is gated behind `0001a` and `0001b` so the -trait-rewrite pattern is already settled and so the SQLite trait impl -attribute has already been dropped. Doing the embedder rewrite without -that pre-work would leave the `async-trait` dep behind for the SQLite -side and force the Cargo.toml deletion into a separate commit later. - ---- - -## Scope - -### In scope - -- Rewrite `LocalEmbedder` declaration in - `crates/vestige-core/src/embedder/mod.rs` to use - `#[trait_variant::make(Embedder: Send)] pub trait LocalEmbedder`. -- Delete the `pub use LocalEmbedder as Embedder;` alias from the same file. - The `Embedder` symbol becomes the trait that `trait_variant::make` emits - at the same module path, so the existing - `pub use embedder::{Embedder, ..., LocalEmbedder};` line in - `crates/vestige-core/src/lib.rs:167` keeps working untouched. -- Drop the `#[async_trait::async_trait]` attribute from the - `FastembedEmbedder` impl block in - `crates/vestige-core/src/embedder/fastembed.rs`. -- Update doc comments on the trait declaration to describe - `trait_variant` rather than `async_trait`. -- Remove `async-trait = "0.1"` from - `crates/vestige-core/Cargo.toml` (line 119 area). Use - `cargo rm async-trait` from inside the crate directory. -- Verify with `grep -rn "async_trait" crates/` returning zero hits. - -### Out of scope - -- Any change to the `MemoryStore` trait or `SqliteMemoryStore` impl; - those were handled by `0001a`. -- Any file moves in `embedder/` (no parallel of `0001b` is required; - `embedder/` already has the `mod.rs` + `fastembed.rs` shape). -- Touching `crates/vestige-mcp/` or any cognitive module. None of them - hold `Arc` or `Box` in production. -- Renaming the `Embedder` / `LocalEmbedder` symbols or changing the - re-exports in `crates/vestige-core/src/lib.rs`. - ---- - -## Prerequisites - -### State assumed at start - -- `0001a` is merged onto the branch. After `0001a`: - - `crates/vestige-core/src/storage/memory_store.rs` declares - `#[trait_variant::make(MemoryStore: Send)] pub trait LocalMemoryStore`. - - The SQLite impl block has no `#[async_trait::async_trait]` attribute. - - `grep -rn async_trait crates/` returns exactly three hits, all in - `crates/vestige-core/src/embedder/` (two in `mod.rs`, one in - `fastembed.rs`), and one Cargo.toml hit. -- `0001b` is merged onto the branch. After `0001b`: - - `crates/vestige-core/src/storage/sqlite.rs` no longer exists as a - single file; the impl lives in `crates/vestige-core/src/storage/sqlite/trait_impl.rs`. - - The embedder files are untouched. - -### Required crates - -| Crate | Version | Action | -|----------------|---------|-----------------------------------------------------------------| -| `trait-variant`| `0.1` | Already declared (line 117 of Cargo.toml). Verify present. | -| `async-trait` | `0.1` | Remove. Only the two embedder files still use it after `0001a`. | - -### Workspace-wide audit before starting - -Run from `/home/delandtj/prppl/vestige-phase2/` (or the equivalent -worktree where this sub-plan executes): - -```bash -grep -rn "async_trait\|async-trait" crates/ tests/ -``` - -Expected hits before this sub-plan starts (after `0001a` + `0001b`): - -``` -crates/vestige-core/Cargo.toml:119:async-trait = "0.1" -crates/vestige-core/src/embedder/mod.rs:24:/// `#[async_trait::async_trait]` makes every `async fn` return a -crates/vestige-core/src/embedder/mod.rs:27:#[async_trait::async_trait] -crates/vestige-core/src/embedder/mod.rs:56:/// Both names refer to the same `async_trait`-annotated trait. -crates/vestige-core/src/embedder/fastembed.rs:44:#[async_trait::async_trait] -``` - -Five hits across two source files and one Cargo.toml. After this sub-plan, -the same grep must return zero hits. - -```bash -grep -rn "async-trait\|async_trait" --include="Cargo.toml" crates/ -``` - -Expected: exactly one hit (`crates/vestige-core/Cargo.toml:119`). No other -workspace crate declares `async-trait` as a direct dependency. This is -the precondition that lets us delete the line cleanly. - ---- - -## Files Touched - -### Trait declaration (vestige-core) - -| File | Lines (approx) | Change | -|-------------------------------------------------|----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `crates/vestige-core/src/embedder/mod.rs` | 21-57 | Replace `#[async_trait::async_trait] pub trait LocalEmbedder: Send + Sync + 'static` with `#[trait_variant::make(Embedder: Send)] pub trait LocalEmbedder: Sync + 'static`. Delete the `pub use LocalEmbedder as Embedder;` alias on line 57. Update doc comments (lines 21-26, 55-56). | - -### Impl block (vestige-core) - -| File | Lines (approx) | Change | -|-------------------------------------------------|----------------|--------------------------------------------------------------------------------------------------------------| -| `crates/vestige-core/src/embedder/fastembed.rs` | 44 | Delete the `#[async_trait::async_trait]` attribute. Keep the `impl LocalEmbedder for FastembedEmbedder { ... }` body verbatim. No `Box::pin`, no `'async_trait` lifetimes, no manual `Pin>`. | - -### Other Embedder impls - -None. `grep -rn "impl.*LocalEmbedder\|impl.*Embedder for" crates/ tests/` -returns exactly one production hit: -`crates/vestige-core/src/embedder/fastembed.rs:45: impl LocalEmbedder for FastembedEmbedder`. -There is no test mock implementing `Embedder` in the test tree (the only -test that touches the trait, `tests/phase_1/embedder_trait.rs`, uses the -concrete `FastembedEmbedder` boxed as `Box`). - -### Call sites (production) - -Verified by: - -```bash -grep -rn "dyn Embedder\|dyn LocalEmbedder" crates/ tests/ --include="*.rs" -grep -rn "Box\|Arc" crates/ tests/ --include="*.rs" -grep -rn "use.*Embedder" crates/ tests/ --include="*.rs" -``` - -Production call sites that may need verification (and the expected change -for each, even though we have already verified that none need an edit): - -| File | Use | Required change | -|------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|-----------------| -| `crates/vestige-core/src/lib.rs:167` | `pub use embedder::{Embedder, EmbedderError, EmbedderResult, FastembedEmbedder, LocalEmbedder};` | None. Both names still exist at `crate::embedder::*` after the rewrite; `Embedder` is now the `trait_variant`-generated trait, `LocalEmbedder` is the source-of-truth trait. The re-export keeps resolving. | -| `crates/vestige-core/src/embedder/fastembed.rs:7` | `use super::{EmbedderError, EmbedderResult, LocalEmbedder};` | None. `LocalEmbedder` is still the source-of-truth trait name. | -| `crates/vestige-core/src/embedder/mod.rs:5` | `pub use fastembed::FastembedEmbedder;` | None. Concrete type, untouched. | -| `crates/vestige-mcp/src/**` | No file imports `Embedder` or `LocalEmbedder` by name; none hold `Arc` or `Box`. | None. Verified by grep returning empty for `dyn Embedder` and `dyn LocalEmbedder` under `crates/vestige-mcp/`. | -| Cognitive modules under `crates/vestige-core/src/advanced/` and `crates/vestige-core/src/neuroscience/` | No file imports `Embedder` or `LocalEmbedder` by name. `advanced/adaptive_embedding.rs` defines its own unrelated `AdaptiveEmbedder` struct. | None. The name collision is purely surface-level; the two types live in different modules and never resolve to each other. | -| `crates/vestige-core/src/embeddings/**` | No file imports `Embedder` or `LocalEmbedder`. The `EmbeddingService` struct is what `FastembedEmbedder` wraps internally. | None. | - -The production audit returns zero files that need editing. - -### Call sites (tests) - -| File | Lines | Use | Required change | -|------------------------------------------------------------|-------|--------------------------------------------------------------------|-----------------| -| `tests/phase_1/embedder_trait.rs` | 3, 19 | `use vestige_core::embedder::{Embedder, FastembedEmbedder};`
                    `let e: Box = Box::new(FastembedEmbedder::new());` | None. `Embedder` is the `trait_variant`-generated Send variant; `Box` keeps compiling. `FastembedEmbedder` implements `LocalEmbedder`; the blanket `impl Embedder for T` that `trait_variant::make` emits gives the boxing for free. | - -The test audit returns zero files that need editing. - -### Cargo dependency cleanup - -| File | Lines | Change | -|-------------------------------------|-----------|-----------------------------------------------------------------------------------------------------| -| `crates/vestige-core/Cargo.toml` | 119 | Remove `async-trait = "0.1"`. Run `cargo rm async-trait` from inside `crates/vestige-core/` so `Cargo.lock` updates atomically with the manifest. | - -### Documentation - -| File | Change | -|---------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------| -| `crates/vestige-core/src/embedder/mod.rs` | Rewrite the trait-level doc comment (lines 21-26) and the `pub use` doc comment (lines 55-56) to describe `trait_variant`, not `async_trait`. See "Trait declaration rewrite" below for the exact new text. | -| `CLAUDE.md` | No change. The repo-level architecture notes do not name the trait attribute. | - ---- - -## Trait Declaration Rewrite - -### Before (state after `0001a` and `0001b` land) - -`crates/vestige-core/src/embedder/mod.rs:1-58`: - -```rust -//! Text-to-vector encoding trait. Pluggable per-install. - -mod fastembed; - -pub use fastembed::FastembedEmbedder; - -/// Error returned by every `Embedder` method. -#[non_exhaustive] -#[derive(Debug, thiserror::Error)] -pub enum EmbedderError { - #[error("embedder initialization failed: {0}")] - Init(String), - #[error("embedding generation failed: {0}")] - EmbedFailed(String), - #[error("invalid input: {0}")] - InvalidInput(String), -} - -pub type EmbedderResult = std::result::Result; - -/// Pluggable embedder. The storage layer NEVER calls fastembed directly; -/// callers compute vectors via this trait and pass them into `MemoryStore`. -/// -/// `#[async_trait::async_trait]` makes every `async fn` return a -/// `Pin>`, which is required for `Box` -/// and `Arc` to be dyn-compatible. -#[async_trait::async_trait] -pub trait LocalEmbedder: Send + Sync + 'static { - async fn embed(&self, text: &str) -> EmbedderResult>; - - fn model_name(&self) -> &str; - - fn dimension(&self) -> usize; - - /// Stable blake3 hash of (model_name || dimension || vestige-core crate version). - /// Lowercase hex, 64 chars. - /// - /// Used by `MemoryStore::register_model` to detect silent model drift - /// (e.g. a fastembed minor upgrade that changes vector output). - fn model_hash(&self) -> String; - - async fn embed_batch(&self, texts: &[&str]) -> EmbedderResult>>; - - /// Returns the `ModelSignature` describing this embedder. Convenience - /// wrapper over the three accessors above. - fn signature(&self) -> crate::storage::ModelSignature { - crate::storage::ModelSignature { - name: self.model_name().to_string(), - dimension: self.dimension(), - hash: self.model_hash(), - } - } -} - -/// Type alias: `Embedder` is the dyn-compatible, Send+Sync variant. -/// Both names refer to the same `async_trait`-annotated trait. -pub use LocalEmbedder as Embedder; -``` - -### After - -`crates/vestige-core/src/embedder/mod.rs:1-55` (approximately): - -```rust -//! Text-to-vector encoding trait. Pluggable per-install. - -mod fastembed; - -pub use fastembed::FastembedEmbedder; - -/// Error returned by every `Embedder` method. -#[non_exhaustive] -#[derive(Debug, thiserror::Error)] -pub enum EmbedderError { - #[error("embedder initialization failed: {0}")] - Init(String), - #[error("embedding generation failed: {0}")] - EmbedFailed(String), - #[error("invalid input: {0}")] - InvalidInput(String), -} - -pub type EmbedderResult = std::result::Result; - -/// Pluggable embedder. The storage layer NEVER calls fastembed directly; -/// callers compute vectors via this trait and pass them into `MemoryStore`. -/// -/// `LocalEmbedder` is the source-of-truth trait. The -/// `#[trait_variant::make(Embedder: Send)]` attribute auto-generates an -/// `Embedder` variant whose returned futures are `Send`, so -/// `Box` and `Arc` are usable on tokio/axum -/// runtimes, while `Box` remains usable on single- -/// threaded executors and thread-local backends. -/// -/// Every method is native async-fn-in-trait (stable on MSRV 1.91); no -/// per-call heap allocation, no boxed futures at the static-dispatch -/// boundary. -#[trait_variant::make(Embedder: Send)] -pub trait LocalEmbedder: Sync + 'static { - async fn embed(&self, text: &str) -> EmbedderResult>; - - fn model_name(&self) -> &str; - - fn dimension(&self) -> usize; - - /// Stable blake3 hash of (model_name || dimension || vestige-core crate version). - /// Lowercase hex, 64 chars. - /// - /// Used by `MemoryStore::register_model` to detect silent model drift - /// (e.g. a fastembed minor upgrade that changes vector output). - fn model_hash(&self) -> String; - - async fn embed_batch(&self, texts: &[&str]) -> EmbedderResult>>; - - /// Returns the `ModelSignature` describing this embedder. Convenience - /// wrapper over the three accessors above. - fn signature(&self) -> crate::storage::ModelSignature { - crate::storage::ModelSignature { - name: self.model_name().to_string(), - dimension: self.dimension(), - hash: self.model_hash(), - } - } -} -``` - -### Both halves of the macro-generated output (for reviewer clarity) - -`trait_variant::make(Embedder: Send)` expands the source-of-truth -`LocalEmbedder` declaration above into the equivalent of: - -```rust -// 1. The source-of-truth trait, exactly as written. -pub trait LocalEmbedder: Sync + 'static { - fn embed(&self, text: &str) -> impl Future>>; - fn model_name(&self) -> &str; - fn dimension(&self) -> usize; - fn model_hash(&self) -> String; - fn embed_batch(&self, texts: &[&str]) -> impl Future>>>; - fn signature(&self) -> crate::storage::ModelSignature { /* default impl unchanged */ } -} - -// 2. The generated Send variant. -pub trait Embedder: Sync + 'static { - fn embed(&self, text: &str) -> impl Future>> + Send; - fn model_name(&self) -> &str; - fn dimension(&self) -> usize; - fn model_hash(&self) -> String; - fn embed_batch(&self, texts: &[&str]) -> impl Future>>> + Send; - fn signature(&self) -> crate::storage::ModelSignature { /* default impl unchanged */ } -} - -// 3. The blanket impl that wires any LocalEmbedder + Send through to Embedder. -impl Embedder for T -where - T: LocalEmbedder + Send, - // (all returned futures of LocalEmbedder's async fns are required to be Send, - // which is satisfied for FastembedEmbedder -- see "Risks" below) -{ /* forwarders */ } -``` - -Notes: - -- The `pub use LocalEmbedder as Embedder;` line on the current - `embedder/mod.rs:57` is **deleted** entirely. `Embedder` is now the - trait that `trait_variant::make` emits at the same module path; the - re-export in `crates/vestige-core/src/lib.rs:167` - (`pub use embedder::{Embedder, ..., LocalEmbedder};`) keeps resolving - unchanged. -- `Sync + 'static` on `LocalEmbedder` (and no `Send` bound on the trait - itself) mirrors the `0001a` pattern for `LocalMemoryStore`. The current - trait carries `Send + Sync + 'static`; the rewrite drops the `Send` - bound from the local variant. `Box` is `Sync` but - not `Send`; `Box` (the generated variant) is `Send + Sync`. -- `trait_variant` 0.1 does **not** require any attribute on the impl - block. The attribute lives only on the trait declaration. See next - section. - ---- - -## Impl Block Migration - -`trait_variant` 0.1 attaches the attribute only to the trait declaration. -The impl side is plain `impl LocalEmbedder for FastembedEmbedder`; no -attribute on the impl, no `#[trait_variant::make(Embedder: Send)]` on the -impl block. The macro auto-generates the blanket -`impl Embedder for T`, so any concrete type that -implements `LocalEmbedder` automatically also implements `Embedder` -provided it is `Send`. - -`FastembedEmbedder` is `Send + Sync` because: - -- `inner: EmbeddingService` is `Send + Sync` (it wraps fastembed's - `TextEmbedding` which is `Send + Sync` after fastembed 4.x; verified - in `crates/vestige-core/src/embeddings/mod.rs`). -- `cached_hash: std::sync::OnceLock` is `Send + Sync` for `T: Send + Sync`. -- The `#[cfg(not(feature = "embeddings"))]` branch carries only - `cached_hash`, which is unconditionally `Send + Sync`. - -No new bound is needed. - -### Before - -`crates/vestige-core/src/embedder/fastembed.rs:38-100` (relevant header): - -```rust -impl Default for FastembedEmbedder { - fn default() -> Self { - Self::new() - } -} - -#[async_trait::async_trait] -impl LocalEmbedder for FastembedEmbedder { - async fn embed(&self, text: &str) -> EmbedderResult> { - // ... body unchanged ... - } - - fn model_name(&self) -> &str { /* ... */ } - fn dimension(&self) -> usize { /* ... */ } - fn model_hash(&self) -> String { /* ... */ } - - async fn embed_batch(&self, texts: &[&str]) -> EmbedderResult>> { - // ... body unchanged ... - } -} -``` - -### After - -`crates/vestige-core/src/embedder/fastembed.rs:38-99` (one fewer line): - -```rust -impl Default for FastembedEmbedder { - fn default() -> Self { - Self::new() - } -} - -impl LocalEmbedder for FastembedEmbedder { - async fn embed(&self, text: &str) -> EmbedderResult> { - // ... body unchanged ... - } - - fn model_name(&self) -> &str { /* ... */ } - fn dimension(&self) -> usize { /* ... */ } - fn model_hash(&self) -> String { /* ... */ } - - async fn embed_batch(&self, texts: &[&str]) -> EmbedderResult>> { - // ... body unchanged ... - } -} -``` - -Diff is exactly one removed line (the `#[async_trait::async_trait]` -attribute on line 44). Every method body, every `async fn` signature, -every `use` statement inside the impl block stays verbatim. No -`Box::pin(async move { ... })`, no manual `Pin>`, no -`'async_trait` lifetime markers; native async-fn-in-trait does this -directly. - -### Why the impl block does not need an attribute - -`trait_variant::make` generates two things from the source trait -(see the "macro-generated output" subsection above): - -1. The source trait itself (`LocalEmbedder`), with native async fns. -2. A second trait (`Embedder`) whose method signatures return - `impl Future + Send` instead of `impl Future`, - plus a blanket impl wiring concrete types through. - -Both are emitted at the macro-call site. `FastembedEmbedder` writes one -impl block (against `LocalEmbedder`); the macro-generated blanket -guarantees `FastembedEmbedder: Embedder` for free. The -`Box` cast on `tests/phase_1/embedder_trait.rs:19` keeps -type-checking unchanged. - ---- - -## Call Site Audit - -Verified via, from the phase2 worktree root: - -```bash -grep -rn "async_trait\|LocalEmbedder\|dyn Embedder" crates/ -grep -rn "use.*Embedder" crates/ tests/ --include="*.rs" -grep -rn "Box\|Arc\|&dyn Embedder" crates/ tests/ --include="*.rs" -grep -rn "Box\|Arc\|&dyn LocalEmbedder" crates/ tests/ --include="*.rs" -grep -rn "impl LocalEmbedder for\|impl Embedder for" crates/ tests/ --include="*.rs" -``` - -### Files that reference the trait object form - -Exactly one, test-only: - -| File | Line | Use | Required change | -|--------------------------------------|------|------------------------------------------------------------------------------------------|-----------------| -| `tests/phase_1/embedder_trait.rs` | 3 | `use vestige_core::embedder::{Embedder, FastembedEmbedder};` | None. `Embedder` is the generated Send variant at the same path. | -| `tests/phase_1/embedder_trait.rs` | 19 | `let e: Box = Box::new(FastembedEmbedder::new());` | None. `FastembedEmbedder: LocalEmbedder + Send` -> blanket gives `: Embedder` -> `Box` is well-formed. | - -### Files that import `Embedder` or `LocalEmbedder` by name - -| File | Line | Use | Required change | -|-----------------------------------------------------|------|----------------------------------------------------------------------------------------------------------------|-----------------| -| `crates/vestige-core/src/lib.rs` | 167 | `pub use embedder::{Embedder, EmbedderError, EmbedderResult, FastembedEmbedder, LocalEmbedder};` | None. Both names still resolve. | -| `crates/vestige-core/src/embedder/mod.rs` | 5 | `pub use fastembed::FastembedEmbedder;` | None. | -| `crates/vestige-core/src/embedder/fastembed.rs` | 7 | `use super::{EmbedderError, EmbedderResult, LocalEmbedder};` | None. | -| `tests/phase_1/embedder_trait.rs` | 3 | `use vestige_core::embedder::{Embedder, FastembedEmbedder};` | None. | - -### Files that implement the trait - -| File | Line | Impl | Required change | -|-----------------------------------------------------|------|-----------------------------------------------------------------------|----------------------------------------------| -| `crates/vestige-core/src/embedder/fastembed.rs` | 45 | `impl LocalEmbedder for FastembedEmbedder` (currently `#[async_trait]`) | Drop the `#[async_trait::async_trait]` attr. | - -No other impls exist. There is no test mock implementing `Embedder` or -`LocalEmbedder` anywhere in the workspace. - -### Files that import `async_trait` directly - -After `0001a` lands, only the embedder pair: - -``` -crates/vestige-core/src/embedder/mod.rs:24 (doc comment) -crates/vestige-core/src/embedder/mod.rs:27 (attribute) -crates/vestige-core/src/embedder/mod.rs:56 (doc comment) -crates/vestige-core/src/embedder/fastembed.rs:44 (attribute) -``` - -Plus the Cargo manifest: - -``` -crates/vestige-core/Cargo.toml:119:async-trait = "0.1" -``` - -### Production files that hold a concrete embedder - -`FastembedEmbedder` is constructed and used by concrete name (not via -trait object) in: the dashboard / MCP layer if it needs to embed query -strings ad-hoc. None of those call sites need an edit because the -concrete type is what they hold, and the concrete type is untouched by -this sub-plan. - -### Conclusion - -| Category | Count | -|--------------------------------------------------|-------| -| Production source files modified | 2 | -| Test source files modified | 0 | -| Cargo manifests modified | 1 | -| Production source files importing `Embedder` / `LocalEmbedder` (verified unchanged) | 3 | -| Test source files importing `Embedder` (verified unchanged) | 1 | -| Direct `async_trait` uses outside the embedder module after `0001a` | 0 | - ---- - -## Cargo.toml Change - -From inside `crates/vestige-core/`: - -```bash -cargo rm async-trait -``` - -This removes line 119 of `Cargo.toml` and updates `Cargo.lock` in one -step. Manual editing is acceptable as a fallback if `cargo rm` is -unavailable in the agent environment; in that case, follow up with -`cargo check -p vestige-core` to refresh the lockfile. - -### Verification - -```bash -# Direct dependency must be gone. -grep -rn "async-trait\|async_trait" --include="Cargo.toml" crates/ -# Expected: empty. - -# Transitive presence is permitted (e.g. via a third-party crate). -cargo tree -p vestige-core --depth 2 | grep async-trait -# Expected: empty for the direct edges; if a sub-dependency still pulls -# async-trait transitively, the output may contain it deeper than depth 2, -# which is fine. We only care about removing the DIRECT edge. -``` - -If `cargo tree --depth 2` returns any `async-trait` line, inspect with -`cargo tree -p vestige-core -i async-trait` to see what is pulling it. -Acceptable parents: any third-party crate. Unacceptable parent: anything -under `vestige-*`, which would mean a missed file. - ---- - -## Commit Sequence - -Three commits, each green on -`cargo test -p vestige-core --features embeddings,vector-search` and -`cargo test -p vestige-core --no-default-features`. - -### Commit 1: rewrite LocalEmbedder trait declaration - -- Touches: `crates/vestige-core/src/embedder/mod.rs` only. -- Action: replace lines 21-57 per the "Trait Declaration Rewrite" - section above. Delete the `pub use LocalEmbedder as Embedder;` line. -- Green after: `cargo check -p vestige-core` (the impl block in - `fastembed.rs` still has its `#[async_trait::async_trait]` attribute; - the macro is harmless when applied to a trait that the impl block - targets by path, because async_trait rewrites the impl's async fns - into boxed-future fns whose signatures still match the native-async - declarations after trait_variant lowering, just as it did for the - SQLite intermediate state in `0001a`'s commit 1). - - **Mitigation if check fails between commits 1 and 2:** combine the - two into a single commit. The split is offered for review convenience; - the build must be green after every commit lands. - -### Commit 2: drop `#[async_trait::async_trait]` from FastembedEmbedder impl - -- Touches: `crates/vestige-core/src/embedder/fastembed.rs` only. -- Action: delete line 44 (`#[async_trait::async_trait]`). -- Green after: - - `cargo test -p vestige-core --features embeddings,vector-search`. - - `cargo test -p vestige-core --no-default-features` (the - `#[cfg(not(feature = "embeddings"))]` branches inside the impl now - stand on their own). - - Phase 1 integration test: `cargo test --test embedder_trait - --features embeddings,vector-search`. - -### Commit 3: drop the async-trait dependency - -- Touches: `crates/vestige-core/Cargo.toml` (plus `Cargo.lock` as a - side effect). -- Action: from inside `crates/vestige-core/`, run `cargo rm async-trait`. -- Green after: `cargo build --workspace --all-targets` and - `cargo test --workspace`. -- Final hard ASCII gate: `! grep -rn "async_trait" crates/` must exit - with status 0 (i.e. the inverted grep finds nothing). - -### Combined alternative - -Commits 1 and 2 may fold into a single commit if the per-step split -feels artificial (the patterns are identical to `0001a`'s commits 3 -and 4). Commit 3 (the Cargo.toml removal) should stay separate so the -dependency-removal diff is visible in isolation. - ---- - -## Verification - -Every command runs from the repo root unless noted otherwise. - -```bash -# 1. Vestige-core, default features (embeddings + vector-search). -cargo test -p vestige-core --features embeddings,vector-search - -# 2. Vestige-core, minimal features (no embeddings, no vector-search). -cargo test -p vestige-core --no-default-features - -# 3. Workspace build, all targets (catches any feature-gated regression -# in the vestige-mcp tools tree). -cargo build --workspace --all-targets - -# 4. Whole-workspace test (vestige-mcp 406 tests + vestige-core 352 -# tests per the CLAUDE.md baseline). -cargo test --workspace - -# 5. Phase 1 embedder integration test (the trait-shape contract). -cargo test --test embedder_trait --features embeddings,vector-search - -# 6. Clippy gate, deny warnings (matches Phase 1 PR policy). -cargo clippy --workspace --all-targets --features embeddings,vector-search -- -D warnings - -# 7. Hard ASCII gate -- async_trait must be gone from source. -! grep -rn "async_trait" crates/ -# Inverted grep: exit 0 iff grep found nothing. - -# 8. Hard ASCII gate -- async-trait must be gone from manifests. -! grep -rn "async-trait" --include="Cargo.toml" crates/ - -# 9. Confirm trait_variant attribute is in place at the embedder. -grep -rn "trait_variant::make" crates/vestige-core/src/embedder/ -# Expected: exactly one hit, in embedder/mod.rs. - -# 10. Workspace-wide trait_variant audit (should match the count after -# 0001a -- two hits total, one for storage, one for embedder). -grep -rn "trait_variant::make" crates/vestige-core/src/ -# Expected: two hits. -``` - -Expected outcomes: - -- Command 1: 352 vestige-core tests pass (matches baseline). -- Command 2: smaller test count, all pass. -- Command 3: workspace builds in dev mode for all targets. -- Command 4: 758 total tests pass (matches CLAUDE.md baseline). -- Command 5: `embedder_trait` integration test passes. The - `fastembed_implements_embedder_trait` assertion (`let e: Box = ...`) is the canary; if `trait_variant::make` failed to - emit the `Embedder` Send variant, this fails to compile. -- Command 6: zero clippy warnings. -- Command 7: empty output. `async_trait` is fully gone from source. -- Command 8: empty output. `async-trait` is fully gone from manifests. -- Command 9: one hit. -- Command 10: two hits. - ---- - -## Acceptance Criteria - -A reviewer should be able to check every box: - -- [ ] `crates/vestige-core/src/embedder/mod.rs` declares the embedder - trait with `#[trait_variant::make(Embedder: Send)] pub trait - LocalEmbedder: Sync + 'static`, no `async_trait` attribute, no - `Send` bound on `LocalEmbedder` itself. -- [ ] `crates/vestige-core/src/embedder/mod.rs` no longer contains - `pub use LocalEmbedder as Embedder;`. -- [ ] `crates/vestige-core/src/embedder/fastembed.rs` declares - `impl LocalEmbedder for FastembedEmbedder` with no attribute on - the impl block. -- [ ] `crates/vestige-core/Cargo.toml` does not declare `async-trait` - as a direct dependency. -- [ ] `grep -rn "async_trait" crates/` returns zero hits. -- [ ] `grep -rn "async-trait" --include="Cargo.toml" crates/` returns - zero hits. -- [ ] `grep -rn "trait_variant::make" crates/vestige-core/src/` returns - exactly two hits (storage trait + embedder trait). -- [ ] All 758 workspace tests pass (`cargo test --workspace`). -- [ ] `tests/phase_1/embedder_trait.rs` compiles and passes with the - `Box` cast intact. -- [ ] `cargo clippy --workspace --all-targets --features - embeddings,vector-search -- -D warnings` is clean. -- [ ] No file under `crates/vestige-mcp/` or under - `crates/vestige-core/src/{neuroscience,advanced,consolidation, - codebase,memory,embeddings}/` was modified by this sub-plan. -- [ ] `Cargo.lock` was updated as a side effect of `cargo rm async-trait` - (it must no longer reference `async-trait`). -- [ ] Doc comments on the embedder trait declaration describe - `trait_variant`, not `async_trait`. - ---- - -## Risks and Mitigations - -- **`trait_variant::make` requires returned futures to be `Send` for the - blanket `impl Embedder for T`. If any - `async fn embed`/`embed_batch` body inside `FastembedEmbedder` captures - a non-Send local, the blanket impl fails to type-check.** - Mitigation: the existing impl bodies call `self.inner.embed(text)` / - `self.inner.embed_batch(texts)`, where `inner: EmbeddingService` is - `Send + Sync` (verified in `crates/vestige-core/src/embeddings/mod.rs`). - No `.await` points exist inside the bodies in either feature branch; - the `EmbeddingService::embed` calls are synchronous. The futures are - trivially `Send`. If a future change introduces a non-Send local - (e.g. an `Rc` or a non-Send guard), the blanket impl will surface that - as a compile error at the dyn cast in `tests/phase_1/embedder_trait.rs`, - which is the correct outcome. -- **The macro's blanket impl interacts oddly with the default `signature` - method.** - Mitigation: `signature` is a synchronous method returning - `crate::storage::ModelSignature`, with no `Send` or `async` concerns. - `trait_variant::make` emits it on both variants as-is. The existing - Phase 1 test `signature_matches_memory_store_registry` exercises this - path and is part of the verification step. -- **`Box` cast in `tests/phase_1/embedder_trait.rs` fails - to resolve after the rewrite.** - Mitigation: the rewrite preserves the `Embedder` symbol at the same - module path; only its provenance changes (now generated by - `trait_variant::make` instead of by `pub use LocalEmbedder as - Embedder;`). The macro is specifically designed so that the generated - trait is dyn-compatible at the Send-bound boundary. Verified by the - identical pattern already working for `MemoryStore` after `0001a`. -- **`cargo rm async-trait` updates `Cargo.lock` but accidentally bumps - other crates.** - Mitigation: run `cargo rm async-trait` and then immediately inspect - the resulting `Cargo.lock` diff. The expected diff is the removal of - the `[[package]] name = "async-trait"` block and its hash. Anything - else is a red flag and should be reverted before committing - (`git checkout -- Cargo.lock` then `cargo update -p async-trait - --precise=remove` -- or fall back to manual edit + `cargo check`). -- **A new workspace crate added in parallel with this work declares - `async-trait` and the dependency removal silently re-introduces it - later.** - Mitigation: the verification step `grep -rn "async-trait" - --include="Cargo.toml" crates/` is part of the acceptance criteria; a - rebase that reintroduces the line will fail this gate. -- **MCP server uses `Embedder` somewhere we missed.** - Mitigation: full workspace grep (`grep -rn "Embedder" crates/`) - returns no hits inside `crates/vestige-mcp/` for the trait names; the - MCP layer uses the concrete `EmbeddingService` from - `crates/vestige-core/src/embeddings/` for ad-hoc embedding calls. The - trait surface is purely internal to `vestige-core`. - ---- - -## Out-of-Band Notes - -- **No other workspace crate declares `async-trait` as a direct - dependency.** Verified by - `grep -rn "async-trait" --include="Cargo.toml" crates/` returning - exactly one hit at `crates/vestige-core/Cargo.toml:119`. There is - nothing to clean up in `crates/vestige-mcp/Cargo.toml` or elsewhere. -- **Order matters across the three Phase 1 amendment sub-plans:** - `0001a` (trait rewrite) -> `0001b` (sqlite split) -> `0001c` (this - one, async-trait sunset). Reversing the order is possible in - principle but would force re-editing the embedder rewrite twice and - leaves the `async-trait` dep behind until very late. -- **This sub-plan amends `feat/storage-trait-phase1` (tip 790c0c8 plus - whatever commits `0001a` and `0001b` added).** The branch has not - been opened upstream yet, so amending in place is safe; no force-push - to a public PR. -- **After this sub-plan lands, the branch is reviewed and merged before - Phase 2 sub-plans (`0002a-` through `0002i-`) begin implementation.** - Phase 2 introduces no async-trait usage; the Postgres backend follows - the same `trait_variant::make` pattern (see ADR 0002 D1). -- **`trait-variant` 0.1 stays in `Cargo.toml`.** It is the only crate - this sub-plan keeps; `async-trait` is the only one it removes. - ---- - -## Self-Contained `/goal` Brief - -For a fresh Claude Code session executing this sub-plan without prior -conversation context: - -1. Check out branch `feat/storage-trait-phase1` (or a worktree off - of it after `0001a` and `0001b` are merged into it). -2. Read this file (`docs/plans/0001c-async-trait-sunset.md`) in full. -3. Read `docs/plans/0001a-trait-rewrite.md` sections "Trait declaration - rewrite" and "Impl block migration" -- they document the exact - pattern this sub-plan mirrors for the embedder. -4. Run the prerequisite audit grep listed under "Prerequisites". If it - returns more than the five hits documented there, stop and report; - the upstream state does not match what this sub-plan assumes. -5. Execute Commit 1 (rewrite `embedder/mod.rs`), then Commit 2 (drop - the attribute on the FastembedEmbedder impl), then Commit 3 - (`cargo rm async-trait`). Run the verification commands listed - above after each commit; do not proceed if any test or clippy gate - fails. -6. Verify every box in "Acceptance Criteria" is ticked. -7. Report file paths touched, test counts, and the final two grep - results (commands 7 and 8 from "Verification") in the closing - message. diff --git a/docs/plans/0002-phase-2-postgres-backend.md b/docs/plans/0002-phase-2-postgres-backend.md deleted file mode 100644 index ed2a186..0000000 --- a/docs/plans/0002-phase-2-postgres-backend.md +++ /dev/null @@ -1,1278 +0,0 @@ -# Phase 2 Plan: PostgreSQL Backend - -> **Supersession Notice (2026-05-26):** This master plan is now archival. Execution is governed by: -> - **ADR**: [`docs/adr/0002-phase-2-execution.md`](../adr/0002-phase-2-execution.md) -- binding decisions -> - **Sub-plans** (executable briefs): -> - Phase 1 amendment: [0001a-trait-rewrite.md](0001a-trait-rewrite.md), [0001b-sqlite-split.md](0001b-sqlite-split.md), [0001c-async-trait-sunset.md](0001c-async-trait-sunset.md) -> - Phase 2: 0002a..0002i (skeleton, pool+config, migrations, store impl, hybrid search, migrate CLI, reembed, tests+benches, runbook) -> -> **Deltas vs body**: trait uses `trait_variant`, error type is `MemoryStoreError`/`MemoryStoreResult`, `connect` is `(url, max_connections)` only, the core table is `knowledge_nodes` (not `memories`) and gains `owner_user_id` + `visibility` + `shared_with_groups` + `codebase`, plus `users`/`groups`/`group_memberships` tables. See ADR 0002 D1-D8. - -**Status**: Draft -**Depends on**: Phase 1 (MemoryStore + Embedder traits, embedding_model registry, domain columns) -**Related**: docs/adr/0001-pluggable-storage-and-network-access.md (Phase 2), docs/prd/001-getting-centralized-vestige.md, docs/plans/local-dev-postgres-setup.md (local cluster provisioning) - ---- - -## Scope - -### In scope - -- `PgMemoryStore` struct implementing the Phase 1 `MemoryStore` trait against `sqlx::PgPool`, including compile-time checked queries via `sqlx::query!` / `sqlx::query_as!`. -- First-class `pgvector` integration: typed `Vector` columns, HNSW index (`vector_cosine_ops`, `m = 16`, `ef_construction = 64`), and use of the cosine-distance operator `<=>`. -- First-class Postgres FTS: GENERATED `tsvector` column (`search_vec`) with `setweight` (A=content, B=node_type, C=tags), GIN index, and `websearch_to_tsquery` at query time. -- Hybrid search via Reciprocal Rank Fusion (RRF) expressed as a single SQL statement with CTEs for FTS and vector subqueries, with optional domain filter through array overlap (`&&`). -- sqlx migrations directory at `crates/vestige-core/migrations/postgres/`, numbered `{NNNN}_{name}.up.sql` / `{NNNN}_{name}.down.sql`, runnable by `sqlx::migrate!` at startup and by `sqlx-cli`. -- Offline query cache committed under `crates/vestige-core/.sqlx/` so a DATABASE_URL is not required at build time. -- Backend selection via `vestige.toml`: `[storage]` section with `backend = "sqlite" | "postgres"` plus the per-backend subsection (`[storage.sqlite]`, `[storage.postgres]`). Exclusive at compile time via `postgres-backend` feature, exclusive at runtime via the enum. -- CLI: `vestige migrate --from sqlite --to postgres --sqlite-path

                    --postgres-url ` -- streaming copy with progress output. -- CLI: `vestige migrate --reembed --model=` -- O(n) re-embed under a new `Embedder`, registry update, HNSW rebuild. -- Testcontainer-based integration tests using the `pgvector/pgvector:pg16` image, behind the `postgres-backend` feature so SQLite-only builds remain untouched. -- `PgMemoryStore` parity with `SqliteMemoryStore` across every public `MemoryStore` method defined in Phase 1. - -### Out of scope - -- Phase 3 (network access): HTTP MCP transport, API key auth, `vestige keys` CLI. The `api_keys` DDL is declared by Phase 3; Phase 2 does not create it. -- Phase 4 (emergent domain classification): `DomainClassifier`, HDBSCAN, discover / rename / merge CLI. Phase 2 provisions the `domains` and `domain_scores` columns and the `domains` table structure so Phase 4 slots in without further migration, but does not compute or classify. -- Phase 5 (federation): cross-node sync. The `review_events` table is declared in Phase 1; Phase 2 only references it where FSRS writes happen. -- Changes to the cognitive engine, Phase 1 traits, or the embedding pipeline itself. Phase 2 only adds a backend. -- SQLCipher parity for Postgres. Operator responsibility (TLS to Postgres, pgcrypto, disk-level encryption) is out of scope for this phase. - ---- - -## Prerequisites - -### Expected Phase 1 artifacts (consumed, not produced) - -Phase 2 treats all of the following as fixed interfaces. Each path is the expected Phase 1 location. - -- `crates/vestige-core/src/storage/mod.rs` -- re-exports the trait and the two concrete backends. -- `crates/vestige-core/src/storage/memory_store.rs` -- defines the `MemoryStore` trait (generated by `trait_variant::make` from `LocalMemoryStore`) with the full CRUD, search, FSRS, graph, and domain surface from the PRD. Phase 2 implements every method here. -- `crates/vestige-core/src/storage/types.rs` -- shared value types: `MemoryRecord`, `SchedulingState`, `SearchQuery`, `SearchResult`, `MemoryEdge`, `Domain`, `StoreStats`, `HealthStatus`. -- `crates/vestige-core/src/storage/error.rs` -- `StoreError` enum plus `pub type StoreResult = Result`. Phase 2 extends this with `StoreError::Postgres(sqlx::Error)` and `StoreError::Migrate(sqlx::migrate::MigrateError)` via `From` impls (the variants themselves MUST live behind `#[cfg(feature = "postgres-backend")]`). -- `crates/vestige-core/src/embedder/mod.rs` -- `Embedder` trait with `embed`, `model_name`, `dimension`, `model_hash`. Phase 2 calls `model_name()`, `dimension()`, and `model_hash()` for the registry. -- `crates/vestige-core/src/storage/sqlite.rs` -- `SqliteMemoryStore: MemoryStore`. Phase 2's `migrate --from sqlite --to postgres` uses this as the source. -- `crates/vestige-core/src/storage/registry.rs` -- `EmbeddingModelRegistry` abstraction that both backends implement. Phase 2 supplies a Postgres version writing to `embedding_model`. -- `crates/vestige-core/migrations/sqlite/` -- V12 (Phase 1) adds `domains TEXT` (JSON-encoded array), `domain_scores TEXT` (JSON), `embedding_model(name, dimension, hash, created_at)`, and `review_events(id, memory_id, timestamp, rating, prior_state, new_state)`. Phase 2 mirrors every column and table in Postgres. - -If any of the above is missing when Phase 2 starts, the first action is to surface the gap back to Phase 1 -- do NOT backfill a partial trait in Phase 2. - -### Required crates (declared in Phase 2, not installed by this doc) - -The agent running Phase 2 uses `cargo add` in `crates/vestige-core/` for each dependency below. Exact versions and feature flags: - -- `sqlx@0.8` with features `runtime-tokio`, `tls-rustls`, `postgres`, `uuid`, `chrono`, `json`, `migrate`, `macros`. Optional (gated by `postgres-backend`). -- `pgvector@0.4` with features `sqlx`. Optional (gated by `postgres-backend`). -- `deadpool` is NOT needed; `sqlx::PgPool` is the pool. -- `toml@0.8` (no features) for `vestige.toml` parsing. Moved to non-optional because both backends share the config surface. -- `figment@0.10` with features `toml`, `env` -- optional, only if Phase 1 has not already picked a config loader. If Phase 1 ships a loader, skip `figment` and reuse. -- `dirs@6` -- already a transitive `directories` dependency; reuse existing. -- `tokio-stream@0.1` (no features). Used by migrate commands for streamed iteration. -- `indicatif@0.17` (no features). Progress bars for the migrate CLI. -- `futures@0.3` with features `std`. Consumed by sqlx stream combinators. - -Dev-only (under `[dev-dependencies]` in `crates/vestige-core/Cargo.toml`, gated by `postgres-backend`): - -- `testcontainers@0.22` with features `blocking` off, `async` on (default). -- `testcontainers-modules@0.10` with features `postgres`. -- `tokio@1` features `macros`, `rt-multi-thread` (already present for core tests). -- `criterion@0.5` already present; add a new `[[bench]]` entry. - -Feature additions in `crates/vestige-core/Cargo.toml`: - -``` -[features] -postgres-backend = ["dep:sqlx", "dep:pgvector", "dep:tokio-stream", "dep:futures"] -``` - -`postgres-backend` is OFF by default. `default = ["embeddings", "vector-search", "bundled-sqlite"]` stays unchanged. `vestige-mcp` forwards a new feature `postgres-backend = ["vestige-core/postgres-backend"]`. - -### External tooling - -- PostgreSQL 16 or newer (uses `gen_random_uuid()` from `pgcrypto` bundled via `CREATE EXTENSION pgcrypto` in migration 0001; pgvector HNSW indexes require pgvector 0.5+). -- The `pgvector` extension installed in the target database (our migration issues `CREATE EXTENSION IF NOT EXISTS vector`). -- `sqlx-cli@0.8` installed on the developer machine for `cargo sqlx prepare --workspace` and `cargo sqlx migrate add` (not a build-time requirement once `.sqlx/` is committed). -- Docker or Podman reachable by the test harness for `testcontainers-modules::postgres` to spin up `pgvector/pgvector:pg16`. -- A local Postgres cluster for `sqlx prepare`, manual migration work, and `vestige migrate --to postgres` smoke runs. The recipe for standing one up on Arch/CachyOS (install, initdb, role + db, pgvector, connection string at `~/.vestige_pg_pw`) lives in `docs/plans/local-dev-postgres-setup.md`. Postgres 18 from the Arch repo satisfies the "16 or newer" requirement above. Phase 2 work assumes `DATABASE_URL` points at that cluster once migrations are applied. - -### Assumed Rust toolchain - -- Rust 2024 edition. -- MSRV 1.91 (per `CLAUDE.md`). `sqlx 0.8` is compatible. -- `rustflags` unchanged. No `nightly`-only features. - ---- - -## Deliverables - -1. Feature gate `postgres-backend` in `crates/vestige-core/Cargo.toml` and `crates/vestige-mcp/Cargo.toml` that cleanly disables all Postgres code paths when off. -2. `crates/vestige-core/src/storage/postgres/mod.rs` -- `PgMemoryStore` struct and `MemoryStore` trait impl (public entry point). -3. `crates/vestige-core/src/storage/postgres/pool.rs` -- `PgMemoryStore::connect(config)` and pool configuration. -4. `crates/vestige-core/src/storage/postgres/search.rs` -- RRF hybrid search query builder and row -> `SearchResult` mapping. -5. `crates/vestige-core/src/storage/postgres/migrations.rs` -- wraps `sqlx::migrate!("./migrations/postgres")` and surfaces typed errors. -6. `crates/vestige-core/src/storage/postgres/registry.rs` -- Postgres `EmbeddingModelRegistry` implementation writing `embedding_model`. -7. `crates/vestige-core/migrations/postgres/0001_init.up.sql` + `0001_init.down.sql` -- extensions, `memories`, `scheduling`, `edges`, `domains`, `embedding_model`, `review_events`, all indexes. -8. `crates/vestige-core/migrations/postgres/0002_hnsw.up.sql` + `0002_hnsw.down.sql` -- HNSW index creation separated so it can be `CREATE INDEX CONCURRENTLY` during reembed. -9. `crates/vestige-core/src/config.rs` -- `VestigeConfig`, `StorageConfig`, `SqliteConfig`, `PostgresConfig`, `EmbeddingsConfig`, plus a single `VestigeConfig::load(path: Option<&Path>)` returning `Result`. -10. `crates/vestige-core/src/storage/postgres/migrate_cli.rs` -- streaming SQLite-to-Postgres copy, domain-aware, with `indicatif` progress. -11. `crates/vestige-core/src/storage/postgres/reembed.rs` -- `ReembedPlan` and its driver; re-encodes all memories via a supplied `Embedder`, updates `embedding_model`, rebuilds HNSW. -12. `crates/vestige-mcp/src/bin/cli.rs` -- two new `clap` subcommands `Migrate` (union of `--from/--to` and `--reembed` variants, one subcommand or two, see Open Questions) wired to deliverables 10 and 11. -13. `crates/vestige-core/.sqlx/` -- offline query cache, committed. -14. `tests/phase_2/` -- six integration test files listed in the Test Plan. -15. `crates/vestige-core/benches/pg_hybrid_search.rs` -- Criterion benches for RRF search at 1k and 100k memories, gated by `postgres-backend`. -16. `docs/runbook/postgres.md` -- brief ops note covering extension install, `max_connections`, backup discipline, and rollback caveats. (Short; only required for the "rollback of migrate" deliverable.) - ---- - -## Detailed Task Breakdown - -### D1. `postgres-backend` feature gate - -- **File**: `crates/vestige-core/Cargo.toml`, `crates/vestige-mcp/Cargo.toml` -- **Depends on**: nothing; this is the first change. -- **Rust snippets**: - -```toml -# crates/vestige-core/Cargo.toml -[features] -default = ["embeddings", "vector-search", "bundled-sqlite"] -bundled-sqlite = ["rusqlite/bundled"] -encryption = ["rusqlite/bundled-sqlcipher"] -postgres-backend = [ - "dep:sqlx", - "dep:pgvector", - "dep:tokio-stream", - "dep:futures", -] - -[dependencies] -sqlx = { version = "0.8", default-features = false, features = [ - "runtime-tokio", "tls-rustls", "postgres", "uuid", "chrono", - "json", "migrate", "macros", -], optional = true } -pgvector = { version = "0.4", features = ["sqlx"], optional = true } -tokio-stream = { version = "0.1", optional = true } -futures = { version = "0.3", optional = true } -toml = "0.8" -indicatif = "0.17" -``` - -- **Behavior notes**: keep the two backends mutually compilable per `CLAUDE.md`. Every `use sqlx::...` sits under `#[cfg(feature = "postgres-backend")]`. Every module under `crates/vestige-core/src/storage/postgres/` carries `#![cfg(feature = "postgres-backend")]` as its file-level attribute. - -### D2. `PgMemoryStore` core struct - -- **File**: `crates/vestige-core/src/storage/postgres/mod.rs` -- **Depends on**: D1, Phase 1 `MemoryStore` trait and value types. -- **Signatures**: - -```rust -#![cfg(feature = "postgres-backend")] - -use std::sync::Arc; -use std::time::Duration; - -use chrono::{DateTime, Utc}; -use pgvector::Vector; -use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; -use sqlx::PgPool; -use uuid::Uuid; - -use crate::embedder::Embedder; -use crate::storage::error::{StoreError, StoreResult}; -use crate::storage::types::{ - Domain, HealthStatus, MemoryEdge, MemoryRecord, SchedulingState, - SearchQuery, SearchResult, StoreStats, -}; -use crate::storage::memory_store::LocalMemoryStore; - -pub mod migrations; -pub mod pool; -pub mod registry; -pub mod search; -pub mod migrate_cli; -pub mod reembed; - -/// Postgres-backed implementation of `MemoryStore`. -/// -/// Cheaply cloneable. Methods take `&self`; interior state lives inside -/// the `PgPool` (which already provides `Sync` via `Arc` internally). -#[derive(Clone)] -pub struct PgMemoryStore { - pool: PgPool, - embedding_dim: i32, - embedding_model: Arc, -} - -#[derive(Debug, Clone)] -pub struct EmbeddingModelDescriptor { - pub name: String, - pub dimension: i32, - pub hash: String, -} - -impl PgMemoryStore { - /// Construct a new store. Runs migrations, reads the registry, validates - /// that the embedder matches the registered model. - pub async fn connect( - url: &str, - max_connections: u32, - embedder: &dyn Embedder, - ) -> StoreResult; - - /// Low-level constructor for tests: supply an existing pool, skip migrate. - pub async fn from_pool( - pool: PgPool, - embedder: &dyn Embedder, - ) -> StoreResult; - - /// Accessor used by migrate/reembed CLI. - pub fn pool(&self) -> &PgPool { &self.pool } - - pub fn embedding_dim(&self) -> i32 { self.embedding_dim } -} - -#[trait_variant::make(crate::storage::memory_store::MemoryStore: Send)] -impl LocalMemoryStore for PgMemoryStore { - async fn init(&self) -> StoreResult<()>; - async fn health_check(&self) -> StoreResult; - - async fn insert(&self, record: &MemoryRecord) -> StoreResult; - async fn get(&self, id: Uuid) -> StoreResult>; - async fn update(&self, record: &MemoryRecord) -> StoreResult<()>; - async fn delete(&self, id: Uuid) -> StoreResult<()>; - - async fn search(&self, query: &SearchQuery) -> StoreResult>; - async fn fts_search(&self, text: &str, limit: usize) -> StoreResult>; - async fn vector_search(&self, embedding: &[f32], limit: usize) -> StoreResult>; - - async fn get_scheduling(&self, memory_id: Uuid) -> StoreResult>; - async fn update_scheduling(&self, state: &SchedulingState) -> StoreResult<()>; - async fn get_due_memories( - &self, - before: DateTime, - limit: usize, - ) -> StoreResult>; - - async fn add_edge(&self, edge: &MemoryEdge) -> StoreResult<()>; - async fn get_edges(&self, node_id: Uuid, edge_type: Option<&str>) -> StoreResult>; - async fn remove_edge(&self, source: Uuid, target: Uuid, edge_type: &str) -> StoreResult<()>; - async fn get_neighbors(&self, node_id: Uuid, depth: usize) -> StoreResult>; - - async fn list_domains(&self) -> StoreResult>; - async fn get_domain(&self, id: &str) -> StoreResult>; - async fn upsert_domain(&self, domain: &Domain) -> StoreResult<()>; - async fn delete_domain(&self, id: &str) -> StoreResult<()>; - async fn classify(&self, embedding: &[f32]) -> StoreResult>; - - async fn count(&self) -> StoreResult; - async fn get_stats(&self) -> StoreResult; - async fn vacuum(&self) -> StoreResult<()>; -} -``` - -- **SQL (inline within impl methods)**: every call uses `sqlx::query!` or `sqlx::query_as!` for compile-time validation. Examples: - -```rust -// insert -sqlx::query!( - r#" - INSERT INTO memories ( - id, domains, domain_scores, content, node_type, tags, - embedding, metadata, created_at, updated_at - ) VALUES ($1, $2, $3, $4, $5, $6, $7::vector, $8, $9, $10) - "#, - record.id, - &record.domains as &[String], - serde_json::to_value(&record.domain_scores)?, - record.content, - record.node_type, - &record.tags as &[String], - record.embedding.as_ref().map(|v| Vector::from(v.clone())) as Option, - record.metadata, - record.created_at, - record.updated_at, -) -.execute(&self.pool) -.await?; -``` - -- **Behavior notes**: - - `StoreError` gets two new variants behind the feature: - -```rust -#[cfg(feature = "postgres-backend")] -#[error("postgres error: {0}")] -Postgres(#[from] sqlx::Error), - -#[cfg(feature = "postgres-backend")] -#[error("postgres migration error: {0}")] -Migrate(#[from] sqlx::migrate::MigrateError), -``` - - - `classify()` on Postgres implements the PRD's cosine-similarity-to-centroid computation inside SQL using `1 - (centroid <=> $1::vector)` over the `domains` table and returns rows sorted descending. This mirrors the behavior a `DomainClassifier` in Phase 4 uses; Phase 2 ships the backend capability but does not call it. - - Connection pool defaults (see D3): `max_connections = 10`, `acquire_timeout = 30s`, `idle_timeout = 600s`, `test_before_acquire = false` (cheap queries; avoid per-acquire roundtrip). - - All methods are `async fn` and use sqlx's `tokio` runtime feature; no blocking `block_on`. - -### D3. Pool construction and config wiring - -- **File**: `crates/vestige-core/src/storage/postgres/pool.rs` -- **Depends on**: D1, D2, D9. -- **Signatures**: - -```rust -#![cfg(feature = "postgres-backend")] - -use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; -use sqlx::{ConnectOptions, PgPool}; -use std::str::FromStr; -use std::time::Duration; - -use crate::config::PostgresConfig; -use crate::storage::error::{StoreError, StoreResult}; - -pub async fn build_pool(cfg: &PostgresConfig) -> StoreResult { - let mut opts = PgConnectOptions::from_str(&cfg.url)?; - opts = opts - .application_name("vestige") - .statement_cache_capacity(256) - .log_statements(tracing::log::LevelFilter::Debug); - - let pool = PgPoolOptions::new() - .max_connections(cfg.max_connections.unwrap_or(10)) - .min_connections(0) - .acquire_timeout(Duration::from_secs(cfg.acquire_timeout_secs.unwrap_or(30))) - .idle_timeout(Some(Duration::from_secs(600))) - .max_lifetime(Some(Duration::from_secs(1800))) - .test_before_acquire(false) - .connect_with(opts) - .await?; - - Ok(pool) -} -``` - -- **Behavior notes**: acquire timeout chosen to exceed the 30-second testcontainer spin-up requirement. `application_name = "vestige"` makes `pg_stat_activity` readable from `psql` during debugging. - -### D4. sqlx migrations directory - -- **File**: `crates/vestige-core/migrations/postgres/0001_init.up.sql`, `0001_init.down.sql`, `0002_hnsw.up.sql`, `0002_hnsw.down.sql`. -- **Depends on**: none (pure SQL). - -`0001_init.up.sql`: - -```sql --- Extensions -CREATE EXTENSION IF NOT EXISTS pgcrypto; -CREATE EXTENSION IF NOT EXISTS vector; - --- Embedding model registry --- Mirrors the SQLite table created in Phase 1. -CREATE TABLE embedding_model ( - id SMALLINT PRIMARY KEY DEFAULT 1 CHECK (id = 1), - name TEXT NOT NULL, - dimension INTEGER NOT NULL CHECK (dimension > 0), - hash TEXT NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT now() -); - --- Domains table (populated by Phase 4 DomainClassifier; Phase 2 only creates --- the empty table so list/get/upsert/delete work against both backends). -CREATE TABLE domains ( - id TEXT PRIMARY KEY, - label TEXT NOT NULL, - centroid vector, - top_terms TEXT[] NOT NULL DEFAULT '{}', - memory_count INTEGER NOT NULL DEFAULT 0, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - metadata JSONB NOT NULL DEFAULT '{}'::jsonb -); - --- Core memories table -CREATE TABLE memories ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - domains TEXT[] NOT NULL DEFAULT '{}', - domain_scores JSONB NOT NULL DEFAULT '{}'::jsonb, - content TEXT NOT NULL, - node_type TEXT NOT NULL DEFAULT 'general', - tags TEXT[] NOT NULL DEFAULT '{}', - embedding vector, - metadata JSONB NOT NULL DEFAULT '{}'::jsonb, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - search_vec TSVECTOR GENERATED ALWAYS AS ( - setweight(to_tsvector('english', coalesce(content, '')), 'A') || - setweight(to_tsvector('english', coalesce(node_type, '')), 'B') || - setweight(to_tsvector('english', coalesce(array_to_string(tags, ' '), '')), 'C') - ) STORED -); - --- FSRS scheduling state (1:1 with memories) -CREATE TABLE scheduling ( - memory_id UUID PRIMARY KEY REFERENCES memories(id) ON DELETE CASCADE, - stability DOUBLE PRECISION NOT NULL DEFAULT 0.0, - difficulty DOUBLE PRECISION NOT NULL DEFAULT 0.0, - retrievability DOUBLE PRECISION NOT NULL DEFAULT 1.0, - last_review TIMESTAMPTZ, - next_review TIMESTAMPTZ, - reps INTEGER NOT NULL DEFAULT 0, - lapses INTEGER NOT NULL DEFAULT 0 -); - --- Graph edges (spreading activation) -CREATE TABLE edges ( - source_id UUID NOT NULL REFERENCES memories(id) ON DELETE CASCADE, - target_id UUID NOT NULL REFERENCES memories(id) ON DELETE CASCADE, - edge_type TEXT NOT NULL DEFAULT 'related', - weight DOUBLE PRECISION NOT NULL DEFAULT 1.0, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - PRIMARY KEY (source_id, target_id, edge_type) -); - --- FSRS review event log (Phase 1 creates this; Phase 2 mirrors it for Postgres). --- Append-only. Used for future federation (Phase 5). -CREATE TABLE review_events ( - id BIGSERIAL PRIMARY KEY, - memory_id UUID NOT NULL REFERENCES memories(id) ON DELETE CASCADE, - timestamp TIMESTAMPTZ NOT NULL DEFAULT now(), - rating SMALLINT NOT NULL, - prior_state JSONB NOT NULL, - new_state JSONB NOT NULL -); - --- Indexes on memories (vector index is declared separately in 0002_hnsw.up.sql) -CREATE INDEX idx_memories_fts ON memories USING GIN (search_vec); -CREATE INDEX idx_memories_domains ON memories USING GIN (domains); -CREATE INDEX idx_memories_tags ON memories USING GIN (tags); -CREATE INDEX idx_memories_node_type ON memories (node_type); -CREATE INDEX idx_memories_created ON memories (created_at); -CREATE INDEX idx_memories_updated ON memories (updated_at); - --- Indexes on scheduling -CREATE INDEX idx_scheduling_next_review ON scheduling (next_review); -CREATE INDEX idx_scheduling_last_review ON scheduling (last_review); - --- Indexes on edges -CREATE INDEX idx_edges_target ON edges (target_id); -CREATE INDEX idx_edges_source ON edges (source_id); -CREATE INDEX idx_edges_type ON edges (edge_type); - --- Indexes on review_events -CREATE INDEX idx_review_events_memory ON review_events (memory_id); -CREATE INDEX idx_review_events_ts ON review_events (timestamp); - --- Update trigger on memories.updated_at -CREATE OR REPLACE FUNCTION memories_set_updated_at() RETURNS TRIGGER AS $$ -BEGIN - NEW.updated_at := now(); - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -CREATE TRIGGER trg_memories_updated_at -BEFORE UPDATE ON memories -FOR EACH ROW EXECUTE FUNCTION memories_set_updated_at(); -``` - -`0001_init.down.sql`: - -```sql -DROP TRIGGER IF EXISTS trg_memories_updated_at ON memories; -DROP FUNCTION IF EXISTS memories_set_updated_at(); - -DROP INDEX IF EXISTS idx_review_events_ts; -DROP INDEX IF EXISTS idx_review_events_memory; -DROP INDEX IF EXISTS idx_edges_type; -DROP INDEX IF EXISTS idx_edges_source; -DROP INDEX IF EXISTS idx_edges_target; -DROP INDEX IF EXISTS idx_scheduling_last_review; -DROP INDEX IF EXISTS idx_scheduling_next_review; -DROP INDEX IF EXISTS idx_memories_updated; -DROP INDEX IF EXISTS idx_memories_created; -DROP INDEX IF EXISTS idx_memories_node_type; -DROP INDEX IF EXISTS idx_memories_tags; -DROP INDEX IF EXISTS idx_memories_domains; -DROP INDEX IF EXISTS idx_memories_fts; - -DROP TABLE IF EXISTS review_events; -DROP TABLE IF EXISTS edges; -DROP TABLE IF EXISTS scheduling; -DROP TABLE IF EXISTS memories; -DROP TABLE IF EXISTS domains; -DROP TABLE IF EXISTS embedding_model; -``` - -`0002_hnsw.up.sql` (separated so reembed can drop-and-recreate without touching the rest of the schema): - -```sql --- HNSW index on memories.embedding. --- pgvector requires the column to have a typmod (fixed dimension) for HNSW. --- The dimension is stamped by the application at startup via ALTER TABLE --- using the embedder's dimension() method (see PgMemoryStore::connect). --- We express the index with the generic vector_cosine_ops operator class. -CREATE INDEX idx_memories_embedding_hnsw - ON memories USING hnsw (embedding vector_cosine_ops) - WITH (m = 16, ef_construction = 64); -``` - -`0002_hnsw.down.sql`: - -```sql -DROP INDEX IF EXISTS idx_memories_embedding_hnsw; -``` - -- **Behavior notes**: - - pgvector HNSW requires a typmod. `PgMemoryStore::connect` runs `ALTER TABLE memories ALTER COLUMN embedding TYPE vector($N)` with `$N = embedder.dimension()` exactly once, guarded by a check against `embedding_model` (first startup ever) or validated against it on subsequent starts. If `embedder.dimension()` differs from the stored one and `embedding_model` is non-empty, return `StoreError::EmbeddingDimensionMismatch` -- the user must run `vestige migrate --reembed`. - - `ALTER COLUMN ... TYPE vector($N)` on a populated column fails unless the data fits; that is the desired safety net. - - The `tsvector` GENERATED column uses `array_to_string(tags, ' ')` rather than `array_to_tsvector` from the PRD sketch, because `array_to_tsvector` is not a core function in Postgres 16 and would require an extension. The behavior is equivalent for weight C. - - `gen_random_uuid()` comes from `pgcrypto`. In Postgres 13+ it is also available from core; we keep the extension for older compatibility paths. - - MVCC: all table writes are transactional; no explicit locks. `INSERT ... ON CONFLICT DO UPDATE` is used in `upsert_domain`, `update_scheduling`, and edge idempotency. - -### D5. Hybrid search via RRF - -- **File**: `crates/vestige-core/src/storage/postgres/search.rs` -- **Depends on**: D2, D4. -- **Signatures**: - -```rust -#![cfg(feature = "postgres-backend")] - -use pgvector::Vector; -use sqlx::PgPool; -use uuid::Uuid; - -use crate::storage::error::StoreResult; -use crate::storage::types::{SearchQuery, SearchResult}; - -const RRF_K: i32 = 60; // constant from Cormack et al. 2009 -const OVERFETCH_MULT: i64 = 3; // matches Phase 1 SQLite overfetch - -pub(crate) async fn rrf_search( - pool: &PgPool, - query: &SearchQuery, -) -> StoreResult>; -``` - -SQL for the full hybrid RRF query. Placeholders: -- `$1` = text query (string, may be empty) -- `$2` = embedding (vector) -- `$3` = overfetch limit per branch (int) -- `$4` = final limit (int) -- `$5` = domain filter (text[] or NULL) -- `$6` = node_type filter (text[] or NULL) -- `$7` = tag filter (text[] or NULL) - -```sql -WITH params AS ( - SELECT - $1::text AS q_text, - $2::vector AS q_vec, - $3::int AS overfetch, - $4::int AS final_limit, - $5::text[] AS dom_filter, - $6::text[] AS nt_filter, - $7::text[] AS tag_filter -), -fts AS ( - SELECT m.id, - ts_rank_cd(m.search_vec, websearch_to_tsquery('english', p.q_text)) AS score, - ROW_NUMBER() OVER ( - ORDER BY ts_rank_cd(m.search_vec, websearch_to_tsquery('english', p.q_text)) DESC - ) AS rank - FROM memories m, params p - WHERE p.q_text <> '' - AND m.search_vec @@ websearch_to_tsquery('english', p.q_text) - AND (p.dom_filter IS NULL OR m.domains && p.dom_filter) - AND (p.nt_filter IS NULL OR m.node_type = ANY(p.nt_filter)) - AND (p.tag_filter IS NULL OR m.tags && p.tag_filter) - LIMIT (SELECT overfetch FROM params) -), -vec AS ( - SELECT m.id, - 1 - (m.embedding <=> p.q_vec) AS score, - ROW_NUMBER() OVER ( - ORDER BY m.embedding <=> p.q_vec - ) AS rank - FROM memories m, params p - WHERE m.embedding IS NOT NULL - AND p.q_vec IS NOT NULL - AND (p.dom_filter IS NULL OR m.domains && p.dom_filter) - AND (p.nt_filter IS NULL OR m.node_type = ANY(p.nt_filter)) - AND (p.tag_filter IS NULL OR m.tags && p.tag_filter) - LIMIT (SELECT overfetch FROM params) -), -fused AS ( - SELECT COALESCE(f.id, v.id) AS id, - COALESCE(1.0 / (60 + f.rank), 0.0) - + COALESCE(1.0 / (60 + v.rank), 0.0) AS rrf_score, - f.score AS fts_score, - v.score AS vector_score - FROM fts f FULL OUTER JOIN vec v ON f.id = v.id -) -SELECT m.id AS "id!: Uuid", - m.domains AS "domains!: Vec", - m.domain_scores AS "domain_scores!: serde_json::Value", - m.content AS "content!", - m.node_type AS "node_type!", - m.tags AS "tags!: Vec", - m.embedding AS "embedding?: Vector", - m.metadata AS "metadata!: serde_json::Value", - m.created_at AS "created_at!: chrono::DateTime", - m.updated_at AS "updated_at!: chrono::DateTime", - fused.rrf_score AS "rrf_score!: f64", - fused.fts_score AS "fts_score?: f64", - fused.vector_score AS "vector_score?: f64" -FROM fused -JOIN memories m ON m.id = fused.id -ORDER BY fused.rrf_score DESC -LIMIT (SELECT final_limit FROM params); -``` - -- **Behavior notes**: - - `OVERFETCH_MULT * query.limit` is passed as `$3`. Final `$4` is `query.limit`. - - Empty text query is allowed; the `fts` CTE returns zero rows (`p.q_text <> ''`) and the result degrades to pure vector search, which matches `vector_search` behavior. - - Null embedding is allowed; the `vec` CTE returns zero rows and the result degrades to pure FTS, which matches `fts_search` behavior. - - `fts_search` and `vector_search` are separate public methods on the trait. Each uses a simpler single-CTE query derived from the above by removing the other branch. Implementing them as thin wrappers over `rrf_search` with nullified inputs is acceptable but adds one extra plan per call; the explicit implementations win on latency. - - `min_retrievability` in `SearchQuery` is applied as a final filter by joining on `scheduling` in the outer `SELECT`. Adding that join unconditionally regresses simple searches; add it only when `query.min_retrievability.is_some()`. - -### D6. `embedding_model` registry impl - -- **File**: `crates/vestige-core/src/storage/postgres/registry.rs` -- **Depends on**: D1, D4 (table exists), Phase 1 `EmbeddingModelRegistry` trait. -- **Signatures**: - -```rust -#![cfg(feature = "postgres-backend")] - -use sqlx::PgPool; - -use crate::embedder::Embedder; -use crate::storage::error::{StoreError, StoreResult}; - -pub(crate) async fn ensure_registry( - pool: &PgPool, - embedder: &dyn Embedder, -) -> StoreResult<()> { - let row = sqlx::query!( - r#"SELECT name, dimension, hash FROM embedding_model WHERE id = 1"# - ) - .fetch_optional(pool) - .await?; - - match row { - None => { - sqlx::query!( - r#" - INSERT INTO embedding_model (id, name, dimension, hash) - VALUES (1, $1, $2, $3) - "#, - embedder.model_name(), - embedder.dimension() as i32, - embedder.model_hash(), - ) - .execute(pool) - .await?; - - // First-ever run: stamp the vector column typmod. - let ddl = format!( - "ALTER TABLE memories ALTER COLUMN embedding TYPE vector({})", - embedder.dimension() - ); - sqlx::query(&ddl).execute(pool).await?; - Ok(()) - } - Some(r) if r.name == embedder.model_name() - && r.dimension == embedder.dimension() as i32 - && r.hash == embedder.model_hash() => Ok(()), - Some(r) => Err(StoreError::EmbeddingMismatch { - expected: format!("{} ({}d, {})", r.name, r.dimension, r.hash), - got: format!( - "{} ({}d, {})", - embedder.model_name(), - embedder.dimension(), - embedder.model_hash() - ), - }), - } -} - -pub(crate) async fn update_registry( - pool: &PgPool, - embedder: &dyn Embedder, -) -> StoreResult<()> { - // Used only by `vestige migrate --reembed` after a full re-encode. - sqlx::query!( - r#" - UPDATE embedding_model - SET name = $1, dimension = $2, hash = $3, created_at = now() - WHERE id = 1 - "#, - embedder.model_name(), - embedder.dimension() as i32, - embedder.model_hash(), - ) - .execute(pool) - .await?; - Ok(()) -} -``` - -- **Behavior notes**: - - `StoreError::EmbeddingMismatch { expected, got }` already exists in Phase 1; Phase 2 just constructs it. - - The `ALTER TABLE ... TYPE vector(N)` DDL is only issued on first init. On subsequent inits the existing typmod already matches. - - Re-embed flow also uses this module, but the DDL path is different -- see D11. - -### D7. `VestigeConfig`: `vestige.toml` backend selection - -- **File**: `crates/vestige-core/src/config.rs` (Phase 1 may already own this file; Phase 2 extends, not replaces) -- **Depends on**: D1. -- **Signatures**: - -```rust -use std::path::{Path, PathBuf}; - -use serde::Deserialize; - -#[derive(Debug, Clone, Deserialize)] -pub struct VestigeConfig { - #[serde(default)] - pub embeddings: EmbeddingsConfig, - #[serde(default)] - pub storage: StorageConfig, - #[serde(default)] - pub server: ServerConfig, - #[serde(default)] - pub auth: AuthConfig, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct EmbeddingsConfig { - pub provider: String, // "fastembed" - pub model: String, // "BAAI/bge-base-en-v1.5" -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(tag = "backend", rename_all = "lowercase")] -pub enum StorageConfig { - Sqlite(SqliteConfig), - #[cfg(feature = "postgres-backend")] - Postgres(PostgresConfig), -} - -#[derive(Debug, Clone, Deserialize)] -pub struct SqliteConfig { - pub path: PathBuf, -} - -#[cfg(feature = "postgres-backend")] -#[derive(Debug, Clone, Deserialize)] -pub struct PostgresConfig { - pub url: String, - #[serde(default)] - pub max_connections: Option, - #[serde(default)] - pub acquire_timeout_secs: Option, -} - -#[derive(Debug, Clone, Default, Deserialize)] -pub struct ServerConfig { /* Phase 3 fills this in */ } - -#[derive(Debug, Clone, Default, Deserialize)] -pub struct AuthConfig { /* Phase 3 fills this in */ } - -impl VestigeConfig { - pub fn load(path: Option<&Path>) -> Result; - pub fn default_path() -> PathBuf; // ~/.vestige/vestige.toml -} - -#[derive(Debug, thiserror::Error)] -pub enum ConfigError { - #[error("io: {0}")] - Io(#[from] std::io::Error), - #[error("toml: {0}")] - Toml(#[from] toml::de::Error), - #[error("invalid config: {0}")] - Invalid(String), -} -``` - -- **Behavior notes**: - - The serde representation matches the PRD: `[storage]` with `backend = "sqlite"` and a matching `[storage.sqlite]` or `[storage.postgres]` subsection. - - Because `StorageConfig` is `#[serde(tag = "backend")]`, an unknown backend string returns a clear error. - - If `postgres-backend` is compiled off and the user writes `backend = "postgres"`, deserialization returns "unknown variant `postgres`" -- loud failure. Phase 2 wraps this into `ConfigError::Invalid("postgres-backend feature not compiled in")`. - - `env`-override hooks (e.g., `VESTIGE_POSTGRES_URL`) are a Phase 3 concern; not added here. - -### D8. `vestige migrate --from sqlite --to postgres` - -- **File**: `crates/vestige-core/src/storage/postgres/migrate_cli.rs` -- **Depends on**: D2, D6, D7, Phase 1 `SqliteMemoryStore`. -- **Signatures**: - -```rust -#![cfg(feature = "postgres-backend")] - -use std::path::Path; -use std::sync::Arc; - -use futures::{StreamExt, TryStreamExt}; -use indicatif::{ProgressBar, ProgressStyle}; -use uuid::Uuid; - -use crate::embedder::Embedder; -use crate::storage::error::{StoreError, StoreResult}; -use crate::storage::postgres::PgMemoryStore; -use crate::storage::sqlite::SqliteMemoryStore; - -#[derive(Debug, Clone)] -pub struct SqliteToPostgresPlan { - pub sqlite_path: std::path::PathBuf, - pub postgres_url: String, - pub max_connections: u32, - pub batch_size: usize, // default 500 -} - -pub struct MigrationReport { - pub memories_copied: u64, - pub scheduling_rows: u64, - pub edges_copied: u64, - pub review_events_copied: u64, - pub domains_copied: u64, - pub errors: Vec<(Uuid, StoreError)>, -} - -pub async fn run_sqlite_to_postgres( - plan: SqliteToPostgresPlan, - embedder: Arc, -) -> StoreResult; -``` - -Algorithm: - -1. Open source `SqliteMemoryStore` in read-only mode (`?mode=ro`). -2. Check source `embedding_model` registry; refuse if it disagrees with the supplied embedder unless the user also passed `--reembed`. -3. Open destination `PgMemoryStore` via `connect` (runs migrations, stamps dim). -4. Stream source rows in batches of `plan.batch_size` via a windowed query ordered by `created_at, id` (stable cursor; survives resume). -5. For each batch: begin a Postgres transaction, `INSERT INTO memories ... ON CONFLICT (id) DO NOTHING` for all rows, `INSERT INTO scheduling` likewise, commit. Copy domain assignments (`domains`, `domain_scores`) verbatim -- they are `[]` and `{}` for pre-Phase-4 SQLite data. -6. After memories finish, stream edges and review_events the same way. -7. Emit progress via `indicatif::ProgressBar` (one bar per table, multi-bar). Each 1000 rows log to tracing at INFO. -8. Return `MigrationReport` for the caller to print. - -- **Behavior notes**: - - Memory-bounded: batch size 500 and sqlx streams mean memory usage stays O(batch * row_size), not O(total_rows). - - Idempotent: re-running replays only the rows not already present; `ON CONFLICT DO NOTHING` means partial runs recover. - - UUID strings from SQLite are parsed via `Uuid::parse_str` -- any mangled ID pushes to `errors` instead of aborting. - - The FTS `search_vec` is regenerated by Postgres via the GENERATED column; no data to copy. - - `review_events` may not exist in Phase 1 SQLite for pre-V12 databases. The migrator detects missing tables via `SELECT name FROM sqlite_master` and skips gracefully. - - A separate `--dry-run` flag prints the counts per table without writing. - -### D9. `vestige migrate --reembed --model=` - -- **File**: `crates/vestige-core/src/storage/postgres/reembed.rs` -- **Depends on**: D2, D6, Phase 1 `Embedder`. -- **Signatures**: - -```rust -#![cfg(feature = "postgres-backend")] - -use std::sync::Arc; -use std::time::Instant; - -use futures::TryStreamExt; -use indicatif::{ProgressBar, ProgressStyle}; -use sqlx::PgPool; -use uuid::Uuid; - -use crate::embedder::Embedder; -use crate::storage::error::{StoreError, StoreResult}; -use crate::storage::postgres::PgMemoryStore; - -#[derive(Debug, Clone)] -pub struct ReembedPlan { - pub batch_size: usize, // default 128 (embedder batch) - pub drop_hnsw_first: bool, // default true - pub concurrent_index: bool, // default false; use CREATE INDEX (not CONCURRENTLY) -} - -pub struct ReembedReport { - pub rows_updated: u64, - pub duration_secs: f64, - pub index_rebuild_secs: f64, -} - -pub async fn run_reembed( - store: &PgMemoryStore, - new_embedder: Arc, - plan: ReembedPlan, -) -> StoreResult; -``` - -Algorithm: - -1. Verify `new_embedder.dimension()` != stored dimension OR `new_embedder.model_hash()` != stored hash -- otherwise no-op and return `rows_updated = 0`. -2. `BEGIN; ALTER TABLE memories ALTER COLUMN embedding DROP NOT NULL`; not actually needed (column is already nullable) but shown here for documentation. -3. If `plan.drop_hnsw_first`, execute `DROP INDEX IF EXISTS idx_memories_embedding_hnsw;` so updates are not slowed by index maintenance. This is the recommended path; `REINDEX` is kept in the Open Questions as an alternative. -4. Stream all `id, content` from `memories` ordered by `id`. -5. For each batch of `plan.batch_size`: call `new_embedder.embed_batch(&texts)` (Phase 1 trait exposes batched embedding when available; otherwise loop single `embed`). Then: - -```sql -UPDATE memories -SET embedding = v.embedding::vector -FROM UNNEST($1::uuid[], $2::real[][]) AS v(id, embedding) -WHERE memories.id = v.id; -``` - -6. After all rows updated: run `ALTER TABLE memories ALTER COLUMN embedding TYPE vector($NEW_DIM)` if dimension changed. -7. Rebuild HNSW. If `plan.concurrent_index`, execute `CREATE INDEX CONCURRENTLY idx_memories_embedding_hnsw ...`; else `CREATE INDEX idx_memories_embedding_hnsw ...`. -8. `update_registry` with the new embedder. -9. Return `ReembedReport`. - -- **Behavior notes**: - - Memory-bounded: batch_size * 2 (old + new texts) vectors in RAM at any time. - - The dimension change must happen AFTER all rows are updated (pgvector validates typmod on write when a typmod is present; we relax-then-tighten). - - `CONCURRENTLY` builds do not hold `AccessExclusiveLock`, but fail inside a transaction. That's why the outer driver runs index DDL as an autocommit statement (sqlx `execute` outside a pool transaction). - - For `--dry-run`, emit what *would* happen (row count, estimated embedder calls, estimated time using `rows / 50`-per-second baseline for local fastembed) and exit. - -### D10. CLI wiring in `vestige-mcp` - -- **File**: `crates/vestige-mcp/src/bin/cli.rs` -- **Depends on**: D8, D9, D7. Requires `vestige-mcp` Cargo feature `postgres-backend`. -- **Signatures**: - -```rust -#[derive(Subcommand)] -enum Commands { - // existing variants: Stats, Health, Consolidate, Restore, Backup, - // Export, Gc, Dashboard, Ingest, Serve ... - - /// Migrate between backends or re-embed memories. - #[cfg(feature = "postgres-backend")] - Migrate(MigrateArgs), -} - -#[derive(clap::Args)] -#[cfg(feature = "postgres-backend")] -struct MigrateArgs { - #[command(subcommand)] - action: MigrateAction, -} - -#[derive(Subcommand)] -#[cfg(feature = "postgres-backend")] -enum MigrateAction { - /// Copy all memories from SQLite to Postgres. - #[command(name = "copy")] - Copy { - #[arg(long)] - from: String, // "sqlite" - #[arg(long)] - to: String, // "postgres" - #[arg(long)] - sqlite_path: PathBuf, - #[arg(long)] - postgres_url: String, - #[arg(long, default_value = "500")] - batch_size: usize, - #[arg(long)] - dry_run: bool, - }, - /// Re-embed all memories with a new embedder. - #[command(name = "reembed")] - Reembed { - #[arg(long)] - model: String, - #[arg(long, default_value = "128")] - batch_size: usize, - #[arg(long, default_value_t = true)] - drop_hnsw_first: bool, - #[arg(long)] - concurrent_index: bool, - #[arg(long)] - dry_run: bool, - }, -} -``` - -The user-facing invocation collapses to the exact string requested by the ADR: - -``` -vestige migrate copy --from sqlite --to postgres \ - --sqlite-path ~/.vestige/vestige.db \ - --postgres-url postgresql://localhost/vestige - -vestige migrate reembed --model=BAAI/bge-large-en-v1.5 -``` - -An alternate top-level layout (single `vestige migrate` with flags `--from`, `--to`, `--reembed`) is equivalent; the subcommand split is preferred because the two flag sets are disjoint (see Open Question 1). - -- **Behavior notes**: - - `--from`/`--to` values are validated; the current Phase 2 build accepts only `sqlite` and `postgres`. - - For `reembed`, the `--model` string resolves to an `Embedder` via a factory already provided by Phase 1 (`Embedder::from_name(&str)`); Phase 2 does not invent new embedder constructors. - - Progress output on `stderr`; machine-readable summary on `stdout` as one-line JSON when `--json` is set (skipped for Phase 2 unless trivial). - -### D11. Offline query cache (`.sqlx/`) - -- **File**: `crates/vestige-core/.sqlx/` (committed directory of `query-*.json`) -- **Depends on**: all `sqlx::query!` call sites being final. -- **Procedure**: the developer runs `cargo sqlx prepare --workspace` with a live Postgres having the schema applied. Output goes into `crates/vestige-core/.sqlx/`. This directory is committed. CI enforces freshness by running `cargo sqlx prepare --workspace --check` against the same live Postgres (or failing that, any dev can reproduce by setting `SQLX_OFFLINE=true`). -- **Behavior notes**: `SQLX_OFFLINE=true` in `build.rs` or env is the default on CI and for downstream consumers. The `vestige-core` docs add a one-liner in README for contributors: "if you change any SQL in Phase 2 modules, rerun `cargo sqlx prepare` with a live DB." - -### D12. Testcontainer harness (integration) - -- **File**: `tests/phase_2/common/mod.rs` (the `common` convention used in `tests/phase_2/` crates) -- **Depends on**: D2 through D11. -- **Signatures**: - -```rust -#![cfg(feature = "postgres-backend")] - -use std::sync::Arc; - -use testcontainers_modules::postgres::Postgres; -use testcontainers::{runners::AsyncRunner, ContainerAsync}; - -use vestige_core::embedder::Embedder; -use vestige_core::storage::postgres::PgMemoryStore; - -pub struct PgHarness { - pub container: ContainerAsync, - pub store: PgMemoryStore, -} - -impl PgHarness { - pub async fn start(embedder: Arc) -> anyhow::Result { - let container = Postgres::default() - .with_tag("pg16") - .with_name("pgvector/pgvector") - .start() - .await?; - let port = container.get_host_port_ipv4(5432).await?; - let url = format!( - "postgresql://postgres:postgres@127.0.0.1:{}/postgres", port - ); - let store = PgMemoryStore::connect(&url, 4, embedder.as_ref()).await?; - Ok(Self { container, store }) - } -} -``` - -- **Behavior notes**: - - Image `pgvector/pgvector:pg16` bundles pgvector into the official postgres:16 image. - - Pool size 4 is enough for tests without starving the container's default `max_connections = 100`. - - `ContainerAsync` is held for the whole test scope; drop tears down the container. - - A fake `TestEmbedder` in `common/test_embedder.rs` provides a deterministic hash-based embedding (no ONNX dependency in CI). - ---- - -## Test Plan - -### Unit tests (colocated in `src/`) - -Under `crates/vestige-core/src/storage/postgres/`: - -- `pool.rs` -- one test per `build_pool` branch: defaults, explicit `max_connections`, invalid URL returns `StoreError::Postgres`. -- `registry.rs` -- three tests: first-init writes row and alters typmod, reopen with same embedder returns Ok, reopen with different dimension returns `EmbeddingMismatch`. -- `search.rs` -- query-builder unit tests for parameter packing: empty text, null embedding, all three filters null, all three filters populated. -- `migrate_cli.rs` -- `SqliteToPostgresPlan::default` returns sane defaults; plan validation rejects empty URL. -- `reembed.rs` -- `ReembedPlan::no_change` returns `rows_updated == 0` when embedder matches registry (no network call). -- `config.rs` -- five tests covering: valid postgres config, valid sqlite config, unknown backend string, missing subsection, feature-gated postgres without feature compiled in. - -### Integration tests (in `tests/phase_2/`) - -Each file is a full integration test crate (`[[test]]` in workspace root Cargo). - -**`tests/phase_2/pg_trait_parity.rs`** - -- Declares the same test matrix as Phase 1's SQLite trait tests, parameterized over `impl MemoryStore`. -- Runs every method: `insert`, `get`, `update`, `delete`, `search`, `fts_search`, `vector_search`, `get_scheduling`, `update_scheduling`, `get_due_memories`, `add_edge`, `get_edges`, `remove_edge`, `get_neighbors`, `list_domains`, `get_domain`, `upsert_domain`, `delete_domain`, `classify`, `count`, `get_stats`, `vacuum`, `health_check`. -- Each test is written once as `async fn roundtrip_(store: &dyn MemoryStore)` and invoked from two wrappers, one for SQLite and one for Postgres. -- Acceptance: every method returns equal results (except for `Uuid` ordering in `list_domains` where the test sorts before comparing). - -**`tests/phase_2/pg_hybrid_search_rrf.rs`** - -- Inserts 20 memories with known content ("rust async trait", "postgres hnsw vector", "fastembed onnx model", ...). -- Case 1: pure FTS. `SearchQuery { text: Some("rust trait"), embedding: None, ... }` returns the three Rust-related rows in order; `fts_score` populated, `vector_score` null. -- Case 2: pure vector. `SearchQuery { text: None, embedding: Some(embed("rust trait")), ... }` returns the same three rows via cosine; `vector_score` populated, `fts_score` null. -- Case 3: hybrid. Both set -- top hit has both scores; `rrf_score >= 1/(60+1) + 1/(60+1) = 0.0328`. -- Case 4: domain filter. 10 memories tagged with `domains = ["dev"]`, 10 with `["home"]`. Query with `domains: Some(vec!["dev"])` returns only dev memories. -- Case 5: edge case -- empty FTS query plus an embedding behaves identically to `vector_search`; empty embedding plus FTS query behaves identically to `fts_search`. - -**`tests/phase_2/pg_migration_sqlite_to_postgres.rs`** - -- Populate a fresh SQLite with 10,000 memories (seeded RNG, deterministic content), 4,000 scheduling rows, 2,000 edges. -- Run `run_sqlite_to_postgres` with a test embedder. -- Assert: `count() == 10_000` on destination; spot-check 25 memories byte-for-byte (content, tags, metadata, domains, domain_scores). -- Assert: FSRS fields (`stability`, `difficulty`, `next_review`) preserved per memory. -- Assert: edges preserved by `(source_id, target_id, edge_type)`. -- Assert: re-running the migration is a no-op (`ON CONFLICT DO NOTHING` path); row count unchanged. - -**`tests/phase_2/pg_migration_reembed.rs`** - -- Start with a fresh store using `TestEmbedder768` (768-dim, hash `h1`). Insert 500 memories. -- Swap to `TestEmbedder1024` (1024-dim, hash `h2`). Run `run_reembed(store, Arc::new(TestEmbedder1024), ReembedPlan::default())`. -- Assert: `rows_updated == 500`; `embedding_model` now has `(name=TestEmbedder1024, dimension=1024, hash=h2)`. -- Assert: `SELECT DISTINCT vector_dims(embedding) FROM memories` returns only `1024`. -- Assert: HNSW index exists after reembed (`SELECT indexrelid FROM pg_indexes WHERE indexname = 'idx_memories_embedding_hnsw'`). -- Assert: memory IDs unchanged (compare pre/post id sets). -- Assert: a hybrid search using `TestEmbedder1024` returns results (post-reembed vectors are queryable). - -**`tests/phase_2/pg_config_parsing.rs`** - -- Parse six `vestige.toml` snippets: - - sqlite + fastembed -> `StorageConfig::Sqlite`. - - postgres + fastembed -> `StorageConfig::Postgres` with `max_connections = 10`. - - postgres with custom `max_connections = 25` and `acquire_timeout_secs = 60`. - - unknown backend `"mysql"` -> `ConfigError`. - - missing subsection `[storage.postgres]` while `backend = "postgres"` -> `ConfigError`. - - malformed URL (empty) -> `ConfigError::Invalid`. - -**`tests/phase_2/pg_concurrency.rs`** - -- Spawn 16 tasks, each inserting 100 memories in parallel for 1,600 total. -- Spawn 4 tasks concurrently running `search` queries; none should fail. -- Spawn 2 tasks concurrently running `update_scheduling` on overlapping IDs -- last write wins (MVCC), neither errors. -- Assert: all 1,600 rows present, no deadlocks, every task returns `Ok`. -- Run time < 10 seconds on a cold container. - -### Compile-time query verification - -- CI step: `cargo sqlx prepare --workspace --check` against a CI-provisioned Postgres (GitHub Actions / Forgejo Actions services block). Fails CI if any `query!` macro goes stale. -- Alternative offline run for contributors: `SQLX_OFFLINE=true cargo check -p vestige-core --features postgres-backend`. CI runs both forms to ensure `.sqlx/` is up to date. -- `.sqlx/` is committed to the repo. A `.gitattributes` entry marks it as `linguist-generated=true` so it doesn't inflate language stats. - -### Benchmarks - -Under `crates/vestige-core/benches/pg_hybrid_search.rs` (Criterion), gated by `postgres-backend`. - -- `pg_search_1k` -- populate 1,000 memories once per bench suite, measure `rrf_search` p50/p99 over 500 iterations. Target: p50 < 10ms, p99 < 30ms on a local container. -- `pg_search_100k` -- 100,000 memories. Target: p50 < 50ms, p99 < 150ms. Validates HNSW scaling. -- Testcontainer shared across both benches via `once_cell`. -- Bench entry in `vestige-core/Cargo.toml`: - -``` -[[bench]] -name = "pg_hybrid_search" -harness = false -required-features = ["postgres-backend"] -``` - ---- - -## Acceptance Criteria - -- [ ] `cargo build -p vestige-core --features postgres-backend` -- zero warnings. -- [ ] `cargo build -p vestige-core` (SQLite-only, default features) -- zero warnings; no Postgres symbols referenced. -- [ ] `cargo build -p vestige-mcp --features postgres-backend` -- zero warnings; `vestige` binary exposes the `migrate` subcommand. -- [ ] `cargo clippy --workspace --all-targets --all-features -- -D warnings` -- clean. -- [ ] `cargo sqlx prepare --workspace --check` -- returns success; `.sqlx/` is current. -- [ ] `cargo test -p vestige-core --features postgres-backend --test pg_trait_parity --test pg_hybrid_search_rrf --test pg_migration_sqlite_to_postgres --test pg_migration_reembed --test pg_config_parsing --test pg_concurrency` -- all green. -- [ ] Testcontainer spin-up p50 under 30 seconds on a developer laptop with a warm Docker daemon. -- [ ] `pg_search_100k` Criterion bench reports p50 < 50ms on reference hardware (logged in the ADR comment trail). -- [ ] `vestige migrate copy --from sqlite --to postgres` on a 10,000-memory corpus completes without data loss: row count parity, content byte-parity on a 1 percent sample, FSRS state preserved (stability, difficulty, reps, lapses, next_review), edge count parity. -- [ ] `vestige migrate reembed` with a dimension-changing embedder returns to a fully queryable state: HNSW present, `embedding_model` updated, no stale vectors, memory IDs untouched. -- [ ] Trait parity: every method on `MemoryStore` has at least one passing test against `PgMemoryStore`. -- [ ] Phase 1's existing SQLite suite continues to pass with zero changes required (Phase 2 is additive). -- [ ] The `postgres-backend` feature does not compile in SQLCipher (`encryption`) simultaneously (mutually exclusive at compile time, per project rule). - ---- - -## Rollback Notes - -- Every `*.up.sql` has a matching `*.down.sql` in `crates/vestige-core/migrations/postgres/`. `sqlx migrate revert` walks them in reverse order. Manual operator procedure: `sqlx migrate revert --database-url $URL --source crates/vestige-core/migrations/postgres`. -- `vestige migrate copy` is a one-way operation. The source SQLite DB is read-only during the run and untouched afterward; users retain their original file indefinitely. Recommended discipline: copy the SQLite file aside before starting, retain for 30 days. -- `vestige migrate reembed` is destructive to the `embedding` column. Recommended discipline: take a logical backup (`pg_dump --table=memories --table=embedding_model --table=scheduling`) before a reembed run. The tool prints that recommendation before starting and exits non-zero unless `--yes` is passed or the user is on a TTY that confirms. -- Feature-gate strategy: the default build remains SQLite-only. Downstream users pull `postgres-backend` explicitly: `cargo install --features postgres-backend vestige-mcp`. If the Postgres implementation fails in the field, users fall back to SQLite simply by flipping `vestige.toml`'s `[storage] backend = "sqlite"` and restarting. No data re-migration is needed if they retained their SQLite file. -- The `docs/runbook/postgres.md` deliverable (D16) captures this discipline as a one-page ops note. - ---- - -## Open Implementation Questions - -Each item has a recommendation. Ship that unless a reviewer objects. - -### Q1. CLI shape: subcommand split vs flag union - -- **Options**: (a) `vestige migrate copy --from sqlite --to postgres ...` and `vestige migrate reembed --model=...` (subcommand split); (b) `vestige migrate --from sqlite --to postgres ...` and `vestige migrate --reembed --model=...` under one `clap` command with disjoint flag groups (flag union). -- **RECOMMENDATION**: (a) subcommand split. The flag sets do not overlap and clap expresses the constraint more cleanly. The ADR string `vestige migrate --from sqlite --to postgres` can still be documented as a canonical alias by having `copy` accept it verbatim when `--from` is present. - -### Q2. Feature flag name - -- **Options**: `postgres-backend`, `postgres`, `backend-postgres`, `pg`. -- **RECOMMENDATION**: `postgres-backend`. Matches the ADR text and is explicit in `Cargo.toml` feature listings. - -### Q3. sqlx offline mode strategy - -- **Options**: (a) commit `.sqlx/` so downstream builds never need DATABASE_URL; (b) require `DATABASE_URL` at build time. -- **RECOMMENDATION**: (a). The repo already ships as a library; many downstream users will build from crates.io with no Postgres available. Committing `.sqlx/` costs ~100 kB. - -### Q4. HNSW rebuild strategy during reembed - -- **Options**: (a) `DROP INDEX; CREATE INDEX`; (b) `REINDEX INDEX CONCURRENTLY`; (c) `CREATE INDEX CONCURRENTLY` on a new name then swap. -- **RECOMMENDATION**: (a) by default for speed on empty / near-empty tables; expose `--concurrent-index` for large production corpora where locking the table is unacceptable. `REINDEX CONCURRENTLY` on pgvector HNSW is supported in pgvector 0.6+ but the community still reports edge cases with `maintenance_work_mem` -- skip unless a user explicitly opts in. - -### Q5. Connection pool sizing default - -- **Options**: 4, 10, 20, `cpus() * 2`. -- **RECOMMENDATION**: 10. Matches the PRD example, covers a single-operator load, and does not exhaust the default Postgres `max_connections = 100`. Configurable via `vestige.toml`. - -### Q6. Testcontainer image pinning - -- **Options**: (a) `pgvector/pgvector:pg16`; (b) `pgvector/pgvector:pg16.2-0.7.4` (exact tag); (c) maintain local Dockerfile. -- **RECOMMENDATION**: (b) pin exact. The float tag `pg16` has shipped breaking changes in the past (e.g., pg 16.0 to 16.1 interop). Pin to a specific pgvector minor and Postgres patch. CI bumps the tag via a single-line change. - -### Q7. Empty-text and null-embedding behavior in `search` - -- **Options**: (a) return an error if both are missing; (b) return an empty result; (c) return all memories sorted by `created_at DESC`. -- **RECOMMENDATION**: (a). A `search` call with no query is a bug in the caller; returning empty silently would hide the bug. The existing Phase 1 SQLite behavior (TBD but likely errors) is the tiebreaker. - -### Q8. `classify()` SQL vs Rust - -- **Options**: (a) compute cosine to all centroids in SQL (`SELECT id, 1 - (centroid <=> $1::vector) FROM domains ORDER BY ...`); (b) load centroids, compute in Rust. -- **RECOMMENDATION**: (a). Leverages pgvector's SIMD paths and avoids round-tripping centroid vectors. At Phase 4 scale (tens of centroids) the difference is marginal, but the SQL path is simpler and matches the rest of the backend. - -### Q9. FSRS `review_events` writes: trait method vs implicit on `update_scheduling` - -- **Options**: (a) add an explicit `record_review(memory_id, rating, prior, new)` method to the Phase 1 trait; (b) have `update_scheduling` write the event atomically. -- **RECOMMENDATION**: this is a Phase 1 question, not Phase 2. Phase 2 implements whichever Phase 1 chose. If Phase 1 missed it, Phase 2 raises a blocker rather than deciding alone. - -### Q10. `tsvector` weight for tags -- PRD used `array_to_tsvector`, we used `array_to_string` - -- **Options**: (a) `array_to_tsvector(tags)` (requires the `tsvector_extra` extension or similar); (b) `to_tsvector('english', array_to_string(tags, ' '))` (plain core Postgres). -- **RECOMMENDATION**: (b). Equivalent ranking, zero extra extensions. If a future tag matches a stopword (`"the"`), it gets dropped, but that is correct behavior for ranking. - -### Q11. `PgMemoryStore::connect` runs migrations automatically? - -- **Options**: (a) always run `sqlx::migrate!` on connect; (b) require the user to run `vestige migrate-schema` explicitly before starting the server. -- **RECOMMENDATION**: (a) during Phase 2; revisit in Phase 3 when the server binary exists. Developer ergonomics win now, and the migrations are idempotent. - -### Q12. Offline query cache freshness vs `sqlx-cli` version skew - -- **Options**: (a) pin `sqlx-cli` version in CI `actions/cache` step; (b) let CI install whatever version `sqlx` depends on. -- **RECOMMENDATION**: (a) pin to the same 0.8.x as the crate. `sqlx prepare` output changes between 0.7 and 0.8 and must match the runtime. - ---- - -## Sequencing - -The Phase 2 agent executes deliverables in this order; deliverables not listed can run in any order relative to each other. - -1. D1 (feature gate + Cargo deps) -- unblocks everything. -2. D7 (config) -- required to construct `PgMemoryStore`. -3. D4 (migrations SQL) -- required before any `query!` compiles. -4. D3 (pool) + D6 (registry) -- small, used by D2. -5. D2 (`PgMemoryStore` core + trait impl) -- the bulk of Phase 2. -6. D5 (RRF search) -- after D2; requires the trait to exist. -7. D12 (test harness) + parity and search tests -- validates D2 and D5 in isolation. -8. D8 (sqlite->pg migrate) + its integration test. -9. D9 (reembed) + its integration test. -10. D10 (CLI wiring). -11. D11 (`.sqlx/` offline cache) -- last, after SQL is frozen. -12. D15 (benches) + D16 (runbook) -- after acceptance tests pass. - -Each deliverable PR includes its own tests; the final Phase 2 PR stacks them (or lands as a single branch if the Phase 1 trait is stable enough to avoid rebase churn). - -### Critical Files for Implementation - -- /home/delandtj/prppl/vestige/crates/vestige-core/src/storage/postgres/mod.rs -- /home/delandtj/prppl/vestige/crates/vestige-core/migrations/postgres/0001_init.up.sql -- /home/delandtj/prppl/vestige/crates/vestige-core/src/storage/postgres/search.rs -- /home/delandtj/prppl/vestige/crates/vestige-core/src/storage/postgres/migrate_cli.rs -- /home/delandtj/prppl/vestige/crates/vestige-mcp/src/bin/cli.rs diff --git a/docs/plans/0002a-skeleton-and-feature-gate.md b/docs/plans/0002a-skeleton-and-feature-gate.md deleted file mode 100644 index 74032dc..0000000 --- a/docs/plans/0002a-skeleton-and-feature-gate.md +++ /dev/null @@ -1,554 +0,0 @@ -# Phase 2 Sub-Plan 0002a -- Skeleton and Feature Gate - -**Status**: Ready -**Depends on**: Phase 1 amendment (sub-plans `0001a-trait-rewrite.md` and -`0001b-sqlite-split.md`) merged. Specifically: -- `MemoryStore` trait declared with `#[trait_variant::make(MemoryStore: Send)]`, - generating a non-Send `LocalMemoryStore` companion trait. The - `pub use MemoryStore as LocalMemoryStore` alias from Phase 1 is gone. -- `crates/vestige-core/src/storage/sqlite.rs` has been split into - `crates/vestige-core/src/storage/sqlite/` with the same public surface. - -This sub-plan covers Phase 2 master-plan deliverables D1 and D2 only: -the `postgres-backend` Cargo feature gate and a compilable `PgMemoryStore` -skeleton whose trait method bodies are `todo!()`. No real Postgres code, no -migrations, no SQL. Later sub-plans (`0002b-pool-and-config.md`, -`0002c-migrations.md`, `0002d-store-impl-bodies.md`, ...) fill the bodies in. - -The success criterion is a clean build under both feature-flag configurations, -nothing more. - ---- - -## Context - -ADR 0002 D4 commits Phase 2 to a `crates/vestige-core/src/storage/postgres/` -directory from day one. The seven other files in that directory -(`pool.rs`, `migrations.rs`, `registry.rs`, `search.rs`, `migrate_cli.rs`, -`reembed.rs`) belong to subsequent sub-plans. This sub-plan creates only -`crates/vestige-core/src/storage/postgres/mod.rs` so the rest can be added -incrementally without breaking the build. - -Per ADR 0002 D2, `PgMemoryStore::connect` mirrors `SqliteMemoryStore::new`: -no `Embedder` argument. The pgvector typmod DDL -(`ALTER TABLE memories ALTER COLUMN embedding TYPE vector($N)`) lives inside -the trait method `register_model`, invoked by the caller after construction. -In this sub-plan `register_model` is a `todo!()` body; `0002c-migrations.md` -and `0002d-store-impl-bodies.md` provide the real implementation. - -The trait surface in `crates/vestige-core/src/storage/memory_store.rs` is the -source of truth for method signatures. Do NOT copy signatures from the master -plan -- they are stale in places (for example, master plan 0002 D2 lists -`remove_edge` as three-arg `(source, target, edge_type)`; the live trait has -two args `(source, target)`). - ---- - -## Cargo manifest changes - -Two optional crates and one new feature flag. Use `cargo add` per the global -CLAUDE.md preference; do not hand-edit `Cargo.toml`. - -```bash -cd crates/vestige-core - -cargo add sqlx@0.8 --optional --no-default-features \ - --features runtime-tokio,tls-rustls,postgres,uuid,chrono,json,migrate,macros - -cargo add pgvector@0.4 --optional --features sqlx -``` - -After both commands, open `crates/vestige-core/Cargo.toml` and add the -`postgres-backend` feature line in the `[features]` block. Place it after -the `metal` feature, before `[dependencies]`: - -```toml -# Postgres backend (mutually compilable with the SQLite backend; default OFF). -# Compile with: --features postgres-backend -postgres-backend = ["dep:sqlx", "dep:pgvector"] -``` - -Do NOT add `tokio-stream`, `futures`, `indicatif`, or `toml` in this sub-plan. -The master plan D1 lists them in the `postgres-backend` feature for -convenience, but their consumers (streaming migrate, progress bar, config -parsing) land in later sub-plans. Adding them here pulls dead weight into the -feature gate. - -Do NOT add the `vestige-mcp` pass-through feature in this sub-plan either. -The MCP crate gets its `postgres-backend` feature in `0002b-pool-and-config.md` -when `MemoryStoreConfig` lands and the binary needs a knob to pick a backend. - -Verify the diff to `crates/vestige-core/Cargo.toml` looks like this and only -this: - -```toml -[features] -# ...existing features unchanged... -postgres-backend = ["dep:sqlx", "dep:pgvector"] - -[dependencies] -# ...existing deps unchanged... -sqlx = { version = "0.8", default-features = false, features = [ - "runtime-tokio", "tls-rustls", "postgres", "uuid", "chrono", - "json", "migrate", "macros", -], optional = true } -pgvector = { version = "0.4", features = ["sqlx"], optional = true } -``` - -The exact ordering of the two new lines inside `[dependencies]` is not -significant; `cargo add` places them at the end. Leave the placement that -`cargo add` produces. - ---- - -## Storage module export - -Edit `crates/vestige-core/src/storage/mod.rs` to expose the new module behind -the feature flag. Two lines change. - -Add to the module-declaration block (after `mod sqlite;`): - -```rust -#[cfg(feature = "postgres-backend")] -mod postgres; -``` - -Add to the re-export block (after the `pub use sqlite::{ ... }` block): - -```rust -#[cfg(feature = "postgres-backend")] -pub use postgres::PgMemoryStore; -``` - -Nothing else in `storage/mod.rs` changes. The `Storage` alias still points at -`SqliteMemoryStore`; the SQLite re-export block is untouched. - ---- - -## Postgres module skeleton - -Create `crates/vestige-core/src/storage/postgres/mod.rs` with the full content -below. This is the only new file in this sub-plan. - -```rust -#![cfg(feature = "postgres-backend")] -//! Postgres-backed implementation of `MemoryStore`. -//! -//! Skeleton only. Every trait method is `todo!()`. Real bodies land in -//! subsequent Phase 2 sub-plans: -//! - `0002b-pool-and-config.md`: pool construction and config wiring -//! - `0002c-migrations.md`: sqlx migration files and `init`/`register_model` -//! - `0002d-store-impl-bodies.md`: CRUD, scheduling, edges, domains -//! - `0002e-hybrid-search.md`: RRF query and search bodies -//! -//! The directory grows companion files (`pool.rs`, `migrations.rs`, -//! `registry.rs`, `search.rs`, `migrate_cli.rs`, `reembed.rs`) in those -//! sub-plans; this skeleton only creates `mod.rs`. - -use chrono::{DateTime, Utc}; -use sqlx::PgPool; -use uuid::Uuid; - -use crate::storage::memory_store::{ - Domain, HealthStatus, LocalMemoryStore, MemoryEdge, MemoryRecord, MemoryStoreResult, - ModelSignature, SchedulingState, SearchQuery, SearchResult, StoreStats, -}; - -/// Postgres-backed implementation of `MemoryStore`. -/// -/// Cheaply cloneable. Methods take `&self`; interior state lives inside the -/// `PgPool` (which already provides `Sync` via `Arc` internally). -#[derive(Clone)] -pub struct PgMemoryStore { - pool: PgPool, - /// Embedding vector dimension. Set to 0 in the skeleton; populated by - /// `register_model` in `0002d-store-impl-bodies.md` once the pgvector - /// `ALTER COLUMN TYPE vector(N)` DDL lands. - embedding_dim: i32, -} - -impl PgMemoryStore { - /// Construct a new store from a connection URL. - /// - /// Mirrors `SqliteMemoryStore::new`: no `Embedder` argument. The pgvector - /// `ALTER TABLE memories ALTER COLUMN embedding TYPE vector($N)` DDL lives - /// inside `register_model`, not here. The caller (cognitive engine - /// bootstrap, migrate CLI, tests) invokes `register_model` after this - /// returns, identically to the SQLite path. - /// - /// Real body lands in `0002b-pool-and-config.md` (pool construction) and - /// `0002c-migrations.md` (initial migration run). - pub async fn connect(_url: &str, _max_connections: u32) -> MemoryStoreResult { - todo!("PgMemoryStore::connect lands in 0002b-pool-and-config.md") - } - - /// Low-level constructor for tests: supply an existing pool, skip migrate. - /// - /// Real body lands in `0002b-pool-and-config.md`. - pub async fn from_pool(_pool: PgPool) -> MemoryStoreResult { - todo!("PgMemoryStore::from_pool lands in 0002b-pool-and-config.md") - } - - /// Accessor used by migrate/reembed CLI. - pub fn pool(&self) -> &PgPool { - &self.pool - } - - /// Currently-registered vector dimension. Returns 0 until `register_model` - /// has been called (real body: `0002d-store-impl-bodies.md`). - pub fn embedding_dim(&self) -> i32 { - self.embedding_dim - } -} - -// trait_variant::make on the trait declaration generates two traits: -// - `MemoryStore` (Send-bound) -// - `LocalMemoryStore` (non-Send companion) -// The implementer writes `impl LocalMemoryStore for ...` plainly; the Send -// variant is generated automatically from the non-Send impl. -impl LocalMemoryStore for PgMemoryStore { - // --- Lifecycle --- - async fn init(&self) -> MemoryStoreResult<()> { - todo!("PgMemoryStore::init lands in 0002c-migrations.md") - } - - async fn health_check(&self) -> MemoryStoreResult { - todo!("PgMemoryStore::health_check lands in 0002d-store-impl-bodies.md") - } - - // --- Embedding model registry --- - async fn registered_model(&self) -> MemoryStoreResult> { - todo!("PgMemoryStore::registered_model lands in 0002d-store-impl-bodies.md") - } - - async fn register_model(&self, _sig: &ModelSignature) -> MemoryStoreResult<()> { - todo!("PgMemoryStore::register_model lands in 0002d-store-impl-bodies.md") - } - - // --- CRUD --- - async fn insert(&self, _record: &MemoryRecord) -> MemoryStoreResult { - todo!("PgMemoryStore::insert lands in 0002d-store-impl-bodies.md") - } - - async fn get(&self, _id: Uuid) -> MemoryStoreResult> { - todo!("PgMemoryStore::get lands in 0002d-store-impl-bodies.md") - } - - async fn update(&self, _record: &MemoryRecord) -> MemoryStoreResult<()> { - todo!("PgMemoryStore::update lands in 0002d-store-impl-bodies.md") - } - - async fn delete(&self, _id: Uuid) -> MemoryStoreResult<()> { - todo!("PgMemoryStore::delete lands in 0002d-store-impl-bodies.md") - } - - // --- Search --- - async fn search(&self, _query: &SearchQuery) -> MemoryStoreResult> { - todo!("PgMemoryStore::search lands in 0002e-hybrid-search.md") - } - - async fn fts_search( - &self, - _text: &str, - _limit: usize, - ) -> MemoryStoreResult> { - todo!("PgMemoryStore::fts_search lands in 0002e-hybrid-search.md") - } - - async fn vector_search( - &self, - _embedding: &[f32], - _limit: usize, - ) -> MemoryStoreResult> { - todo!("PgMemoryStore::vector_search lands in 0002e-hybrid-search.md") - } - - // --- FSRS Scheduling --- - async fn get_scheduling( - &self, - _memory_id: Uuid, - ) -> MemoryStoreResult> { - todo!("PgMemoryStore::get_scheduling lands in 0002d-store-impl-bodies.md") - } - - async fn update_scheduling(&self, _state: &SchedulingState) -> MemoryStoreResult<()> { - todo!("PgMemoryStore::update_scheduling lands in 0002d-store-impl-bodies.md") - } - - async fn get_due_memories( - &self, - _before: DateTime, - _limit: usize, - ) -> MemoryStoreResult> { - todo!("PgMemoryStore::get_due_memories lands in 0002d-store-impl-bodies.md") - } - - // --- Graph (spreading activation) --- - async fn add_edge(&self, _edge: &MemoryEdge) -> MemoryStoreResult<()> { - todo!("PgMemoryStore::add_edge lands in 0002d-store-impl-bodies.md") - } - - async fn get_edges( - &self, - _node_id: Uuid, - _edge_type: Option<&str>, - ) -> MemoryStoreResult> { - todo!("PgMemoryStore::get_edges lands in 0002d-store-impl-bodies.md") - } - - async fn remove_edge(&self, _source: Uuid, _target: Uuid) -> MemoryStoreResult<()> { - todo!("PgMemoryStore::remove_edge lands in 0002d-store-impl-bodies.md") - } - - async fn get_neighbors( - &self, - _node_id: Uuid, - _depth: usize, - ) -> MemoryStoreResult> { - todo!("PgMemoryStore::get_neighbors lands in 0002d-store-impl-bodies.md") - } - - // --- Domains (Phase 1: stubs return empty; full impl in Phase 4) --- - async fn list_domains(&self) -> MemoryStoreResult> { - todo!("PgMemoryStore::list_domains lands in 0002d-store-impl-bodies.md") - } - - async fn get_domain(&self, _id: &str) -> MemoryStoreResult> { - todo!("PgMemoryStore::get_domain lands in 0002d-store-impl-bodies.md") - } - - async fn upsert_domain(&self, _domain: &Domain) -> MemoryStoreResult<()> { - todo!("PgMemoryStore::upsert_domain lands in 0002d-store-impl-bodies.md") - } - - async fn delete_domain(&self, _id: &str) -> MemoryStoreResult<()> { - todo!("PgMemoryStore::delete_domain lands in 0002d-store-impl-bodies.md") - } - - async fn classify(&self, _embedding: &[f32]) -> MemoryStoreResult> { - todo!("PgMemoryStore::classify lands in 0002d-store-impl-bodies.md") - } - - // --- Bulk / Maintenance --- - async fn count(&self) -> MemoryStoreResult { - todo!("PgMemoryStore::count lands in 0002d-store-impl-bodies.md") - } - - async fn get_stats(&self) -> MemoryStoreResult { - todo!("PgMemoryStore::get_stats lands in 0002d-store-impl-bodies.md") - } - - async fn vacuum(&self) -> MemoryStoreResult<()> { - todo!("PgMemoryStore::vacuum lands in 0002d-store-impl-bodies.md") - } -} -``` - -Notes on the skeleton: - -- The file-level `#![cfg(feature = "postgres-backend")]` means the whole file - vanishes when the feature is off. The `mod postgres;` line in - `storage/mod.rs` is itself feature-gated, so this is belt-and-braces; both - gates are needed because the file-level attribute is what allows the file to - use `sqlx::PgPool` unconditionally inside it. -- `EmbeddingModelDescriptor` (a separate Postgres-internal type that the - master plan sketched on the struct) is dropped. The trait surface already - carries `ModelSignature` for the registry round-trip; the registry storage - layout is a private concern of `registry.rs`, which is added later. Keep - `PgMemoryStore` minimal until a real consumer needs the extra type. -- The struct only carries `pool` and `embedding_dim`. The model descriptor - field from the master plan sketch goes away with `EmbeddingModelDescriptor`. - If `register_model` later needs to cache the descriptor on the struct, it - can be added then; the skeleton does not speculate. -- The two trivial accessors (`pool`, `embedding_dim`) get real bodies. Every - other method is `todo!()` so it returns `!` and trivially coerces to the - declared return type at the type checker; this is what lets the build pass - with no error variants and no SQL. - ---- - -## Connect signature - -Per ADR 0002 D2: - -```rust -pub async fn connect(url: &str, max_connections: u32) -> MemoryStoreResult; -pub async fn from_pool(pool: PgPool) -> MemoryStoreResult; -``` - -No `&dyn Embedder` argument. This deliberately differs from master plan 0002, -which predates the Phase 1 freeze. The pgvector-specific DDL -(`ALTER TABLE memories ALTER COLUMN embedding TYPE vector($N)`) does not run -inside `connect`; it runs inside `register_model(&ModelSignature)`, which the -caller invokes after `connect` returns. - -In this sub-plan `register_model` is `todo!()`. The real body lands in -`0002d-store-impl-bodies.md` after `0002c-migrations.md` ships the -`0001_init.up.sql` migration that creates the `memories` table with a -placeholder `embedding vector` column (no typmod), against which -`register_model` later runs the typmod stamp. - ---- - -## Error variant additions: deferred - -`MemoryStoreError` does NOT gain `Postgres(sqlx::Error)` or -`Migrate(sqlx::migrate::MigrateError)` in this sub-plan. - -The reason is mechanical: `todo!()` evaluates to the never type `!`, which -coerces to any `MemoryStoreResult` regardless of the error variants -available. With every method body a `todo!()`, the skeleton has no expression -that needs to convert a `sqlx::Error` or `sqlx::migrate::MigrateError` into -`MemoryStoreError`. Adding the variants here would mean adding the -`#[cfg(feature = "postgres-backend")]` and `#[from]` plumbing to -`memory_store.rs` with no consumer yet -- dead code at every level except the -enum definition itself. - -`0002d-store-impl-bodies.md` introduces both variants in the same commit that -turns the first `todo!()` into a real `sqlx::query!` call. That keeps the -diff to `memory_store.rs` next to the first usage site, which is easier to -review than adding variants ahead of need. - -For reference, the variants that will be added in `0002d-store-impl-bodies.md` -look like this: - -```rust -#[cfg(feature = "postgres-backend")] -#[error("postgres error: {0}")] -Postgres(#[from] sqlx::Error), - -#[cfg(feature = "postgres-backend")] -#[error("postgres migration error: {0}")] -Migrate(#[from] sqlx::migrate::MigrateError), -``` - -Do not pre-add them here. - ---- - -## Verification - -Run these commands from the workspace root. All four must produce a clean -build, zero warnings on the diff-affected files, no test changes. - -```bash -# 1. Default features (SQLite backend, postgres-backend OFF). Must build. -cargo build --workspace --all-targets - -# 2. Workspace clippy with default features. Must be clean. -cargo clippy --workspace --all-targets -- -D warnings - -# 3. Postgres feature enabled. Must build. -cargo build -p vestige-core --features postgres-backend - -# 4. Clippy with postgres feature enabled. Must be clean. -cargo clippy -p vestige-core --features postgres-backend --all-targets -- -D warnings -``` - -Expected outcomes: - -- `cargo build --workspace --all-targets` finishes with no compilation of - `sqlx` or `pgvector` (both are optional, no consumer with default features). - The `postgres` module is excluded entirely via `#[cfg]`. -- `cargo build -p vestige-core --features postgres-backend` compiles `sqlx`, - `pgvector`, and `storage/postgres/mod.rs`. The build succeeds because every - trait method is `todo!()`; nothing actually runs SQL. -- Both `clippy` invocations pass with `-D warnings`. The `todo!()` macro does - not emit a `dead_code` lint by itself, and the trivial accessors are used by - later sub-plans (clippy on the postgres feature alone may flag them as - unused if you run with `--lib` only; the `--all-targets` form keeps tests - and benches in scope so this does not fire). -- If clippy flags `unused_variables` on the underscore-prefixed parameters in - the `todo!()` bodies, the underscore prefix is already the standard - suppression; if a future clippy version disagrees, add - `#[allow(unused_variables)]` to the impl block, not to each method. - -Tests are not modified in this sub-plan. The unit tests in -`memory_store.rs` (`memory_store_error_from_storage_error`, -`model_signature_serde_round_trip`, `memory_record_serde_round_trip`) keep -passing because no type they touch changes. - -Do NOT run `cargo test` against the postgres feature -- there is no Postgres -running and no test exercises `PgMemoryStore` yet. The build check is the -contract. - ---- - -## Acceptance criteria - -1. `crates/vestige-core/Cargo.toml` declares `sqlx = "0.8"` and - `pgvector = "0.4"` as optional dependencies with the exact feature sets - specified above. -2. `crates/vestige-core/Cargo.toml` declares `postgres-backend = ["dep:sqlx", - "dep:pgvector"]` and nothing else inside that feature. -3. `crates/vestige-mcp/Cargo.toml` is unchanged. -4. `crates/vestige-core/src/storage/mod.rs` adds exactly two - feature-gated lines: `mod postgres;` and `pub use postgres::PgMemoryStore;`. - No other change. -5. `crates/vestige-core/src/storage/postgres/mod.rs` exists and contains the - `PgMemoryStore` struct, `impl PgMemoryStore` block with real `pool` and - `embedding_dim` accessors and `todo!()` bodies for `connect` and - `from_pool`, and the full `impl LocalMemoryStore for PgMemoryStore` block - with `todo!()` for every trait method. -6. The trait impl method signatures match `memory_store.rs` byte-for-byte - (including `remove_edge(&self, source: Uuid, target: Uuid)` two-arg form, - not the three-arg form from the master plan). -7. `MemoryStoreError` is unchanged. -8. No other files in the crate are touched. No new files in - `storage/postgres/` besides `mod.rs`. -9. The four verification commands above all succeed. - ---- - -## Commit sequence - -One commit is recommended. The two changes (Cargo manifest + module skeleton) -do not compile in isolation: the manifest change without the skeleton produces -unused-optional-dep warnings, and the skeleton without the manifest change -fails to find `sqlx`. Splitting them adds no review value, since the second -commit is the one that has to compile cleanly. - -```bash -git add crates/vestige-core/Cargo.toml \ - crates/vestige-core/Cargo.lock \ - crates/vestige-core/src/storage/mod.rs \ - crates/vestige-core/src/storage/postgres/mod.rs - -git commit -m "feat(storage): scaffold postgres-backend feature and PgMemoryStore skeleton - -Adds the postgres-backend Cargo feature gating sqlx 0.8 and pgvector 0.4. -Introduces crates/vestige-core/src/storage/postgres/mod.rs with the -PgMemoryStore struct, connect/from_pool/pool/embedding_dim, and a trait impl -whose method bodies are todo!() pending later Phase 2 sub-plans. - -Builds clean with default features (SQLite only) and with --features -postgres-backend. No runtime behaviour change. - -Refs ADR 0002 D1, D2, D4." -``` - -If for any reason the manifest change must be reviewed separately (for -example, a security review of the sqlx version pin), split as: - -1. `cargo add` for sqlx and pgvector + manual feature line in Cargo.toml. - Build with default features will pass but `--features postgres-backend` - will fail (no module to satisfy the feature). This is acceptable for a - short-lived intermediate commit. -2. `storage/mod.rs` edits + `storage/postgres/mod.rs` creation. Both builds - pass. - -Default to the single-commit form unless asked to split. - ---- - -## Followups - -- `0002b-pool-and-config.md` adds `pool.rs`, `PostgresConfig`, and the - `vestige-mcp` `postgres-backend` pass-through feature. -- `0002c-migrations.md` adds `crates/vestige-core/migrations/postgres/` with - `0001_init.{up,down}.sql` and `0002_hnsw.{up,down}.sql`, plus - `postgres/migrations.rs` invoking `sqlx::migrate!`. `init()` body lands here. -- `0002d-store-impl-bodies.md` introduces the two `MemoryStoreError` variants - and replaces every `todo!()` in CRUD / scheduling / edges / domains / - registry with real `sqlx::query!` bodies. -- `0002e-hybrid-search.md` fills the three search bodies via the RRF query. diff --git a/docs/plans/0002b-pool-and-config.md b/docs/plans/0002b-pool-and-config.md deleted file mode 100644 index 9941413..0000000 --- a/docs/plans/0002b-pool-and-config.md +++ /dev/null @@ -1,886 +0,0 @@ -# Sub-plan 0002b -- Pool construction and VestigeConfig - -**Status**: Draft -**Master plan**: [0002-phase-2-postgres-backend.md](0002-phase-2-postgres-backend.md) -**ADR**: [0002-phase-2-execution.md](../adr/0002-phase-2-execution.md) -**Predecessor**: [0002a-skeleton-and-feature-gate.md](0002a-skeleton-and-feature-gate.md) - ---- - -## Context - -This sub-plan delivers two of the master plan's deliverables now that the -`0002a` skeleton has landed: - -- **D3** -- pool construction in - `crates/vestige-core/src/storage/postgres/pool.rs`. Replaces the `todo!()` - body of `PgMemoryStore::connect` with a real `PgPool` builder that reads a - `PostgresConfig`. Registry/migration calls remain `todo!()`; those are - filled in by sub-plans `0002c` (migrations) and `0002d` (store bodies + - registry). -- **D7** -- new module `crates/vestige-core/src/config.rs` containing - `VestigeConfig`, `StorageConfig`, `SqliteConfig`, `PostgresConfig`, - `EmbeddingsConfig`, plus a `ConfigError` enum and a loader that reads - `vestige.toml`. The loader is wired into `vestige-mcp` so the running - server picks SQLite or Postgres at startup based on the config file. - -After this sub-plan: - -- `cargo build` (default features, no `postgres-backend`) compiles and the - MCP server still defaults to SQLite when no `vestige.toml` is present. -- `cargo build --features postgres-backend` compiles, with - `PgMemoryStore::connect` now wiring through `pool.rs` (registry/migration - still `todo!()` until `0002c` and `0002d`). -- A `vestige.toml` example can be round-tripped through - `VestigeConfig::load` in a unit test. - -This sub-plan deliberately does NOT: - -- Add migrations (`0002c`). -- Fill in real CRUD/search bodies on `PgMemoryStore` (`0002d`, `0002e`). -- Add env-var override support (Phase 3 concern, called out in master plan - D7 behaviour notes). - ---- - -## Dependencies - -- `0002a-skeleton-and-feature-gate.md` must be merged. That sub-plan creates - `crates/vestige-core/src/storage/postgres/mod.rs` with: - - `PgMemoryStore` struct holding `pool: PgPool`. - - `PgMemoryStore::connect(url: &str, max_connections: u32) -> MemoryStoreResult` - body = `todo!()`. - - `PgMemoryStore::from_pool(pool: PgPool) -> MemoryStoreResult` - body = `todo!()`. - - The trait impl block with all methods routed to `todo!()`. - - The `postgres-backend` feature gate on the module declaration in - `storage/mod.rs`. - -This sub-plan extends those bodies and adds two siblings: `pool.rs` and -`registry.rs` (the latter is a stub here, real body in `0002d`). - ---- - -## Audit step (do this first) - -Before adding `config.rs`, confirm there is no existing top-level config -loader. Run from the repo root: - -```bash -rg -nF 'VestigeConfig' crates/ -rg -nF 'toml::from_str' crates/ -rg -n '#\[derive.*Deserialize.*\]' crates/vestige-core/src/ -``` - -If a `VestigeConfig` struct already exists from Phase 1, treat the "Config -module" section below as additive: extend the existing struct rather than -creating a new file. The cross-cut additions in that case are: - -1. Add the `StorageConfig` enum (gated and ungated branches). -2. Add `SqliteConfig`, `PostgresConfig`. -3. Add the `default_path()` helper if missing. -4. Add `ConfigError` if a different error enum is used today (rename/extend - instead of duplicating). - -As of the audit at the time of this writing, no `VestigeConfig` exists in -`vestige-core`. `directories::ProjectDirs` is already used in -`vestige-core/src/embeddings/local.rs` and in -`vestige-mcp/src/protocol/auth.rs`, so the `directories` crate is already a -workspace dependency -- no new dep there. - ---- - -## Cargo manifest additions - -Add `toml` to `vestige-core`. `serde` and `thiserror` are already present -from Phase 1; `directories` is already a transitive dep but we add it -explicitly so `default_path()` is supported. - -```bash -cd crates/vestige-core -cargo add toml@0.8 -cargo add directories@5 -``` - -No new deps on `vestige-mcp`; it already depends on `vestige-core`. - -`sqlx` is already added by `0002a` behind the `postgres-backend` feature -with `runtime-tokio`, `tls-rustls`, `postgres`, `uuid`, `chrono`, -`json`, `macros`, `migrate` features. The pool module only uses what is -already pulled in. - ---- - -## Config module - -**File**: `crates/vestige-core/src/config.rs` (new). -**Re-exported** from `crates/vestige-core/src/lib.rs` as `pub mod config;` plus -`pub use config::{VestigeConfig, StorageConfig, SqliteConfig, EmbeddingsConfig, ConfigError};` -and `#[cfg(feature = "postgres-backend")] pub use config::PostgresConfig;`. - -Full content: - -```rust -//! Vestige top-level configuration. -//! -//! Loaded from `~/.vestige/vestige.toml` by default; the path is overridable -//! via `VestigeConfig::load(Some(&path))`. Parsing uses serde + toml; the -//! `[storage]` section is internally-tagged on a `backend` field so a single -//! enum dispatch picks SQLite or Postgres. - -use std::path::{Path, PathBuf}; - -use serde::Deserialize; - -/// Top-level configuration as parsed from `vestige.toml`. -#[derive(Debug, Clone, Deserialize, Default)] -#[serde(default, deny_unknown_fields)] -pub struct VestigeConfig { - pub embeddings: EmbeddingsConfig, - pub storage: StorageConfig, - /// Reserved for Phase 3. Empty in Phase 2. - pub server: ServerConfig, - /// Reserved for Phase 3. Empty in Phase 2. - pub auth: AuthConfig, -} - -/// Embedding provider selection. -#[derive(Debug, Clone, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct EmbeddingsConfig { - /// Provider key. Phase 2 ships `"fastembed"` only. - pub provider: String, - /// Model name. For fastembed this is e.g. `"nomic-ai/nomic-embed-text-v1.5"`. - pub model: String, -} - -impl Default for EmbeddingsConfig { - fn default() -> Self { - Self { - provider: "fastembed".to_string(), - model: crate::DEFAULT_EMBEDDING_MODEL.to_string(), - } - } -} - -/// Storage backend selection. Internally tagged on the `backend` field: -/// -/// ```toml -/// [storage] -/// backend = "sqlite" -/// -/// [storage.sqlite] -/// path = "/home/user/.vestige/vestige.db" -/// ``` -/// -/// or, when compiled with `--features postgres-backend`: -/// -/// ```toml -/// [storage] -/// backend = "postgres" -/// -/// [storage.postgres] -/// url = "postgres://vestige:secret@localhost:5432/vestige" -/// max_connections = 10 -/// acquire_timeout_secs = 30 -/// ``` -#[derive(Debug, Clone, Deserialize)] -#[serde(tag = "backend", rename_all = "lowercase", deny_unknown_fields)] -pub enum StorageConfig { - Sqlite(SqliteConfig), - #[cfg(feature = "postgres-backend")] - Postgres(PostgresConfig), -} - -impl Default for StorageConfig { - fn default() -> Self { - StorageConfig::Sqlite(SqliteConfig::default()) - } -} - -/// SQLite backend configuration. -#[derive(Debug, Clone, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct SqliteConfig { - /// Path to the `vestige.db` file. If unset, the SqliteMemoryStore - /// constructor picks its platform default location. - #[serde(default)] - pub path: Option, -} - -impl Default for SqliteConfig { - fn default() -> Self { - Self { path: None } - } -} - -/// Postgres backend configuration. Only present when the `postgres-backend` -/// Cargo feature is enabled. -#[cfg(feature = "postgres-backend")] -#[derive(Debug, Clone, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct PostgresConfig { - /// `postgres://user:pass@host:port/db` -- forwarded to - /// `PgConnectOptions::from_str`. - pub url: String, - /// Pool size. Default `10`. - #[serde(default)] - pub max_connections: Option, - /// Acquire timeout in seconds. Default `30`. Set above 30 so - /// testcontainer-based test fixtures do not race. - #[serde(default)] - pub acquire_timeout_secs: Option, -} - -/// Reserved for Phase 3 (bind address, ports, TLS). -#[derive(Debug, Clone, Default, Deserialize)] -#[serde(default, deny_unknown_fields)] -pub struct ServerConfig {} - -/// Reserved for Phase 3 (API keys, claims). -#[derive(Debug, Clone, Default, Deserialize)] -#[serde(default, deny_unknown_fields)] -pub struct AuthConfig {} - -/// Errors raised while locating, reading, or parsing `vestige.toml`. -#[derive(Debug, thiserror::Error)] -pub enum ConfigError { - #[error("config io: {0}")] - Io(#[from] std::io::Error), - #[error("config toml: {0}")] - Toml(#[from] toml::de::Error), - #[error("config dir: could not locate user home")] - NoHome, - #[error("invalid config: {0}")] - Invalid(String), -} - -impl VestigeConfig { - /// Load config from `path` or from `default_path()` when `None`. - /// - /// Returns `VestigeConfig::default()` (SQLite + fastembed defaults) when - /// the file does not exist. Any other I/O or parse failure is surfaced - /// as a `ConfigError`. - pub fn load(path: Option<&Path>) -> Result { - let resolved: PathBuf = match path { - Some(p) => p.to_path_buf(), - None => Self::default_path()?, - }; - - match std::fs::read_to_string(&resolved) { - Ok(text) => { - let cfg: VestigeConfig = toml::from_str(&text)?; - cfg.validate()?; - Ok(cfg) - } - Err(e) if e.kind() == std::io::ErrorKind::NotFound => { - Ok(Self::default()) - } - Err(e) => Err(ConfigError::Io(e)), - } - } - - /// `~/.vestige/vestige.toml`. The directory is NOT created here; loading - /// a missing file falls back to defaults. - pub fn default_path() -> Result { - let dirs = directories::ProjectDirs::from("", "vestige", "vestige") - .ok_or(ConfigError::NoHome)?; - // ProjectDirs::config_dir() varies per OS. Vestige convention is - // ~/.vestige/vestige.toml on Linux/macOS regardless of XDG, so we - // build the path off the home dir explicitly. - let home = directories::UserDirs::new() - .ok_or(ConfigError::NoHome)? - .home_dir() - .to_path_buf(); - let _ = dirs; // keep the dep wired; future Phase 3 may use it - Ok(home.join(".vestige").join("vestige.toml")) - } - - /// Light cross-field validation. Heavy validation (URL parsing, - /// directory existence) is left to the backend constructors. - fn validate(&self) -> Result<(), ConfigError> { - if self.embeddings.provider.is_empty() { - return Err(ConfigError::Invalid( - "embeddings.provider must not be empty".into(), - )); - } - if self.embeddings.model.is_empty() { - return Err(ConfigError::Invalid( - "embeddings.model must not be empty".into(), - )); - } - match &self.storage { - StorageConfig::Sqlite(_) => {} - #[cfg(feature = "postgres-backend")] - StorageConfig::Postgres(cfg) => { - if cfg.url.is_empty() { - return Err(ConfigError::Invalid( - "storage.postgres.url must not be empty".into(), - )); - } - } - } - Ok(()) - } -} -``` - -### Serde behaviour with `postgres-backend` off - -`StorageConfig` is generated by serde only for the variants that are -compiled in. When `postgres-backend` is off and the user writes: - -```toml -[storage] -backend = "postgres" - -[storage.postgres] -url = "..." -``` - -serde returns a `toml::de::Error` of the form -`unknown variant `postgres`, expected `sqlite``. That error path goes -through `From for ConfigError`, surfacing as -`ConfigError::Toml(..)`. The MCP server prints this once at startup and -exits with a non-zero code; there is no panic. - -To make the error friendlier we wrap that specific case in a clearer -message via a thin post-parse check. Add this small helper after parsing -in `load()`: - -```rust -// (Inside the Ok(text) arm in load(), wrapping the parse step.) -let cfg: VestigeConfig = match toml::from_str(&text) { - Ok(c) => c, - Err(e) => { - let msg = e.to_string(); - if msg.contains("unknown variant `postgres`") { - return Err(ConfigError::Invalid( - "storage.backend = \"postgres\" requires building with --features postgres-backend".into(), - )); - } - return Err(ConfigError::Toml(e)); - } -}; -``` - -This keeps the strict default deny_unknown_fields behaviour while giving the -user a one-line action item. - ---- - -## Pool module - -**File**: `crates/vestige-core/src/storage/postgres/pool.rs` (new). - -```rust -#![cfg(feature = "postgres-backend")] - -//! `PgPool` construction for the Postgres backend. -//! -//! Pool defaults follow ADR 0002 D2 + master plan D3: -//! - max_connections = 10 -//! - acquire_timeout = 30s (must exceed testcontainer warmup) -//! - idle_timeout = 600s -//! - max_lifetime = 1800s -//! - test_before_acquire = false (cheap queries; saves a roundtrip) - -use std::str::FromStr; -use std::time::Duration; - -use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; -use sqlx::{ConnectOptions, PgPool}; - -use crate::config::PostgresConfig; -use crate::storage::memory_store::{MemoryStoreError, MemoryStoreResult}; - -const DEFAULT_MAX_CONNECTIONS: u32 = 10; -const DEFAULT_ACQUIRE_TIMEOUT_SECS: u64 = 30; -const IDLE_TIMEOUT_SECS: u64 = 600; -const MAX_LIFETIME_SECS: u64 = 1800; -const STATEMENT_CACHE_CAPACITY: usize = 256; - -/// Build a Postgres connection pool from a `PostgresConfig`. Does NOT run -/// migrations or stamp the embedding registry; those are the caller's job -/// (`PgMemoryStore::connect`). -pub async fn build_pool(cfg: &PostgresConfig) -> MemoryStoreResult { - let opts = PgConnectOptions::from_str(&cfg.url) - .map_err(MemoryStoreError::from)? - .application_name("vestige") - .statement_cache_capacity(STATEMENT_CACHE_CAPACITY) - .log_statements(tracing::log::LevelFilter::Debug); - - let max_conn = cfg.max_connections.unwrap_or(DEFAULT_MAX_CONNECTIONS); - let acquire = cfg - .acquire_timeout_secs - .unwrap_or(DEFAULT_ACQUIRE_TIMEOUT_SECS); - - let pool = PgPoolOptions::new() - .max_connections(max_conn) - .min_connections(0) - .acquire_timeout(Duration::from_secs(acquire)) - .idle_timeout(Some(Duration::from_secs(IDLE_TIMEOUT_SECS))) - .max_lifetime(Some(Duration::from_secs(MAX_LIFETIME_SECS))) - .test_before_acquire(false) - .connect_with(opts) - .await - .map_err(MemoryStoreError::from)?; - - Ok(pool) -} -``` - -### Wiring into `PgMemoryStore::connect` - -In `crates/vestige-core/src/storage/postgres/mod.rs`, replace the -`todo!()` body left by `0002a` for `connect` and `from_pool` with: - -```rust -// In crates/vestige-core/src/storage/postgres/mod.rs - -use sqlx::PgPool; - -use crate::config::PostgresConfig; -use crate::storage::memory_store::{MemoryStoreError, MemoryStoreResult}; - -mod pool; -mod registry; // see "Registry stub" section below - -pub struct PgMemoryStore { - pool: PgPool, -} - -impl PgMemoryStore { - /// Convenience constructor matching `SqliteMemoryStore::new` shape. - /// Takes a URL + pool size for the common case. - pub async fn connect(url: &str, max_connections: u32) -> MemoryStoreResult { - let cfg = PostgresConfig { - url: url.to_string(), - max_connections: Some(max_connections), - acquire_timeout_secs: None, - }; - Self::connect_with(&cfg).await - } - - /// Full-config constructor. - pub async fn connect_with(cfg: &PostgresConfig) -> MemoryStoreResult { - let pool = pool::build_pool(cfg).await?; - Self::from_pool(pool).await - } - - /// Construct from an already-built pool (used by tests and the migrate - /// CLI to share a pool across operations). - pub async fn from_pool(pool: PgPool) -> MemoryStoreResult { - // Migrations are added by 0002c. - // todo!("run sqlx::migrate! once 0002c lands") - registry::ensure_registry_stub(&pool).await?; - Ok(Self { pool }) - } -} -``` - -`connect_with` is the long-lived API; `connect` becomes a thin shim that -stays compatible with the master-plan-mandated signature. - -### Registry stub - -**File**: `crates/vestige-core/src/storage/postgres/registry.rs` (new, stub). - -```rust -#![cfg(feature = "postgres-backend")] - -//! Embedding registry. Real body lands in sub-plan 0002d. - -use sqlx::PgPool; - -use crate::storage::memory_store::MemoryStoreResult; - -/// Placeholder. Real implementation in 0002d reads/writes `embedding_model` -/// and stamps `ALTER TABLE memories ALTER COLUMN embedding TYPE vector($N)`. -pub(crate) async fn ensure_registry_stub(_pool: &PgPool) -> MemoryStoreResult<()> { - // Intentionally a no-op until 0002c lands the table + 0002d lands the - // real body. Leaving this as todo!() would crash the MCP server at - // startup the moment a user switches `backend = "postgres"`, which is - // not what we want for the build verification step in this sub-plan. - Ok(()) -} -``` - -The no-op keeps `cargo build --features postgres-backend` not just -compiling but also allowing the MCP server to *boot* against a Postgres -URL pointing at an already-migrated database (the local-dev-postgres-setup -docs cover bringing up such a DB by hand). Real init lands in `0002d`. - ---- - -## Error variants - -**File**: `crates/vestige-core/src/storage/memory_store.rs` (edit). - -The Phase 1 enum `MemoryStoreError` gains two feature-gated variants. These -were deferred in `0002a` and become required as soon as `pool.rs` calls -`.map_err(MemoryStoreError::from)` on `sqlx::Error`. - -```rust -// Within enum MemoryStoreError { ... } in memory_store.rs - -#[cfg(feature = "postgres-backend")] -#[error("postgres error: {0}")] -Postgres(#[from] sqlx::Error), - -#[cfg(feature = "postgres-backend")] -#[error("postgres migration error: {0}")] -Migrate(#[from] sqlx::migrate::MigrateError), -``` - -Both use thiserror's `#[from]` attribute so the `?` operator works in -`pool.rs`, the migrate module (`0002c`), and registry code (`0002d`). -Default-features build (no `postgres-backend`) sees neither variant; the -enum stays exhaustive on stable. - -If clippy fires on `non_exhaustive` due to the gated variants, add -`#[non_exhaustive]` on the enum. That has no caller-side effect since the -enum is constructed only inside the crate. - ---- - -## vestige-mcp wiring - -### Cargo feature passthrough - -**File**: `crates/vestige-mcp/Cargo.toml` (edit). - -Add a feature that forwards through to `vestige-core`. Default features in -`vestige-mcp` stay unchanged. - -```toml -[features] -default = ["embeddings", "vector-search"] -embeddings = ["vestige-core/embeddings"] -vector-search = ["vestige-core/vector-search"] -postgres-backend = ["vestige-core/postgres-backend"] -``` - -Verify with: - -```bash -cargo build -p vestige-mcp --features postgres-backend -``` - -### Backend dispatch at startup - -**File**: `crates/vestige-mcp/src/main.rs` (edit around the existing -`Storage::new(storage_path)` call -- see audit note above; in the current -worktree this is around line 285). - -The current code is roughly: - -```rust -let storage_path = match prepare_storage_path(config.data_dir) { ... }; -let storage = match Storage::new(storage_path) { ... }; -``` - -Replace that with a dispatch driven by `VestigeConfig`: - -```rust -use std::sync::Arc; - -use vestige_core::config::{StorageConfig, VestigeConfig}; -use vestige_core::storage::SqliteMemoryStore; -#[cfg(feature = "postgres-backend")] -use vestige_core::storage::postgres::PgMemoryStore; -use vestige_core::storage::MemoryStore; - -// Earlier: still call prepare_storage_path to honour --data-dir override. -let storage_path = match prepare_storage_path(config.data_dir.clone()) { ... }; - -// New: load vestige.toml (or fall back to defaults). -let vestige_cfg = match VestigeConfig::load(config.config_path.as_deref()) { - Ok(c) => c, - Err(e) => { - eprintln!("vestige: failed to load config: {e}"); - std::process::exit(2); - } -}; - -let storage: Arc = match &vestige_cfg.storage { - StorageConfig::Sqlite(sqlite_cfg) => { - // CLI flag --data-dir wins over the config file path. - let path = storage_path.clone().or_else(|| sqlite_cfg.path.clone()); - let s = SqliteMemoryStore::new(path).unwrap_or_else(|e| { - eprintln!("vestige: sqlite init failed: {e}"); - std::process::exit(3); - }); - Arc::new(s) - } - #[cfg(feature = "postgres-backend")] - StorageConfig::Postgres(pg_cfg) => { - let s = PgMemoryStore::connect_with(pg_cfg).await.unwrap_or_else(|e| { - eprintln!("vestige: postgres init failed: {e}"); - std::process::exit(3); - }); - Arc::new(s) - } -}; -``` - -The `config_path: Option` field on the local `Config` (or -clap-derived `Args`) struct must be added if not present; it accepts -`--config `. Default behaviour (no flag) goes through -`VestigeConfig::default_path()`. - -If the existing main wires `Storage` through a concrete type rather than -`Arc`, the dispatch above lives behind a helper: - -```rust -async fn build_store(cfg: &VestigeConfig, cli_path: Option) - -> Result, anyhow::Error> -{ ... } -``` - -and the caller chains `.into()` as needed. Phase 1 already moved -cognitive modules to `Arc` so this should be a pure -substitution; if a concrete-type holdout is found, fix it locally in this -sub-plan (separate commit) rather than punting. - ---- - -## vestige.toml example - -The canonical example to ship in `docs/` (Phase 2 docs land in `0002i`, -runbook), shown here for reference and used verbatim by the unit test -below. - -```toml -# vestige.toml -- top-level configuration -# -# Default location: ~/.vestige/vestige.toml -# Override: vestige-mcp --config /path/to/vestige.toml - -[embeddings] -provider = "fastembed" -model = "nomic-ai/nomic-embed-text-v1.5" - -# --- SQLite backend (default) --- -[storage] -backend = "sqlite" - -[storage.sqlite] -path = "/home/user/.vestige/vestige.db" - -# --- Postgres backend (requires --features postgres-backend) --- -# [storage] -# backend = "postgres" -# -# [storage.postgres] -# url = "postgres://vestige:secret@localhost:5432/vestige" -# max_connections = 10 -# acquire_timeout_secs = 30 - -[server] -# Reserved for Phase 3 (bind address, ports, TLS). - -[auth] -# Reserved for Phase 3 (API keys, claims). -``` - ---- - -## Verification - -Run all of these from the repo root. The first three are the gates that -must pass before this sub-plan is considered done. - -### 1. Default build (no Postgres) - -```bash -cargo build -p vestige-core -cargo build -p vestige-mcp -cargo test -p vestige-core --lib -``` - -Expected: clean build. `VestigeConfig::default()` selects SQLite; the MCP -server boots the same way it did pre-sub-plan. - -### 2. Postgres-feature build - -```bash -cargo build -p vestige-core --features postgres-backend -cargo build -p vestige-mcp --features postgres-backend -``` - -Expected: clean build. `PgMemoryStore::connect_with` resolves to -`pool::build_pool` + `registry::ensure_registry_stub`; no `todo!()` is -reachable on the build path. `connect` and `from_pool` are exported. - -### 3. Clippy across both feature sets - -```bash -cargo clippy -p vestige-core -- -D warnings -cargo clippy -p vestige-core --features postgres-backend -- -D warnings -cargo clippy -p vestige-mcp --features postgres-backend -- -D warnings -``` - -### 4. Unit test: round-trip the example - -Add this test to `crates/vestige-core/src/config.rs`: - -```rust -#[cfg(test)] -mod tests { - use super::*; - - const EXAMPLE_SQLITE: &str = r#" -[embeddings] -provider = "fastembed" -model = "nomic-ai/nomic-embed-text-v1.5" - -[storage] -backend = "sqlite" - -[storage.sqlite] -path = "/home/user/.vestige/vestige.db" -"#; - - #[cfg(feature = "postgres-backend")] - const EXAMPLE_POSTGRES: &str = r#" -[embeddings] -provider = "fastembed" -model = "nomic-ai/nomic-embed-text-v1.5" - -[storage] -backend = "postgres" - -[storage.postgres] -url = "postgres://vestige:secret@localhost:5432/vestige" -max_connections = 10 -acquire_timeout_secs = 30 -"#; - - #[test] - fn parses_sqlite_example() { - let cfg: VestigeConfig = toml::from_str(EXAMPLE_SQLITE).expect("parse"); - match cfg.storage { - StorageConfig::Sqlite(s) => assert!(s.path.is_some()), - #[cfg(feature = "postgres-backend")] - StorageConfig::Postgres(_) => panic!("wrong variant"), - } - assert_eq!(cfg.embeddings.provider, "fastembed"); - } - - #[cfg(feature = "postgres-backend")] - #[test] - fn parses_postgres_example() { - let cfg: VestigeConfig = toml::from_str(EXAMPLE_POSTGRES).expect("parse"); - match cfg.storage { - StorageConfig::Postgres(p) => { - assert_eq!(p.url, "postgres://vestige:secret@localhost:5432/vestige"); - assert_eq!(p.max_connections, Some(10)); - assert_eq!(p.acquire_timeout_secs, Some(30)); - } - StorageConfig::Sqlite(_) => panic!("wrong variant"), - } - } - - #[cfg(not(feature = "postgres-backend"))] - #[test] - fn rejects_postgres_when_feature_off() { - let toml_text = r#" -[storage] -backend = "postgres" - -[storage.postgres] -url = "postgres://x/y" -"#; - let res: Result = toml::from_str(toml_text); - assert!(res.is_err(), "must fail without postgres-backend feature"); - } - - #[test] - fn defaults_pick_sqlite() { - let cfg = VestigeConfig::default(); - assert!(matches!(cfg.storage, StorageConfig::Sqlite(_))); - } - - #[test] - fn load_missing_file_returns_default() { - let tmp = std::env::temp_dir().join("vestige-no-such-file.toml"); - let _ = std::fs::remove_file(&tmp); - let cfg = VestigeConfig::load(Some(&tmp)).expect("missing file is OK"); - assert!(matches!(cfg.storage, StorageConfig::Sqlite(_))); - } - - #[test] - fn load_roundtrip_from_disk() { - let tmp = std::env::temp_dir().join("vestige-roundtrip.toml"); - std::fs::write(&tmp, EXAMPLE_SQLITE).unwrap(); - let cfg = VestigeConfig::load(Some(&tmp)).expect("load"); - assert!(matches!(cfg.storage, StorageConfig::Sqlite(_))); - let _ = std::fs::remove_file(&tmp); - } -} -``` - -Run: - -```bash -cargo test -p vestige-core --lib config:: -cargo test -p vestige-core --lib config:: --features postgres-backend -``` - -### 5. Smoke: server boots with default config - -```bash -# default build, no vestige.toml on disk -cargo run -p vestige-mcp -- --help -# should print help, no panic -``` - ---- - -## Acceptance criteria - -- [ ] `cargo build -p vestige-core` (default features) succeeds. -- [ ] `cargo build -p vestige-core --features postgres-backend` succeeds. -- [ ] `cargo build -p vestige-mcp` (default features) succeeds. -- [ ] `cargo build -p vestige-mcp --features postgres-backend` succeeds. -- [ ] `cargo clippy` with and without `postgres-backend` is clean on both - crates. -- [ ] `crates/vestige-core/src/config.rs` exists, exposes - `VestigeConfig`, `StorageConfig`, `SqliteConfig`, `EmbeddingsConfig`, - `ConfigError`, plus `PostgresConfig` when the feature is on. -- [ ] `VestigeConfig::load(None)` returns `Ok(default)` when - `~/.vestige/vestige.toml` is missing. -- [ ] `VestigeConfig::load(Some(&path))` round-trips both the SQLite and - Postgres example blocks above. -- [ ] With `postgres-backend` off, parsing `backend = "postgres"` returns - a clear `ConfigError::Invalid` mentioning the feature flag, NOT a - panic. -- [ ] `crates/vestige-core/src/storage/postgres/pool.rs` exists, - implementing `build_pool(&PostgresConfig) -> MemoryStoreResult` - with the documented defaults. -- [ ] `PgMemoryStore::connect`, `connect_with`, and `from_pool` all wire - through `pool::build_pool`. None of them is `todo!()`. The registry - step is a no-op stub documented as filled in by `0002d`. -- [ ] `MemoryStoreError::Postgres(sqlx::Error)` and - `MemoryStoreError::Migrate(sqlx::migrate::MigrateError)` exist - behind `#[cfg(feature = "postgres-backend")]` with `#[from]`. -- [ ] `vestige-mcp` has a `postgres-backend` feature that forwards to - `vestige-core/postgres-backend`. -- [ ] `vestige-mcp/src/main.rs` selects SQLite vs Postgres at startup - based on `VestigeConfig`. SQLite is the default when no config file - is present. -- [ ] Unit tests in the "Verification" section pass on both feature sets. - ---- - -## Out of scope (handled by other sub-plans) - -- Migrations (`crates/vestige-core/migrations/postgres/*.sql`) -- `0002c`. -- Real `PgMemoryStore` CRUD/search/scheduling/edges bodies -- `0002d`, - `0002e`. -- `ensure_registry` real body with `ALTER COLUMN TYPE vector(N)` -- `0002d`. -- `vestige migrate --from sqlite --to postgres` CLI -- `0002f`. -- Re-embed flow -- `0002g`. -- Env-var override (`VESTIGE_POSTGRES_URL`, etc.) -- Phase 3. -- RLS, multi-tenant column population -- Phase 3. diff --git a/docs/plans/0002c-migrations.md b/docs/plans/0002c-migrations.md deleted file mode 100644 index 78b6ac6..0000000 --- a/docs/plans/0002c-migrations.md +++ /dev/null @@ -1,1119 +0,0 @@ -# Phase 2 Sub-plan 0002c: sqlx Migrations - -**Status**: Draft -**Depends on**: `0002a-skeleton-and-feature-gate.md` (PgMemoryStore skeleton, error variants), `0002b-pool-and-config.md` (PgPool builder, PostgresConfig) -**Related**: docs/adr/0002-phase-2-execution.md (D7 multi-tenancy reservation, D8 codebase column), docs/plans/0002-phase-2-postgres-backend.md (D4 master SQL), docs/plans/local-dev-postgres-setup.md (local cluster + role + DB) - ---- - -## Context - -This sub-plan covers Phase 2 deliverable D4 (sqlx migration files under -`crates/vestige-core/migrations/postgres/`) PLUS the schema additions decided -in ADR 0002: - -- D7 -- multi-tenancy reservation: `users`, `groups`, `group_memberships` - tables, plus `owner_user_id`, `visibility`, `shared_with_groups` columns on - `knowledge_nodes`. Phase 3 fills these in; Phase 2 just reserves them so the auth - filter is later additive instead of an online migration over a populated, - HNSW-indexed table. -- D8 -- `codebase` promoted to a first-class indexed column on `knowledge_nodes`. - -This sub-plan also adds the parity SQLite migration (V15) that mirrors D7 + -D8 on the SQLite side, so a single-user SQLite deployment sees the same -columns (with stand-in defaults). - -After this sub-plan lands: - -- A fresh Postgres database, with the `vestige` role from the local-dev - setup, can be initialized by running `sqlx::migrate!` against - `crates/vestige-core/migrations/postgres/`, plus one programmatic - `register_model` call before the HNSW migration. -- A fresh SQLite database initialized by `apply_migrations` lands at - schema_version = 15 with the new tables and columns present. -- `PgMemoryStore::connect` wires the migrator into the connect path - (pool build -> migrator up-to v1 -> register_model -> migrator up-to v2). -- The SQLite test suite continues to pass. -- No `sqlx::query!` calls are introduced yet; the offline `.sqlx/` cache is - filled out in `0002d-store-impl-bodies.md`. - -The deliverable is purely schema. No query bodies, no row-mapping, no search. - ---- - -## Postgres migration files - -Layout, relative to repo root: - -``` -crates/vestige-core/migrations/postgres/ - 0001_init.up.sql - 0001_init.down.sql - 0002_hnsw.up.sql - 0002_hnsw.down.sql -``` - -The `migrations/postgres/` directory is sibling-of-`src/`, not under `src/`, -because `sqlx::migrate!` and `sqlx-cli` both look for a path relative to -`CARGO_MANIFEST_DIR`. The directory is committed. - -### 0001_init.up.sql - -Creates extensions, the multi-tenancy tables (D7), the embedding registry, -the domains catalogue, the `knowledge_nodes` table (with D7 + D8 columns merged in), -the FSRS scheduling and edges tables, the review-events log, all non-vector -indexes, the updated_at trigger, and the bootstrap `local` user row. - -The HNSW vector index is deliberately NOT here -- it requires a typmod on -`knowledge_nodes.embedding`, which is stamped by `register_model` at runtime. See -the "HNSW typmod ordering" section below. - -```sql --- crates/vestige-core/migrations/postgres/0001_init.up.sql --- --- Phase 2 initial schema for the Postgres backend. --- Includes D7 multi-tenancy reservation (users/groups/group_memberships, --- owner_user_id/visibility/shared_with_groups on knowledge_nodes) and D8 --- (codebase first-class column on knowledge_nodes). --- --- The HNSW index on knowledge_nodes.embedding lives in 0002_hnsw.up.sql; it --- requires the column typmod to be stamped first by register_model(). - --- Extensions ---------------------------------------------------------------- - -CREATE EXTENSION IF NOT EXISTS pgcrypto; -CREATE EXTENSION IF NOT EXISTS vector; - --- Embedding model registry -------------------------------------------------- --- Mirrors the SQLite table created in Phase 1 V14. --- One logical row enforced by CHECK (id = 1). - -CREATE TABLE embedding_model ( - id SMALLINT PRIMARY KEY DEFAULT 1 CHECK (id = 1), - name TEXT NOT NULL, - dimension INTEGER NOT NULL CHECK (dimension > 0), - hash TEXT NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT now() -); - --- Domains catalogue --------------------------------------------------------- --- Populated by the Phase 4 DomainClassifier. Phase 2 creates the empty --- table so list/get/upsert/delete work uniformly against both backends. - -CREATE TABLE domains ( - id TEXT PRIMARY KEY, - label TEXT NOT NULL, - centroid vector, - top_terms TEXT[] NOT NULL DEFAULT '{}', - memory_count INTEGER NOT NULL DEFAULT 0, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - metadata JSONB NOT NULL DEFAULT '{}'::jsonb -); - --- Multi-tenancy (D7) -------------------------------------------------------- --- Reserved in Phase 2; populated in Phase 3. --- Single bootstrap user inserted at the bottom of this file. - -CREATE TABLE users ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - handle TEXT NOT NULL UNIQUE, - display_name TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - metadata JSONB NOT NULL DEFAULT '{}'::jsonb -); - -CREATE TABLE groups ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - handle TEXT NOT NULL UNIQUE, - display_name TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - metadata JSONB NOT NULL DEFAULT '{}'::jsonb -); - -CREATE TABLE group_memberships ( - user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, - group_id UUID NOT NULL REFERENCES groups(id) ON DELETE CASCADE, - role TEXT NOT NULL DEFAULT 'member', - joined_at TIMESTAMPTZ NOT NULL DEFAULT now(), - PRIMARY KEY (user_id, group_id), - CHECK (role IN ('member', 'admin')) -); - --- Core knowledge_nodes table ------------------------------------------------- --- Original Phase 2 columns merged with D7 (owner_user_id, visibility, --- shared_with_groups) and D8 (codebase). - -CREATE TABLE knowledge_nodes ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - - -- Content - content TEXT NOT NULL, - node_type TEXT NOT NULL DEFAULT 'general', - tags TEXT[] NOT NULL DEFAULT '{}', - metadata JSONB NOT NULL DEFAULT '{}'::jsonb, - - -- Phase 4 emergent domains (Phase 2 leaves empty) - domains TEXT[] NOT NULL DEFAULT '{}', - domain_scores JSONB NOT NULL DEFAULT '{}'::jsonb, - - -- Embedding (typmod stamped by register_model before 0002_hnsw runs) - embedding vector, - - -- D8: first-class codebase column for high-frequency scoped queries - codebase TEXT, - - -- D7: multi-tenancy reservation. Defaults make Phase 2 single-user - -- behaviour identical to Phase 1. - owner_user_id UUID NOT NULL DEFAULT '00000000-0000-0000-0000-000000000001' - REFERENCES users(id), - visibility TEXT NOT NULL DEFAULT 'private', - shared_with_groups UUID[] NOT NULL DEFAULT '{}', - - -- Timestamps - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - - -- Generated full-text search vector. Phase 2 uses websearch_to_tsquery - -- against this column at query time (see 0002e-hybrid-search.md). - search_vec TSVECTOR GENERATED ALWAYS AS ( - setweight(to_tsvector('english', coalesce(content, '')), 'A') || - setweight(to_tsvector('english', coalesce(node_type, '')), 'B') || - setweight(to_tsvector('english', coalesce(array_to_string(tags, ' '), '')), 'C') - ) STORED, - - -- Visibility tri-state CHECK constraint. See "Visibility CHECK - -- constraint" section below for the cardinality variant we - -- intentionally do NOT add yet. - CHECK (visibility IN ('private', 'group', 'public')) -); - --- FSRS scheduling state (1:1 with knowledge_nodes) --------------------------- --- --- Note: the FK column is named `memory_id` (not `node_id`) to match the --- Phase 1 SQLite trait surface: `SchedulingState { memory_id: Uuid, ... }` --- and `get_scheduling(memory_id: Uuid)` / `update_scheduling(&state)`. The --- table is `knowledge_nodes` but the Rust identifier remained `memory_id` --- across Phase 1 and is preserved here so both backends speak the same --- language at the trait boundary. - -CREATE TABLE scheduling ( - memory_id UUID PRIMARY KEY REFERENCES knowledge_nodes(id) ON DELETE CASCADE, - stability DOUBLE PRECISION NOT NULL DEFAULT 0.0, - difficulty DOUBLE PRECISION NOT NULL DEFAULT 0.0, - retrievability DOUBLE PRECISION NOT NULL DEFAULT 1.0, - last_review TIMESTAMPTZ, - next_review TIMESTAMPTZ, - reps INTEGER NOT NULL DEFAULT 0, - lapses INTEGER NOT NULL DEFAULT 0 -); - --- Spreading activation graph edges ------------------------------------------ - -CREATE TABLE edges ( - source_id UUID NOT NULL REFERENCES knowledge_nodes(id) ON DELETE CASCADE, - target_id UUID NOT NULL REFERENCES knowledge_nodes(id) ON DELETE CASCADE, - edge_type TEXT NOT NULL DEFAULT 'related', - weight DOUBLE PRECISION NOT NULL DEFAULT 1.0, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - PRIMARY KEY (source_id, target_id, edge_type) -); - --- FSRS review event log (append-only; Phase 5 federation reads) ------------- - -CREATE TABLE review_events ( - id BIGSERIAL PRIMARY KEY, - memory_id UUID NOT NULL REFERENCES knowledge_nodes(id) ON DELETE CASCADE, - timestamp TIMESTAMPTZ NOT NULL DEFAULT now(), - rating SMALLINT NOT NULL, - prior_state JSONB NOT NULL, - new_state JSONB NOT NULL -); - --- Indexes ------------------------------------------------------------------- - --- knowledge_nodes: full-text, arrays, hot scalar columns, D7+D8 access patterns -CREATE INDEX idx_knowledge_nodes_fts ON knowledge_nodes USING GIN (search_vec); -CREATE INDEX idx_knowledge_nodes_domains ON knowledge_nodes USING GIN (domains); -CREATE INDEX idx_knowledge_nodes_tags ON knowledge_nodes USING GIN (tags); -CREATE INDEX idx_knowledge_nodes_node_type ON knowledge_nodes (node_type); -CREATE INDEX idx_knowledge_nodes_created ON knowledge_nodes (created_at); -CREATE INDEX idx_knowledge_nodes_updated ON knowledge_nodes (updated_at); - --- D7 visibility filter (Phase 3 query: WHERE owner_user_id = $me ...) -CREATE INDEX idx_knowledge_nodes_owner ON knowledge_nodes (owner_user_id); -CREATE INDEX idx_knowledge_nodes_shared_groups ON knowledge_nodes USING GIN (shared_with_groups); - --- D8 codebase scoping (Phase 4 HDBSCAN per-repo, sharing rules in Phase 4). --- Partial index keeps the index small in single-user mode where most rows --- never set a codebase. -CREATE INDEX idx_knowledge_nodes_codebase - ON knowledge_nodes (codebase) - WHERE codebase IS NOT NULL; - --- scheduling: hot lookup paths for FSRS pickers -CREATE INDEX idx_scheduling_next_review ON scheduling (next_review); -CREATE INDEX idx_scheduling_last_review ON scheduling (last_review); - --- edges: bidirectional + edge type -CREATE INDEX idx_edges_target ON edges (target_id); -CREATE INDEX idx_edges_source ON edges (source_id); -CREATE INDEX idx_edges_type ON edges (edge_type); - --- review_events: per-memory and chronological -CREATE INDEX idx_review_events_memory ON review_events (memory_id); -CREATE INDEX idx_review_events_ts ON review_events (timestamp); - --- users / groups: unique handle indexes are implicit; add nothing extra. --- group_memberships: primary key (user_id, group_id) is the access path. - --- updated_at trigger on knowledge_nodes ---------------------------------------- - -CREATE OR REPLACE FUNCTION knowledge_nodes_set_updated_at() RETURNS TRIGGER AS $$ -BEGIN - NEW.updated_at := now(); - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -CREATE TRIGGER trg_knowledge_nodes_updated_at -BEFORE UPDATE ON knowledge_nodes -FOR EACH ROW EXECUTE FUNCTION knowledge_nodes_set_updated_at(); - --- Bootstrap rows ------------------------------------------------------------ --- Single 'local' user matches the default on knowledge_nodes.owner_user_id so --- single-user Phase 2 inserts never violate the FK. - -INSERT INTO users (id, handle, display_name) - VALUES ('00000000-0000-0000-0000-000000000001', 'local', 'Local User'); -``` - -### 0001_init.down.sql - -Reverse-dependency drop order. Trigger and function first, then indexes, -then tables, then extensions are left alone (extensions are global; we do -not drop them in a `down`). - -```sql --- crates/vestige-core/migrations/postgres/0001_init.down.sql - -DROP TRIGGER IF EXISTS trg_knowledge_nodes_updated_at ON knowledge_nodes; -DROP FUNCTION IF EXISTS knowledge_nodes_set_updated_at(); - --- knowledge_nodes indexes -DROP INDEX IF EXISTS idx_knowledge_nodes_codebase; -DROP INDEX IF EXISTS idx_knowledge_nodes_shared_groups; -DROP INDEX IF EXISTS idx_knowledge_nodes_owner; -DROP INDEX IF EXISTS idx_knowledge_nodes_updated; -DROP INDEX IF EXISTS idx_knowledge_nodes_created; -DROP INDEX IF EXISTS idx_knowledge_nodes_node_type; -DROP INDEX IF EXISTS idx_knowledge_nodes_tags; -DROP INDEX IF EXISTS idx_knowledge_nodes_domains; -DROP INDEX IF EXISTS idx_knowledge_nodes_fts; - --- scheduling indexes -DROP INDEX IF EXISTS idx_scheduling_last_review; -DROP INDEX IF EXISTS idx_scheduling_next_review; - --- edges indexes -DROP INDEX IF EXISTS idx_edges_type; -DROP INDEX IF EXISTS idx_edges_source; -DROP INDEX IF EXISTS idx_edges_target; - --- review_events indexes -DROP INDEX IF EXISTS idx_review_events_ts; -DROP INDEX IF EXISTS idx_review_events_memory; - --- Tables, reverse dependency order -DROP TABLE IF EXISTS review_events; -DROP TABLE IF EXISTS edges; -DROP TABLE IF EXISTS scheduling; -DROP TABLE IF EXISTS knowledge_nodes; -DROP TABLE IF EXISTS group_memberships; -DROP TABLE IF EXISTS groups; -DROP TABLE IF EXISTS users; -DROP TABLE IF EXISTS domains; -DROP TABLE IF EXISTS embedding_model; - --- Extensions are intentionally NOT dropped. They may be in use by other --- databases on the cluster; dropping them is an admin choice. -``` - -### 0002_hnsw.up.sql - -Single statement; separated from 0001 so reembed (sub-plan 0002g) can -DROP/CREATE this index in isolation without touching anything else. - -```sql --- crates/vestige-core/migrations/postgres/0002_hnsw.up.sql --- --- HNSW index on knowledge_nodes.embedding. This migration runs AFTER --- register_model() has stamped the typmod via: --- --- ALTER TABLE knowledge_nodes ALTER COLUMN embedding TYPE vector($N) --- --- where $N is the embedder's dimension(). Without the typmod, pgvector --- rejects HNSW creation with: --- --- ERROR: column does not have dimensions --- --- See "HNSW typmod ordering" in 0002c-migrations.md and the connect() --- sequence in 0002a-skeleton-and-feature-gate.md / 0002d-store-impl-bodies.md. --- --- Operator class: vector_cosine_ops -> distance operator `<=>`. --- Build parameters: m = 16, ef_construction = 64 (pgvector defaults; see --- the master plan 0002 D5 RRF discussion for the rationale). - -CREATE INDEX idx_knowledge_nodes_embedding_hnsw - ON knowledge_nodes USING hnsw (embedding vector_cosine_ops) - WITH (m = 16, ef_construction = 64); -``` - -### 0002_hnsw.down.sql - -```sql --- crates/vestige-core/migrations/postgres/0002_hnsw.down.sql - -DROP INDEX IF EXISTS idx_knowledge_nodes_embedding_hnsw; -``` - ---- - -## HNSW typmod ordering - -pgvector's HNSW index requires the indexed column to have a typmod (fixed -dimension). `vector` (unconstrained) is rejected; `vector(768)` is accepted. -We cannot bake the dimension into 0001 because the dimension is an -embedder-determined runtime value -- different builds may use different -embedders. - -This forces an ordering: - -1. Apply migration 0001 (creates `knowledge_nodes.embedding vector`, no typmod). -2. Connect, decide which embedder is in use, run - `ALTER TABLE knowledge_nodes ALTER COLUMN embedding TYPE vector($N)` - inside `register_model`. -3. Apply migration 0002 (creates HNSW; succeeds because the column now has - a typmod). - -`sqlx::migrate!("...")` runs ALL pending migrations in a single call. It is -not designed to pause between two specific migrations so application code -can interleave a runtime DDL step. So we have two options: - -**Option A: Migration 0002 lives outside the sqlx migrations directory.** -Keep `0001_init.{up,down}.sql` only in `migrations/postgres/`; promote -`0002_hnsw.up.sql` to a Rust `include_str!` constant or a separate -`migrations/postgres-hnsw/` directory, run it manually by `PgMemoryStore` -after `register_model`. - -Pros: simple control flow, one `sqlx::migrate!()` call. -Cons: `sqlx_migrations` table does not record 0002, so `sqlx-cli migrate -info` lies. The HNSW index becomes "shadow" schema state from sqlx's POV. -Reembed (sub-plan 0002g) has to also know about this file outside the -normal migrations directory. - -**Option B (chosen): Both migrations live in the directory; the runner -splits them programmatically.** Use `sqlx::migrate::Migrator::new` to load -the directory and call its `run_to(...)` method with a specific version. - -```rust -// crates/vestige-core/src/storage/postgres/migrations.rs -use sqlx::migrate::Migrator; -use sqlx::PgPool; - -use crate::storage::error::MemoryStoreResult; - -/// Embedded migrator. Loaded at compile time from the migrations directory -/// alongside the crate. Path is relative to CARGO_MANIFEST_DIR. -static MIGRATOR: Migrator = sqlx::migrate!("./migrations/postgres"); - -/// Run migrations up to (and including) version 1. -/// -/// This must be called BEFORE register_model so the schema (knowledge_nodes table, -/// embedding_model registry, etc.) exists for register_model to write into -/// and to ALTER. -pub(crate) async fn run_pre_register(pool: &PgPool) -> MemoryStoreResult<()> { - MIGRATOR.run_to(pool, 1).await?; - Ok(()) -} - -/// Run any remaining migrations (currently: HNSW = version 2). -/// -/// Called AFTER register_model has stamped the embedding column's typmod. -pub(crate) async fn run_post_register(pool: &PgPool) -> MemoryStoreResult<()> { - MIGRATOR.run(pool).await?; - Ok(()) -} -``` - -Pros: sqlx is the only source of truth for migration version state; -`sqlx-cli migrate info` is accurate; reembed re-applies 0002 by name; future -migrations slot in normally. -Cons: relies on `Migrator::run_to`, which exists in sqlx 0.7+ and is the -documented API for staged migration. If that API ever disappears we fall -back to Option A. - -Decision: Option B. `Migrator::run_to(target_version)` is stable in sqlx -0.8. Sub-plan 0002a's `MemoryStoreError` already gains -`#[from] sqlx::migrate::MigrateError` to absorb whichever error variant -this surfaces. - -The `connect()` sequence in sub-plan 0002d will therefore look like: - -```rust -// Sketch only; full body lives in 0002d-store-impl-bodies.md. -pub async fn connect(url: &str, max_connections: u32) -> MemoryStoreResult { - let pool = crate::storage::postgres::pool::build(url, max_connections).await?; - crate::storage::postgres::migrations::run_pre_register(&pool).await?; - let store = Self { pool }; - // register_model is called by the cognitive engine bootstrap, NOT here. - // After it runs, the engine calls store.finalize_schema() which calls - // run_post_register. Same shape as SqliteMemoryStore. - Ok(store) -} - -pub async fn finalize_schema(&self) -> MemoryStoreResult<()> { - crate::storage::postgres::migrations::run_post_register(&self.pool).await -} -``` - -`finalize_schema` lands in 0002d; this sub-plan only ships `run_pre_register` -and `run_post_register` plus their wiring into `connect`. - ---- - -## SQLite V15 migration - -The Phase 1 SQLite schema lives in `crates/vestige-core/src/storage/migrations.rs` -as a `MIGRATIONS` slice. V14 is the latest entry. V15 is appended to mirror -D7 (multi-tenancy) and D8 (codebase) on the SQLite side, so a single-user -SQLite deployment sees the same surface area. - -Constraints versus the Postgres migration: - -- No `UUID[]` -- `shared_with_groups` is a TEXT JSON-encoded `'[]'`. -- No `gen_random_uuid()` -- the bootstrap user UUID is a literal. -- No partial indexes for our chosen pattern (SQLite *does* support partial - indexes since 3.8; we use one for `codebase` to match Postgres). -- No `ADD COLUMN IF NOT EXISTS` -- the V15 column additions are split into a - `MIGRATION_V15_ALTER_COLUMNS` slice exactly like V14 did, so the migration - is idempotent on replay. - -### Insertion point in migrations.rs - -Add to the `MIGRATIONS` slice immediately after V14: - -```rust -// In MIGRATIONS slice, after the V14 entry: -Migration { - version: 15, - description: "ADR 0002 D7+D8: multi-tenancy reservation + codebase column", - up: MIGRATION_V15_UP, -}, -``` - -### V15 SQL - -```rust -/// V15: ADR 0002 D7 + D8. -/// -/// D7 reserves users / groups / group_memberships and owner_user_id / -/// visibility / shared_with_groups columns on knowledge_nodes. Single-user -/// SQLite mode never reads these (the trait surface ignores visibility -/// because there is exactly one user) but they exist so Phase 3 does not -/// have to ALTER a populated table. -/// -/// D8 adds a first-class `codebase` column. -/// -/// Like V14, the ALTER TABLE statements are split into -/// MIGRATION_V15_ALTER_COLUMNS because SQLite has no ADD COLUMN IF NOT EXISTS. -const MIGRATION_V15_UP: &str = r#" --- Migration V15: multi-tenancy reservation + codebase column. - --- 1. Users / groups / group_memberships ----------------------------------- --- Mirrors the Postgres D7 tables. Single bootstrap user inserted below. - -CREATE TABLE IF NOT EXISTS users ( - id TEXT PRIMARY KEY, - handle TEXT NOT NULL UNIQUE, - display_name TEXT, - created_at TEXT NOT NULL, - metadata TEXT NOT NULL DEFAULT '{}' -); - -CREATE TABLE IF NOT EXISTS groups ( - id TEXT PRIMARY KEY, - handle TEXT NOT NULL UNIQUE, - display_name TEXT, - created_at TEXT NOT NULL, - metadata TEXT NOT NULL DEFAULT '{}' -); - -CREATE TABLE IF NOT EXISTS group_memberships ( - user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - group_id TEXT NOT NULL REFERENCES groups(id) ON DELETE CASCADE, - role TEXT NOT NULL DEFAULT 'member' CHECK (role IN ('member', 'admin')), - joined_at TEXT NOT NULL, - PRIMARY KEY (user_id, group_id) -); - --- 2. Bootstrap 'local' user. Same UUID as the Postgres default so a future --- portable export from SQLite -> import to Postgres preserves owner_user_id. - -INSERT OR IGNORE INTO users (id, handle, display_name, created_at) - VALUES ('00000000-0000-0000-0000-000000000001', 'local', 'Local User', - datetime('now')); - --- 3. Per-memory column additions are applied separately by the migration --- runner (see MIGRATION_V15_ALTER_COLUMNS). - --- 4. Indexes that do not depend on the new columns. Index creation on the --- new knowledge_nodes columns is done after MIGRATION_V15_ALTER_COLUMNS --- runs (see runner glue below). - -UPDATE schema_version SET version = 15, applied_at = datetime('now'); -"#; - -/// V15 column additions. SQLite has no ADD COLUMN IF NOT EXISTS, so the -/// runner skips "duplicate column" errors per statement (same shape as V14). -pub const MIGRATION_V15_ALTER_COLUMNS: &[&str] = &[ - // D7 columns. Defaults match the Postgres side. shared_with_groups is - // a JSON-encoded array. - "ALTER TABLE knowledge_nodes ADD COLUMN owner_user_id TEXT NOT NULL DEFAULT '00000000-0000-0000-0000-000000000001'", - "ALTER TABLE knowledge_nodes ADD COLUMN visibility TEXT NOT NULL DEFAULT 'private'", - "ALTER TABLE knowledge_nodes ADD COLUMN shared_with_groups TEXT NOT NULL DEFAULT '[]'", - // D8 column. - "ALTER TABLE knowledge_nodes ADD COLUMN codebase TEXT", -]; - -/// V15 index creation. Runs AFTER the ALTER COLUMN statements succeed. -/// Kept as a separate batch so a partial replay (columns already there, -/// indexes not yet) still creates the indexes. -const MIGRATION_V15_INDEXES: &str = r#" -CREATE INDEX IF NOT EXISTS idx_nodes_owner_user_id ON knowledge_nodes(owner_user_id); -CREATE INDEX IF NOT EXISTS idx_nodes_codebase ON knowledge_nodes(codebase) WHERE codebase IS NOT NULL; --- shared_with_groups is TEXT JSON in SQLite; we do not add a GIN-equivalent --- index. Phase 3 lookups on the SQLite side will scan; SQLite never serves --- the multi-user query path in Phase 2-4 anyway. -"#; -``` - -### Runner glue - -Extend `apply_migrations` in `migrations.rs` to recognise V15 the same way -it recognises V14: - -```rust -// Existing pattern for V14 lives in apply_migrations; extend it: -if migration.version == 15 { - for stmt in MIGRATION_V15_ALTER_COLUMNS { - if let Err(e) = conn.execute_batch(stmt) { - let msg = e.to_string(); - if msg.contains("duplicate column name") { - tracing::debug!( - "V15 ALTER TABLE skipped (column already exists): {}", - msg - ); - } else { - return Err(e); - } - } - } - // Indexes run *after* the columns exist. - conn.execute_batch(MIGRATION_V15_INDEXES)?; -} - -// Then the normal: -conn.execute_batch(migration.up)?; -``` - -Order of operations on a fresh in-memory DB: - -1. V1 - V14 run as before. -2. V15: column ALTERs run first (so MIGRATION_V15_INDEXES sees them). -3. V15 main body creates users/groups/group_memberships and the bootstrap row. -4. V15 indexes batch runs. -5. schema_version advances to 15. - -This intentionally mirrors how V14 handles its ALTER + index pair. - -### Existing-data backfill - -Existing SQLite databases (every Phase 1 deployment) have populated -`knowledge_nodes` rows. The V15 ALTER COLUMN ADD COLUMN statements assign -the default values to every existing row: - -- `owner_user_id` -> `'00000000-0000-0000-0000-000000000001'` -- `visibility` -> `'private'` -- `shared_with_groups` -> `'[]'` -- `codebase` -> NULL - -Phase 2 leaves these defaults in place. Phase 3 owns the migration story -for populating real owner UUIDs and visibility values. - ---- - -## Rust wrapper - -Single file: - -```rust -// crates/vestige-core/src/storage/postgres/migrations.rs -// -// sqlx::migrate! wrapper for the Postgres backend. -// -// We split the migration apply into two halves around register_model: -// - run_pre_register: applies everything up to and including version 1 -// (schema, indexes, bootstrap row). Safe to call on a -// fresh DB. -// - run_post_register: applies the remainder (currently: 0002_hnsw, which -// needs the embedding column typmod stamped first). -// -// See docs/plans/0002c-migrations.md "HNSW typmod ordering" for why this -// split exists. - -#![cfg(feature = "postgres-backend")] - -use sqlx::PgPool; -use sqlx::migrate::Migrator; - -use crate::storage::error::MemoryStoreResult; - -/// Embedded migrator. Path is relative to CARGO_MANIFEST_DIR -/// (`crates/vestige-core/`). -static MIGRATOR: Migrator = sqlx::migrate!("./migrations/postgres"); - -/// Apply migrations through version 1 (the schema-only migration). -/// -/// Idempotent: sqlx::migrate consults the `_sqlx_migrations` table and is -/// a no-op on a database already at version 1 or higher. -pub(crate) async fn run_pre_register(pool: &PgPool) -> MemoryStoreResult<()> { - MIGRATOR.run_to(pool, 1).await?; - Ok(()) -} - -/// Apply any remaining migrations. Called after `register_model` has -/// stamped the typmod on `knowledge_nodes.embedding`. -pub(crate) async fn run_post_register(pool: &PgPool) -> MemoryStoreResult<()> { - MIGRATOR.run(pool).await?; - Ok(()) -} -``` - -Wiring into `PgMemoryStore::connect`. The skeleton from 0002a uses -`todo!()` for everything past pool construction. This sub-plan replaces -that with `run_pre_register` only; `run_post_register` is invoked by -`finalize_schema`, which lands in 0002d. Sketch: - -```rust -// In crates/vestige-core/src/storage/postgres/mod.rs (sub-plan 0002a wires -// pool construction; this sub-plan adds the run_pre_register call): - -impl PgMemoryStore { - pub async fn connect(url: &str, max_connections: u32) -> MemoryStoreResult { - let pool = super::pool::build(url, max_connections).await?; - super::migrations::run_pre_register(&pool).await?; - Ok(Self { pool }) - } -} -``` - -Module wire-up in `crates/vestige-core/src/storage/postgres/mod.rs`: - -```rust -mod migrations; // pub(crate) functions; not re-exported. -``` - -### Error variant - -Sub-plan 0002a already added (under feature gate) to `MemoryStoreError`: - -```rust -#[cfg(feature = "postgres-backend")] -#[error("postgres migration error: {0}")] -Migrate(#[from] sqlx::migrate::MigrateError), -``` - -`run_pre_register` / `run_post_register` use the `?` operator and the -`#[from]` conversion handles it; no extra error handling code is needed. - ---- - -## Visibility CHECK constraint - -ADR 0002 D7 specifies the tri-state enum: - -``` -visibility IN ('private', 'group', 'public') -``` - -This sub-plan includes that CHECK on the `knowledge_nodes` table (see 0001_init.up.sql -above) on both sides: - -- Postgres: `CHECK (visibility IN ('private', 'group', 'public'))` inline on - the table. -- SQLite: same CHECK constraint can be added to V15 if desired. (It is not - in the V15 body above because adding a CHECK via ALTER TABLE on SQLite - requires a table rebuild; we trust the application layer for SQLite, since - SQLite never serves the multi-user query path in Phase 2.) - -The stronger consistency rule from the ADR 0002 follow-ups section, - -``` -CHECK ( - visibility = 'private' - OR cardinality(shared_with_groups) > 0 - OR visibility = 'public' -) -``` - -is intentionally NOT added in this sub-plan. Rationale: - -- The rule is a "no orphan group rows" sanity check, not a correctness - requirement for Phase 2 (single-user mode never touches the column). -- Phase 3 is the first phase that writes `visibility = 'group'`. The check - belongs in the Phase 3 migration that lights up auth, alongside the - application code that ensures `shared_with_groups` is populated before - the visibility flips. -- Adding it now and discovering Phase 3 wants a different shape forces an - online CHECK constraint replacement. - -Recommendation: include only the IN check in Phase 2; revisit the -cardinality check in Phase 3. - ---- - -## Offline sqlx cache - -`crates/vestige-core/.sqlx/` is the on-disk cache of compile-time-checked -queries that `sqlx::query!` / `sqlx::query_as!` emit at build time when -`SQLX_OFFLINE=true`. It is committed to the repo so builds without -`DATABASE_URL` (CI, downstream consumers, contributors without Postgres) -succeed. - -This sub-plan does NOT yet generate or commit `.sqlx/` content. Reasons: - -- `sqlx::query!` calls are introduced in `0002d-store-impl-bodies.md` (real - CRUD bodies) and `0002e-hybrid-search.md` (RRF). This sub-plan ships only - the migrations directory and a wrapper that uses `sqlx::migrate!` -- which - is a compile-time macro that reads files, not a query macro that needs a - DB connection. -- Generating an empty `.sqlx/` directory now is noise that gets immediately - overwritten in the next sub-plan. - -Sub-plan 0002d will land the procedure: - -```sh -# Local dev box with vestige DB initialised per local-dev-postgres-setup.md. -export DATABASE_URL="postgresql://vestige:$(cat ~/.vestige_pg_pw)@127.0.0.1:5432/vestige" - -# Apply migrations against the dev DB. -cargo sqlx migrate run \ - --source crates/vestige-core/migrations/postgres \ - --database-url "$DATABASE_URL" - -# Generate the offline cache. -cargo sqlx prepare --workspace -- --features postgres-backend - -# Verify cache compiles offline. -SQLX_OFFLINE=true cargo check --workspace --features postgres-backend -``` - -The `.sqlx/` directory commit policy is: committed, reviewed in PRs that -add or change `query!` calls, regenerated locally and pushed. - -What this sub-plan DOES need from sqlx-cli, for verification only (see next -section): `cargo sqlx migrate run --source crates/vestige-core/migrations/postgres`. - ---- - -## Verification - -Two halves: Postgres migrations run cleanly on a fresh DB; SQLite V15 does -not break the Phase 1 store. - -### Postgres - -Prerequisites: Postgres 18 with pgvector, a role with CREATEDB and EXTENSION -rights, per `docs/plans/local-dev-postgres-setup.md`. Alternatively, a -container: - -```sh -podman run --rm -d --name vestige-pg \ - -e POSTGRES_PASSWORD=devpw \ - -e POSTGRES_USER=vestige \ - -e POSTGRES_DB=vestige \ - -p 5432:5432 \ - docker.io/pgvector/pgvector:pg18 - -export DATABASE_URL="postgresql://vestige:devpw@127.0.0.1:5432/vestige" -``` - -Steps: - -1. Apply migrations. From the repo root: - - ```sh - cargo install sqlx-cli --no-default-features --features postgres - cargo sqlx migrate run \ - --source crates/vestige-core/migrations/postgres \ - --database-url "$DATABASE_URL" - ``` - - Expected output: `Applied 1/migrate init` (`0002` is gated on typmod; - sqlx-cli will run it and pgvector will reject the HNSW creation with - "column does not have dimensions". This is the expected behaviour when - running migrations without going through the Rust connect path. To run - 0002 manually for verification, first stamp the typmod: - - ```sh - psql "$DATABASE_URL" -c "ALTER TABLE knowledge_nodes ALTER COLUMN embedding TYPE vector(768);" - cargo sqlx migrate run \ - --source crates/vestige-core/migrations/postgres \ - --database-url "$DATABASE_URL" - ``` - - Now 0002 should apply.) - -2. Verify tables exist: - - ```sh - psql "$DATABASE_URL" -c "\dt" - ``` - - Expected (alphabetical): - ``` - domains - edges - embedding_model - group_memberships - groups - knowledge_nodes - review_events - scheduling - users - ``` - -3. Verify the bootstrap user row: - - ```sh - psql "$DATABASE_URL" -c "SELECT id, handle, display_name FROM users;" - ``` - - Expected: - ``` - id | handle | display_name - --------------------------------------+--------+-------------- - 00000000-0000-0000-0000-000000000001 | local | Local User - ``` - -4. Verify HNSW index (only after the typmod stamp + migrate 0002): - - ```sh - psql "$DATABASE_URL" -c "\d knowledge_nodes" - ``` - - The trailing `Indexes:` block should include `idx_knowledge_nodes_embedding_hnsw`. - -5. Verify the D7+D8 columns are present: - - ```sh - psql "$DATABASE_URL" -c " - SELECT column_name, data_type, column_default - FROM information_schema.columns - WHERE table_name = 'knowledge_nodes' - AND column_name IN ('owner_user_id', 'visibility', - 'shared_with_groups', 'codebase') - ORDER BY column_name; - " - ``` - - Expected: four rows, with `owner_user_id` defaulting to the bootstrap - UUID, `visibility` to `'private'::text`, `shared_with_groups` to - `'{}'::uuid[]`, `codebase` NULL-default. - -6. Verify CHECK constraint: - - ```sh - psql "$DATABASE_URL" -c " - INSERT INTO knowledge_nodes (content, visibility) VALUES ('test', 'bogus'); - " - # Expected: ERROR: new row for relation \"knowledge_nodes\" violates check constraint - ``` - -7. Roll back to verify down migrations work: - - ```sh - cargo sqlx migrate revert \ - --source crates/vestige-core/migrations/postgres \ - --database-url "$DATABASE_URL" - cargo sqlx migrate revert \ - --source crates/vestige-core/migrations/postgres \ - --database-url "$DATABASE_URL" - ``` - - `\dt` should then list only the sqlx-managed `_sqlx_migrations` table. - -8. Rust-side smoke test (no `sqlx::query!` calls yet, so cannot live in - a `#[sqlx::test]`-decorated function until 0002d). Manual: - - ```sh - cargo build -p vestige-core --features postgres-backend - ``` - - Should compile. The `sqlx::migrate!("./migrations/postgres")` macro - reads the directory at compile time; a missing file or syntax error - surfaces as a compile error. - -### SQLite - -1. Run the existing test suite: - - ```sh - cargo test -p vestige-core - ``` - - Expected: 352 (or current count + new V15 tests) tests pass, zero - warnings. - -2. New test in `migrations.rs#tests`: - - ```rust - #[test] - fn test_v15_advances_to_15_and_adds_d7_d8_columns() { - let conn = rusqlite::Connection::open_in_memory().expect("open in-memory"); - apply_migrations(&conn).expect("apply_migrations succeeds"); - - let version = get_current_version(&conn).expect("read schema_version"); - assert_eq!(version, 15, "schema_version should advance to 15"); - - // Tables exist - for tbl in ["users", "groups", "group_memberships"] { - let n: i32 = conn.query_row( - "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?1", - [tbl], - |r| r.get(0), - ).expect("query sqlite_master"); - assert_eq!(n, 1, "table {tbl} should exist after V15"); - } - - // Bootstrap user row exists - let n: i32 = conn.query_row( - "SELECT COUNT(*) FROM users WHERE id = '00000000-0000-0000-0000-000000000001'", - [], - |r| r.get(0), - ).expect("query users"); - assert_eq!(n, 1, "bootstrap local user row should exist"); - - // D7+D8 columns on knowledge_nodes - let cols: Vec = conn - .prepare("PRAGMA table_info(knowledge_nodes)") - .unwrap() - .query_map([], |r| r.get::<_, String>(1)) - .unwrap() - .collect::>() - .unwrap(); - for c in ["owner_user_id", "visibility", "shared_with_groups", "codebase"] { - assert!(cols.iter().any(|x| x == c), - "knowledge_nodes should have column {c}"); - } - } - ``` - -3. Idempotency: re-applying V15 on an already-V15 DB must not error. - `apply_migrations` already skips when `current_version >= migration.version`; - no extra test needed beyond ensuring the V14 + V15 ALTER pattern works. - -4. Existing-data backfill smoke: insert a row before applying V15, then - verify the defaults populate: - - ```rust - #[test] - fn test_v15_backfills_existing_rows_with_defaults() { - let conn = rusqlite::Connection::open_in_memory().expect("open"); - - // Apply migrations through V14 only. - // (We rely on the fact that re-running apply_migrations is a no-op, - // so we apply all, then probe the columns. The V15 ALTER on a - // populated table is what we are testing implicitly.) - apply_migrations(&conn).expect("V1-V15"); - - // Insert a row using only Phase 1 columns; V15 defaults must - // populate owner_user_id / visibility / shared_with_groups / codebase. - conn.execute( - "INSERT INTO knowledge_nodes (id, content, node_type, created_at, updated_at, last_accessed) - VALUES ('test', 'hello', 'fact', datetime('now'), datetime('now'), datetime('now'))", - [], - ).expect("insert"); - - let (owner, vis, shared, codebase): (String, String, String, Option) = - conn.query_row( - "SELECT owner_user_id, visibility, shared_with_groups, codebase - FROM knowledge_nodes WHERE id = 'test'", - [], - |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?)), - ).expect("query"); - - assert_eq!(owner, "00000000-0000-0000-0000-000000000001"); - assert_eq!(vis, "private"); - assert_eq!(shared, "[]"); - assert_eq!(codebase, None); - } - ``` - -5. Live deployment: apply V15 to a copy of `~/.vestige/vestige.db` and - verify the existing 150 memories all carry the four new columns with - default values: - - ```sh - cp ~/.vestige/vestige.db /tmp/v15-test.db - sqlite3 /tmp/v15-test.db <<'SQL' - .schema knowledge_nodes - SELECT COUNT(*) FROM knowledge_nodes; - SELECT DISTINCT owner_user_id, visibility, shared_with_groups - FROM knowledge_nodes LIMIT 5; - SQL - # (Migration applies on first read by the vestige binary running V15.) - ``` - - Capture pre- and post-counts. Expected: no row count change, all new - columns populated by defaults. - ---- - -## Acceptance criteria - -- [ ] `crates/vestige-core/migrations/postgres/` directory contains exactly - four files: `0001_init.up.sql`, `0001_init.down.sql`, - `0002_hnsw.up.sql`, `0002_hnsw.down.sql`. Content matches this - sub-plan. -- [ ] `crates/vestige-core/src/storage/postgres/migrations.rs` exports - `run_pre_register` and `run_post_register` as `pub(crate)` async - functions returning `MemoryStoreResult<()>`. Compiles with - `--features postgres-backend`. -- [ ] `PgMemoryStore::connect` (sub-plan 0002a skeleton) is updated to call - `run_pre_register` immediately after pool construction. `connect` - still returns before `register_model` runs; `run_post_register` - lands in 0002d via `finalize_schema`. -- [ ] `crates/vestige-core/src/storage/migrations.rs` has a new V15 entry - in `MIGRATIONS`, with `MIGRATION_V15_UP`, `MIGRATION_V15_ALTER_COLUMNS`, - and `MIGRATION_V15_INDEXES` constants. `apply_migrations` handles - V15 the same shape as V14. -- [ ] `cargo test -p vestige-core` passes. New tests cover V15 advance, - D7+D8 column existence, bootstrap user row, and existing-row backfill. -- [ ] `cargo build -p vestige-core --features postgres-backend` compiles - (the `sqlx::migrate!` macro will fail at compile time if any of the - four SQL files is missing or malformed). -- [ ] `cargo sqlx migrate run --source crates/vestige-core/migrations/postgres` - against a fresh container applies 0001 cleanly; `\dt` lists the nine - Phase 2 tables; `users` contains the bootstrap row. -- [ ] After the manual typmod stamp documented above, `cargo sqlx migrate - run` applies 0002 and `\d knowledge_nodes` shows `idx_knowledge_nodes_embedding_hnsw`. -- [ ] `cargo sqlx migrate revert` twice cleans the DB back to only the - `_sqlx_migrations` table. -- [ ] Inserting a row with `visibility = 'bogus'` is rejected by the CHECK - constraint. -- [ ] No `sqlx::query!` / `sqlx::query_as!` calls are added in this - sub-plan; the `.sqlx/` offline cache is not yet generated. -- [ ] The existing live SQLite DB on the development machine migrates from - V14 to V15 without row count change, and the 150 existing rows all - receive the four V15 default values. diff --git a/docs/plans/0002d-store-impl-bodies.md b/docs/plans/0002d-store-impl-bodies.md deleted file mode 100644 index adfd8aa..0000000 --- a/docs/plans/0002d-store-impl-bodies.md +++ /dev/null @@ -1,1771 +0,0 @@ -# Phase 2 Sub-Plan 0002d -- Store Implementation Bodies - -**Status**: Ready -**Depends on**: -- `0002a-skeleton-and-feature-gate.md` -- `PgMemoryStore` struct + trait impl block exist with `todo!()` bodies. -- `0002b-pool-and-config.md` -- `PgPool` is constructable, `MemoryStoreError::Postgres` and `MemoryStoreError::Migrate` variants exist behind the `postgres-backend` feature. -- `0002c-migrations.md` -- the two sqlx migrations (`0001_init`, `0002_hnsw`) exist, the schema is applied on `connect`, the `knowledge_nodes` / `scheduling` / `edges` / `domains` / `embedding_model` / `users` / `groups` / `group_memberships` / `review_events` tables exist with the D7+D8 columns. - -This sub-plan replaces every `todo!()` in -`crates/vestige-core/src/storage/postgres/mod.rs` with a real sqlx-backed -body, and adds `crates/vestige-core/src/storage/postgres/registry.rs` with -the `ensure_registry` / `register_model` typmod-stamping logic. - -The hybrid `search()` method is the meatiest single body in the backend -(RRF in one SQL statement) and lives in its own sub-plan -(`0002e-hybrid-search.md`). The bodies for the trivial single-branch -variants `fts_search` and `vector_search` are still inside this sub-plan -because they share row-mapping infrastructure with the CRUD bodies. - -Out of scope for this sub-plan: -- The full hybrid `search()` -- see `0002e-hybrid-search.md`. -- SQLite -> Postgres migrate CLI -- see `0002f-migrate-cli.md`. -- Re-embed flow -- see `0002g-reembed.md`. -- Phase 3 visibility filter -- explicitly NOT wired in Phase 2; see the - "Visibility filter posture" section below. - ---- - -## Context - -The Phase 1 `MemoryStore` trait surface is defined in -`crates/vestige-core/src/storage/memory_store.rs` and is the source of -truth for method signatures. ADR 0002 D7 added owner / visibility / -shared_with_groups columns to the `knowledge_nodes` table; ADR 0002 D8 promoted -`codebase` to a first-class column. The sqlx bodies in this sub-plan must -write to and read from those columns, but per ADR 0002 D7 they must NOT -filter on them in Phase 2 -- the visibility filter is a Phase 3 -deliverable that takes an `AuthContext` parameter. - -The semantics of every body must match the SQLite backend's current -behaviour. Where Postgres has native types (`UUID`, `JSONB`, `vector`, -`TEXT[]`, `TIMESTAMPTZ`) we use them directly; the SQLite backend's -RFC3339-string-and-JSON-blob encoding is an artefact of SQLite typing, -not the trait contract. - -Compile-time SQL validation uses sqlx's `query!` / `query_as!` macros. -The first time these macros run against a real database in CI they -populate `.sqlx/` query metadata; the metadata file is committed so -offline builds (CI without a live Postgres) succeed. - ---- - -## MemoryRecord type changes - -ADR 0002 D7 and D8 added four new columns to the `knowledge_nodes` table. -The `MemoryRecord` struct in -`crates/vestige-core/src/storage/memory_store.rs` must grow matching -fields so the trait surface can carry the data through both backends. -This is an additive change to the public type. - -Add to `MemoryRecord` (after the existing `metadata` field): - -```rust -/// Owner of this memory. Defaults to the local bootstrap user -/// (`00000000-0000-0000-0000-000000000001`) in single-user mode. -pub owner_user_id: Uuid, - -/// Tri-state visibility. ADR 0002 D7. -pub visibility: Visibility, - -/// Group IDs this memory is shared with when `visibility == Group`. -/// Empty for `Private` and `Public`. -pub shared_with_groups: Vec, - -/// First-class codebase tag. ADR 0002 D8. None if the ingest pipeline -/// could not infer one. -pub codebase: Option, -``` - -Add a new enum next to `MemoryRecord`: - -```rust -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum Visibility { - Private, - Group, - Public, -} - -impl Visibility { - pub fn as_str(&self) -> &'static str { - match self { - Self::Private => "private", - Self::Group => "group", - Self::Public => "public", - } - } - - pub fn from_str(s: &str) -> MemoryStoreResult { - match s { - "private" => Ok(Self::Private), - "group" => Ok(Self::Group), - "public" => Ok(Self::Public), - other => Err(MemoryStoreError::Backend( - format!("unknown visibility value: {other}"), - )), - } - } -} - -impl Default for Visibility { - fn default() -> Self { Self::Private } -} -``` - -`MemoryRecord` already derives `Serialize` and `Deserialize`; the new -fields ride along automatically. Two callers must change as part of this -sub-plan: - -1. **SQLite backend (V15 migration ships in `0001b-sqlite-split.md` or - the same Phase 1 amendment branch)**: the SQLite backend reads the - four new columns out of `knowledge_nodes` (V15 added them) and - populates the new fields in `Self::node_to_record`. Bootstrap user - ID is the same constant on both backends. Existing call sites that - construct `MemoryRecord` literally (in tests, in cognitive modules) - may default-init the four new fields: - - ```rust - MemoryRecord { - // ... existing fields ... - owner_user_id: LOCAL_USER_ID, - visibility: Visibility::default(), - shared_with_groups: Vec::new(), - codebase: None, - metadata: serde_json::json!({}), - } - ``` - - A single `pub const LOCAL_USER_ID: Uuid = uuid::uuid!("00000000-0000-0000-0000-000000000001");` - in `storage::memory_store` provides the bootstrap constant. - -2. **Cognitive modules that build `MemoryRecord` from the ingest - pipeline**: the ingest path already captures `codebase` in metadata - (see ADR 0002 D8). Lift it from `metadata.codebase` to the new - `codebase` field at the boundary where `MemoryRecord` is built. The - `metadata.codebase` JSON key is removed in the same commit; the - column is now the only source of truth. - -The change is purely additive to the trait surface -- no method -signatures change. Backwards compatibility for stored data (in the -SQLite case) comes from V15 defaulting the new columns to `'private'` -and the bootstrap user. The Postgres schema applies the same defaults -in `0001_init.up.sql`. - ---- - -## Registry module - -New file: `crates/vestige-core/src/storage/postgres/registry.rs`. - -```rust -#![cfg(feature = "postgres-backend")] - -//! Embedding-model registry for the Postgres backend. -//! -//! The `embedding_model` table stores exactly one row (id = 1) describing -//! the model whose vectors live in `knowledge_nodes.embedding`. Phase 2 enforces -//! that the active embedder matches the registered model on every write; -//! re-embed (`0002g-reembed.md`) is the only flow allowed to change the -//! row. -//! -//! The pgvector column `knowledge_nodes.embedding` is created in -//! `0001_init.up.sql` with a placeholder type (`vector`) -- no typmod. -//! On first connect we stamp the real dimension via -//! `ALTER TABLE knowledge_nodes ALTER COLUMN embedding TYPE vector($N)` so the -//! HNSW index (created in `0002_hnsw.up.sql`) sees a sized type. - -use sqlx::PgPool; - -use crate::storage::memory_store::{ - MemoryStoreError, MemoryStoreResult, ModelSignature, -}; - -/// Look up the registered signature, if any. Returns `Ok(None)` on a -/// fresh database. -pub(crate) async fn fetch_registry( - pool: &PgPool, -) -> MemoryStoreResult> { - let row = sqlx::query!( - r#" - SELECT name, dimension, hash - FROM embedding_model - WHERE id = 1 - "# - ) - .fetch_optional(pool) - .await?; - - Ok(row.map(|r| ModelSignature { - name: r.name, - dimension: r.dimension as usize, - hash: r.hash, - })) -} - -/// First-ever call inserts the row and stamps the typmod on -/// `knowledge_nodes.embedding`. Subsequent calls compare against the stored -/// row and return `ModelMismatch` if any field differs. -pub(crate) async fn ensure_registry( - pool: &PgPool, - sig: &ModelSignature, -) -> MemoryStoreResult<()> { - let existing = fetch_registry(pool).await?; - - match existing { - None => { - sqlx::query!( - r#" - INSERT INTO embedding_model (id, name, dimension, hash) - VALUES (1, $1, $2, $3) - "#, - sig.name, - sig.dimension as i32, - sig.hash, - ) - .execute(pool) - .await?; - - stamp_vector_typmod(pool, sig.dimension).await?; - Ok(()) - } - Some(reg) if reg == *sig => Ok(()), - Some(reg) => Err(MemoryStoreError::ModelMismatch { - registered_name: reg.name, - registered_dim: reg.dimension, - registered_hash: reg.hash, - actual_name: sig.name.clone(), - actual_dim: sig.dimension, - actual_hash: sig.hash.clone(), - }), - } -} - -/// Called only by the re-embed flow (`0002g-reembed.md`) after a full -/// re-encode has rewritten every row. Updates the registry row and -/// re-stamps the typmod for the new dimension. -pub(crate) async fn update_registry_for_reembed( - pool: &PgPool, - sig: &ModelSignature, -) -> MemoryStoreResult<()> { - sqlx::query!( - r#" - UPDATE embedding_model - SET name = $1, dimension = $2, hash = $3, created_at = now() - WHERE id = 1 - "#, - sig.name, - sig.dimension as i32, - sig.hash, - ) - .execute(pool) - .await?; - - stamp_vector_typmod(pool, sig.dimension).await?; - Ok(()) -} - -async fn stamp_vector_typmod(pool: &PgPool, dim: usize) -> MemoryStoreResult<()> { - // pgvector's typmod is part of the column type, not a bound parameter. - // `format!` is safe here because `dim` is a `usize` cast to a decimal - // literal; there is no path for user-controlled SQL to reach this - // string. - let ddl = format!( - "ALTER TABLE knowledge_nodes ALTER COLUMN embedding TYPE vector({dim})" - ); - sqlx::query(&ddl).execute(pool).await?; - Ok(()) -} -``` - -Wire the new module into `crates/vestige-core/src/storage/postgres/mod.rs`: - -```rust -pub(crate) mod registry; -``` - -The `fetch_registry` / `ensure_registry` functions are reached from the -trait methods `registered_model` and `register_model` (see method bodies -below). `update_registry_for_reembed` is reached only from -`postgres::reembed`, which is filled in by `0002g-reembed.md`. - ---- - -## Method-by-method bodies - -Every body below replaces a `todo!()` in -`crates/vestige-core/src/storage/postgres/mod.rs`. Method order matches -the trait declaration in `memory_store.rs`. - -Common imports at the top of `mod.rs`: - -```rust -use chrono::{DateTime, Utc}; -use pgvector::Vector; -use uuid::Uuid; - -use crate::storage::memory_store::{ - Domain, HealthStatus, LocalMemoryStore, MemoryEdge, MemoryRecord, - MemoryStoreError, MemoryStoreResult, ModelSignature, SchedulingState, - SearchQuery, SearchResult, StoreStats, Visibility, -}; -``` - -Recurring row-to-record helper (private to `mod.rs`): - -```rust -fn row_to_record( - id: Uuid, - content: String, - node_type: String, - tags: Vec, - domains: Vec, - domain_scores: serde_json::Value, - codebase: Option, - owner_user_id: Uuid, - visibility: String, - shared_with_groups: Vec, - embedding: Option, - metadata: serde_json::Value, - created_at: DateTime, - updated_at: DateTime, -) -> MemoryStoreResult { - let domain_scores: std::collections::HashMap = - serde_json::from_value(domain_scores).unwrap_or_default(); - let embedding = embedding.map(|v| v.to_vec()); - Ok(MemoryRecord { - id, - domains, - domain_scores, - content, - node_type, - tags, - embedding, - created_at, - updated_at, - metadata, - owner_user_id, - visibility: Visibility::from_str(&visibility)?, - shared_with_groups, - codebase, - }) -} -``` - -### Lifecycle - -#### `init` - -```rust -async fn init(&self) -> MemoryStoreResult<()> -``` - -Migrations already ran in `connect`; this is a no-op identical to -SQLite's behaviour. - -```rust -async fn init(&self) -> MemoryStoreResult<()> { - Ok(()) -} -``` - -#### `health_check` - -```rust -async fn health_check(&self) -> MemoryStoreResult -``` - -Issue a trivial `SELECT 1`. Pool acquisition errors degrade to -`HealthStatus::Degraded`; any other error path returns `Unavailable`. - -```rust -async fn health_check(&self) -> MemoryStoreResult { - match sqlx::query_scalar!("SELECT 1::int") - .fetch_one(&self.pool) - .await - { - Ok(_) => Ok(HealthStatus::Healthy), - Err(sqlx::Error::PoolTimedOut) => Ok(HealthStatus::Degraded { - reason: "pool exhausted".to_string(), - }), - Err(e) => Ok(HealthStatus::Unavailable { - reason: e.to_string(), - }), - } -} -``` - -### Embedding-model registry - -#### `registered_model` - -```rust -async fn registered_model(&self) -> MemoryStoreResult> -``` - -Thin pass-through to `registry::fetch_registry`. The Postgres backend -does not cache the row in-memory the way the SQLite backend does -- -sqlx's prepared-statement cache already keeps the SELECT cheap, and -`registered_model` is not on the hot path. - -```rust -async fn registered_model(&self) -> MemoryStoreResult> { - crate::storage::postgres::registry::fetch_registry(&self.pool).await -} -``` - -#### `register_model` - -```rust -async fn register_model(&self, sig: &ModelSignature) -> MemoryStoreResult<()> -``` - -Delegate to `registry::ensure_registry`, which handles the -"insert + stamp typmod" first-run path and the "compare" subsequent path. - -```rust -async fn register_model(&self, sig: &ModelSignature) -> MemoryStoreResult<()> { - crate::storage::postgres::registry::ensure_registry(&self.pool, sig).await -} -``` - -### CRUD - -#### `insert` - -```rust -async fn insert(&self, record: &MemoryRecord) -> MemoryStoreResult -``` - -Single `INSERT` into `knowledge_nodes` with all D7+D8 columns. Bind embedding -as `Option` -- pgvector's sqlx integration handles the -typmod check at execution time, so a length mismatch surfaces as -`MemoryStoreError::Postgres`. The caller-supplied UUID is preserved -(same contract as SQLite). Initial scheduling state is inserted in the -same transaction so a memory is never queryable without a scheduling -row. - -```rust -async fn insert(&self, record: &MemoryRecord) -> MemoryStoreResult { - let embedding: Option = record - .embedding - .as_ref() - .map(|v| Vector::from(v.clone())); - let domain_scores = serde_json::to_value(&record.domain_scores) - .unwrap_or_else(|_| serde_json::json!({})); - - let mut tx = self.pool.begin().await?; - - sqlx::query!( - r#" - INSERT INTO knowledge_nodes ( - id, - owner_user_id, - visibility, - shared_with_groups, - codebase, - content, - node_type, - tags, - domains, - domain_scores, - embedding, - metadata, - created_at, - updated_at - ) - VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10::jsonb, - $11, $12::jsonb, $13, $14 - ) - "#, - record.id, - record.owner_user_id, - record.visibility.as_str(), - &record.shared_with_groups as &[Uuid], - record.codebase.as_deref(), - record.content, - record.node_type, - &record.tags as &[String], - &record.domains as &[String], - domain_scores, - embedding as Option, - record.metadata, - record.created_at, - record.updated_at, - ) - .execute(&mut *tx) - .await?; - - // Seed scheduling state. Mirrors SQLite defaults from `knowledge_nodes` - // (stability=1.0, difficulty=0.3, retrievability=1.0, reps=0, lapses=0, - // next_review = created_at + 1 day). - sqlx::query!( - r#" - INSERT INTO scheduling ( - memory_id, stability, difficulty, retrievability, - last_review, next_review, reps, lapses - ) - VALUES ($1, 1.0, 0.3, 1.0, NULL, $2, 0, 0) - "#, - record.id, - record.created_at + chrono::Duration::days(1), - ) - .execute(&mut *tx) - .await?; - - tx.commit().await?; - Ok(record.id) -} -``` - -Tricky bits: -- `&record.tags as &[String]` -- sqlx requires an explicit slice cast - to bind a `Vec` as `text[]`. -- `&record.shared_with_groups as &[Uuid]` -- same pattern for `uuid[]`. -- `embedding as Option` -- type annotation is mandatory in the - macro because the inference path bottoms out at a generic; pgvector's - `Encode` impl resolves only with a known concrete type. -- The `$10::jsonb` and `$12::jsonb` casts force sqlx to encode through - the JSONB path even if the parameter type-resolves to `JSON`. This - matters because the migrations created the columns as `JSONB`, and - sqlx 0.8 does not always pick JSONB without the cast. - -#### `get` - -```rust -async fn get(&self, id: Uuid) -> MemoryStoreResult> -``` - -`SELECT *` filtered by primary key. Row mapping goes through -`row_to_record`. - -```rust -async fn get(&self, id: Uuid) -> MemoryStoreResult> { - let row = sqlx::query!( - r#" - SELECT - id AS "id!: Uuid", - owner_user_id AS "owner_user_id!: Uuid", - visibility, - shared_with_groups AS "shared_with_groups!: Vec", - codebase, - content, - node_type, - tags AS "tags!: Vec", - domains AS "domains!: Vec", - domain_scores AS "domain_scores!: serde_json::Value", - embedding AS "embedding: Vector", - metadata AS "metadata!: serde_json::Value", - created_at AS "created_at!: DateTime", - updated_at AS "updated_at!: DateTime" - FROM knowledge_nodes - WHERE id = $1 - "#, - id, - ) - .fetch_optional(&self.pool) - .await?; - - let Some(r) = row else { return Ok(None) }; - - Ok(Some(row_to_record( - r.id, r.content, r.node_type, r.tags, r.domains, - r.domain_scores, r.codebase, r.owner_user_id, r.visibility, - r.shared_with_groups, r.embedding, r.metadata, - r.created_at, r.updated_at, - )?)) -} -``` - -The `AS "name!: Type"` annotations tell sqlx the exact Rust type for -each column, which is required for `Vec` (from `uuid[]`) and -`Vector` (from `vector`). The `!` means "trust me, this column is NOT -NULL"; sqlx skips its `Option` wrapping for those columns. The -`embedding` column is nullable, so it gets `Option` (no `!`). - -#### `update` - -```rust -async fn update(&self, record: &MemoryRecord) -> MemoryStoreResult<()> -``` - -Update everything the caller might have changed. `updated_at` is set -server-side via `now()` so clock drift between hosts does not leak into -the timeline. (If the caller wants to forge `updated_at` -- e.g. the -migrate CLI replaying SQLite timestamps -- it goes through `insert`, not -`update`.) The schema's `BEFORE UPDATE` trigger could replace this; we -write `updated_at = now()` explicitly to be backend-agnostic. - -```rust -async fn update(&self, record: &MemoryRecord) -> MemoryStoreResult<()> { - let embedding: Option = record - .embedding - .as_ref() - .map(|v| Vector::from(v.clone())); - let domain_scores = serde_json::to_value(&record.domain_scores) - .unwrap_or_else(|_| serde_json::json!({})); - - let rows = sqlx::query!( - r#" - UPDATE knowledge_nodes SET - owner_user_id = $2, - visibility = $3, - shared_with_groups = $4, - codebase = $5, - content = $6, - node_type = $7, - tags = $8, - domains = $9, - domain_scores = $10::jsonb, - embedding = $11, - metadata = $12::jsonb, - updated_at = now() - WHERE id = $1 - "#, - record.id, - record.owner_user_id, - record.visibility.as_str(), - &record.shared_with_groups as &[Uuid], - record.codebase.as_deref(), - record.content, - record.node_type, - &record.tags as &[String], - &record.domains as &[String], - domain_scores, - embedding as Option, - record.metadata, - ) - .execute(&self.pool) - .await? - .rows_affected(); - - if rows == 0 { - return Err(MemoryStoreError::NotFound(record.id.to_string())); - } - Ok(()) -} -``` - -#### `delete` - -```rust -async fn delete(&self, id: Uuid) -> MemoryStoreResult<()> -``` - -Single `DELETE` by id. `scheduling`, `edges`, and `review_events` all -have `ON DELETE CASCADE` on their `memory_id` foreign key, so this one -statement clears every dependent row. - -```rust -async fn delete(&self, id: Uuid) -> MemoryStoreResult<()> { - let rows = sqlx::query!( - "DELETE FROM knowledge_nodes WHERE id = $1", - id, - ) - .execute(&self.pool) - .await? - .rows_affected(); - - if rows == 0 { - return Err(MemoryStoreError::NotFound(id.to_string())); - } - Ok(()) -} -``` - -### Search (single-branch variants) - -The full hybrid `search` is implemented in `0002e-hybrid-search.md`. -The two single-branch variants below ship in this sub-plan. - -#### `fts_search` - -```rust -async fn fts_search(&self, text: &str, limit: usize) -> MemoryStoreResult> -``` - -PostgreSQL full-text search using the precomputed `search_vec` tsvector -column and `websearch_to_tsquery` (handles bare words, phrases, and -boolean operators). Ranking with `ts_rank_cd` (cover-density) so longer -matches outrank shorter ones; the SQLite backend uses BM25 from FTS5 but -the trait contract only requires "higher is better". - -```rust -async fn fts_search( - &self, - text: &str, - limit: usize, -) -> MemoryStoreResult> { - let limit = limit.min(1000) as i64; - let rows = sqlx::query!( - r#" - SELECT - m.id AS "id!: Uuid", - m.owner_user_id AS "owner_user_id!: Uuid", - m.visibility, - m.shared_with_groups AS "shared_with_groups!: Vec", - m.codebase, - m.content, - m.node_type, - m.tags AS "tags!: Vec", - m.domains AS "domains!: Vec", - m.domain_scores AS "domain_scores!: serde_json::Value", - m.embedding AS "embedding: Vector", - m.metadata AS "metadata!: serde_json::Value", - m.created_at AS "created_at!: DateTime", - m.updated_at AS "updated_at!: DateTime", - ts_rank_cd(m.search_vec, websearch_to_tsquery('english', $1)) - AS "score!: f64" - FROM knowledge_nodes m - WHERE m.search_vec @@ websearch_to_tsquery('english', $1) - ORDER BY score DESC - LIMIT $2 - "#, - text, - limit, - ) - .fetch_all(&self.pool) - .await?; - - let mut out = Vec::with_capacity(rows.len()); - for r in rows { - let rec = row_to_record( - r.id, r.content, r.node_type, r.tags, r.domains, - r.domain_scores, r.codebase, r.owner_user_id, r.visibility, - r.shared_with_groups, r.embedding, r.metadata, - r.created_at, r.updated_at, - )?; - out.push(SearchResult { - record: rec, - score: r.score, - fts_score: Some(r.score), - vector_score: None, - }); - } - Ok(out) -} -``` - -The `'english'` text-search configuration matches the GIN index built in -`0001_init.up.sql`. If a future migration parameterises the config, both -the index and this query change together. - -#### `vector_search` - -```rust -async fn vector_search(&self, embedding: &[f32], limit: usize) -> MemoryStoreResult> -``` - -pgvector cosine distance. The HNSW index on `embedding` (built in -`0002_hnsw.up.sql` with `vector_cosine_ops`) makes the `<=>` operator -index-accelerated. We convert the returned distance (0 = identical, 2 = -opposite for cosine on normalized vectors) to a similarity in `[0, 1]` -via `1 - distance`; this matches the SQLite backend's convention. - -```rust -async fn vector_search( - &self, - embedding: &[f32], - limit: usize, -) -> MemoryStoreResult> { - let query_vec = Vector::from(embedding.to_vec()); - let limit = limit.min(1000) as i64; - - let rows = sqlx::query!( - r#" - SELECT - m.id AS "id!: Uuid", - m.owner_user_id AS "owner_user_id!: Uuid", - m.visibility, - m.shared_with_groups AS "shared_with_groups!: Vec", - m.codebase, - m.content, - m.node_type, - m.tags AS "tags!: Vec", - m.domains AS "domains!: Vec", - m.domain_scores AS "domain_scores!: serde_json::Value", - m.embedding AS "embedding: Vector", - m.metadata AS "metadata!: serde_json::Value", - m.created_at AS "created_at!: DateTime", - m.updated_at AS "updated_at!: DateTime", - (1.0 - (m.embedding <=> $1)) AS "score!: f64" - FROM knowledge_nodes m - WHERE m.embedding IS NOT NULL - ORDER BY m.embedding <=> $1 - LIMIT $2 - "#, - query_vec as Vector, - limit, - ) - .fetch_all(&self.pool) - .await?; - - let mut out = Vec::with_capacity(rows.len()); - for r in rows { - let rec = row_to_record( - r.id, r.content, r.node_type, r.tags, r.domains, - r.domain_scores, r.codebase, r.owner_user_id, r.visibility, - r.shared_with_groups, r.embedding, r.metadata, - r.created_at, r.updated_at, - )?; - out.push(SearchResult { - record: rec, - score: r.score, - fts_score: None, - vector_score: Some(r.score), - }); - } - Ok(out) -} -``` - -The `query_vec as Vector` cast is the same type-annotation trick as -`insert` -- sqlx needs the concrete pgvector type to wire up encoding. -The `ORDER BY m.embedding <=> $1` (no `score`) is intentional: it lets -the HNSW index serve the query directly. Sorting by the computed -`score` column would force a sequential scan because the index orders -by distance, not similarity. - -### Scheduling - -The Postgres `scheduling` table is a separate row keyed on `memory_id`, -not embedded in `knowledge_nodes` (unlike SQLite where FSRS columns live on -`knowledge_nodes`). The bodies abstract that difference at the SQL -boundary; callers see the same `SchedulingState` value. - -#### `get_scheduling` - -```rust -async fn get_scheduling(&self, memory_id: Uuid) -> MemoryStoreResult> -``` - -```rust -async fn get_scheduling( - &self, - memory_id: Uuid, -) -> MemoryStoreResult> { - let row = sqlx::query!( - r#" - SELECT - memory_id AS "memory_id!: Uuid", - stability AS "stability!: f64", - difficulty AS "difficulty!: f64", - retrievability AS "retrievability!: f64", - last_review AS "last_review: DateTime", - next_review AS "next_review: DateTime", - reps AS "reps!: i32", - lapses AS "lapses!: i32" - FROM scheduling - WHERE memory_id = $1 - "#, - memory_id, - ) - .fetch_optional(&self.pool) - .await?; - - Ok(row.map(|r| SchedulingState { - memory_id: r.memory_id, - stability: r.stability, - difficulty: r.difficulty, - retrievability: r.retrievability, - last_review: r.last_review, - next_review: r.next_review, - reps: r.reps as u32, - lapses: r.lapses as u32, - })) -} -``` - -#### `update_scheduling` - -```rust -async fn update_scheduling(&self, state: &SchedulingState) -> MemoryStoreResult<()> -``` - -Upsert -- the `INSERT ... ON CONFLICT DO UPDATE` form -- so cognitive -modules that update scheduling for a freshly-inserted memory don't have -to race with the seed row from `insert`. - -```rust -async fn update_scheduling( - &self, - state: &SchedulingState, -) -> MemoryStoreResult<()> { - sqlx::query!( - r#" - INSERT INTO scheduling ( - memory_id, stability, difficulty, retrievability, - last_review, next_review, reps, lapses - ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8) - ON CONFLICT (memory_id) DO UPDATE SET - stability = EXCLUDED.stability, - difficulty = EXCLUDED.difficulty, - retrievability = EXCLUDED.retrievability, - last_review = EXCLUDED.last_review, - next_review = EXCLUDED.next_review, - reps = EXCLUDED.reps, - lapses = EXCLUDED.lapses - "#, - state.memory_id, - state.stability, - state.difficulty, - state.retrievability, - state.last_review, - state.next_review, - state.reps as i32, - state.lapses as i32, - ) - .execute(&self.pool) - .await?; - Ok(()) -} -``` - -#### `get_due_memories` - -```rust -async fn get_due_memories( - &self, - before: DateTime, - limit: usize, -) -> MemoryStoreResult> -``` - -Join `knowledge_nodes` and `scheduling`, filter on `next_review <= before`. -Single query returns both halves of the tuple. - -```rust -async fn get_due_memories( - &self, - before: DateTime, - limit: usize, -) -> MemoryStoreResult> { - let limit = limit.min(10_000) as i64; - let rows = sqlx::query!( - r#" - SELECT - m.id AS "id!: Uuid", - m.owner_user_id AS "owner_user_id!: Uuid", - m.visibility, - m.shared_with_groups AS "shared_with_groups!: Vec", - m.codebase, - m.content, - m.node_type, - m.tags AS "tags!: Vec", - m.domains AS "domains!: Vec", - m.domain_scores AS "domain_scores!: serde_json::Value", - m.embedding AS "embedding: Vector", - m.metadata AS "metadata!: serde_json::Value", - m.created_at AS "created_at!: DateTime", - m.updated_at AS "updated_at!: DateTime", - s.stability AS "stability!: f64", - s.difficulty AS "difficulty!: f64", - s.retrievability AS "retrievability!: f64", - s.last_review AS "last_review: DateTime", - s.next_review AS "next_review: DateTime", - s.reps AS "reps!: i32", - s.lapses AS "lapses!: i32" - FROM knowledge_nodes m - JOIN scheduling s ON s.memory_id = m.id - WHERE s.next_review IS NOT NULL AND s.next_review <= $1 - ORDER BY s.next_review ASC - LIMIT $2 - "#, - before, - limit, - ) - .fetch_all(&self.pool) - .await?; - - let mut out = Vec::with_capacity(rows.len()); - for r in rows { - let rec = row_to_record( - r.id, r.content, r.node_type, r.tags, r.domains, - r.domain_scores, r.codebase, r.owner_user_id, r.visibility, - r.shared_with_groups, r.embedding, r.metadata, - r.created_at, r.updated_at, - )?; - let state = SchedulingState { - memory_id: rec.id, - stability: r.stability, - difficulty: r.difficulty, - retrievability: r.retrievability, - last_review: r.last_review, - next_review: r.next_review, - reps: r.reps as u32, - lapses: r.lapses as u32, - }; - out.push((rec, state)); - } - Ok(out) -} -``` - -### Graph (edges) - -#### `add_edge` - -```rust -async fn add_edge(&self, edge: &MemoryEdge) -> MemoryStoreResult<()> -``` - -`INSERT ... ON CONFLICT` -- updating the weight if an edge already -exists (matches SQLite's `save_connection` semantics). - -```rust -async fn add_edge(&self, edge: &MemoryEdge) -> MemoryStoreResult<()> { - sqlx::query!( - r#" - INSERT INTO edges ( - source_id, target_id, edge_type, weight, created_at - ) - VALUES ($1, $2, $3, $4, $5) - ON CONFLICT (source_id, target_id, edge_type) DO UPDATE SET - weight = EXCLUDED.weight - "#, - edge.source_id, - edge.target_id, - edge.edge_type, - edge.weight, - edge.created_at, - ) - .execute(&self.pool) - .await?; - Ok(()) -} -``` - -#### `get_edges` - -```rust -async fn get_edges( - &self, - node_id: Uuid, - edge_type: Option<&str>, -) -> MemoryStoreResult> -``` - -Return every edge incident to `node_id` in either direction, optionally -filtered by `edge_type`. The optional filter binds as nullable; `$2 IS -NULL OR edge_type = $2` keeps the prepared statement reusable. - -```rust -async fn get_edges( - &self, - node_id: Uuid, - edge_type: Option<&str>, -) -> MemoryStoreResult> { - let rows = sqlx::query!( - r#" - SELECT - source_id AS "source_id!: Uuid", - target_id AS "target_id!: Uuid", - edge_type, - weight AS "weight!: f64", - created_at AS "created_at!: DateTime" - FROM edges - WHERE (source_id = $1 OR target_id = $1) - AND ($2::text IS NULL OR edge_type = $2) - "#, - node_id, - edge_type, - ) - .fetch_all(&self.pool) - .await?; - - Ok(rows - .into_iter() - .map(|r| MemoryEdge { - source_id: r.source_id, - target_id: r.target_id, - edge_type: r.edge_type, - weight: r.weight, - created_at: r.created_at, - }) - .collect()) -} -``` - -#### `remove_edge` - -```rust -async fn remove_edge(&self, source: Uuid, target: Uuid) -> MemoryStoreResult<()> -``` - -Note: the live trait signature is two args (`source`, `target`). The -master plan's stale three-arg signature including `edge_type` is not -implemented -- the live trait surface wins. Deletes every edge between -the pair regardless of `edge_type`. - -```rust -async fn remove_edge( - &self, - source: Uuid, - target: Uuid, -) -> MemoryStoreResult<()> { - sqlx::query!( - "DELETE FROM edges WHERE source_id = $1 AND target_id = $2", - source, - target, - ) - .execute(&self.pool) - .await?; - Ok(()) -} -``` - -#### `get_neighbors` - -```rust -async fn get_neighbors( - &self, - node_id: Uuid, - depth: usize, -) -> MemoryStoreResult> -``` - -Recursive CTE walks the edge graph outward from `node_id` for up to -`depth` hops. Weights compound multiplicatively along the path (same as -SQLite BFS). Cap the visited set at 256 rows to match SQLite. Direction -is treated as undirected by unioning both halves of each edge inside -the CTE. - -```rust -async fn get_neighbors( - &self, - node_id: Uuid, - depth: usize, -) -> MemoryStoreResult> { - if depth == 0 { - let Some(rec) = self.get(node_id).await? else { - return Err(MemoryStoreError::NotFound(node_id.to_string())); - }; - return Ok(vec![(rec, 1.0)]); - } - - let depth_i = depth.min(16) as i32; - let rows = sqlx::query!( - r#" - WITH RECURSIVE walk(node_id, weight, hops) AS ( - SELECT $1::uuid, 1.0::float8, 0 - UNION ALL - SELECT - CASE WHEN e.source_id = w.node_id THEN e.target_id - ELSE e.source_id END, - w.weight * e.weight, - w.hops + 1 - FROM walk w - JOIN edges e - ON e.source_id = w.node_id OR e.target_id = w.node_id - WHERE w.hops < $2 - ), - best AS ( - SELECT node_id, MAX(weight) AS weight - FROM walk - GROUP BY node_id - LIMIT 256 - ) - SELECT - m.id AS "id!: Uuid", - m.owner_user_id AS "owner_user_id!: Uuid", - m.visibility, - m.shared_with_groups AS "shared_with_groups!: Vec", - m.codebase, - m.content, - m.node_type, - m.tags AS "tags!: Vec", - m.domains AS "domains!: Vec", - m.domain_scores AS "domain_scores!: serde_json::Value", - m.embedding AS "embedding: Vector", - m.metadata AS "metadata!: serde_json::Value", - m.created_at AS "created_at!: DateTime", - m.updated_at AS "updated_at!: DateTime", - b.weight AS "weight!: f64" - FROM best b - JOIN knowledge_nodes m ON m.id = b.node_id - "#, - node_id, - depth_i, - ) - .fetch_all(&self.pool) - .await?; - - let mut out = Vec::with_capacity(rows.len()); - for r in rows { - let rec = row_to_record( - r.id, r.content, r.node_type, r.tags, r.domains, - r.domain_scores, r.codebase, r.owner_user_id, r.visibility, - r.shared_with_groups, r.embedding, r.metadata, - r.created_at, r.updated_at, - )?; - out.push((rec, r.weight)); - } - Ok(out) -} -``` - -The CTE can visit a node multiple times via different paths; the `best` -sub-CTE picks the highest weight per node. The `LIMIT 256` matches the -SQLite BFS cap. Postgres' recursive CTE is breadth-first by hop count -because of the `WHERE w.hops < $2` predicate. - -### Domains (Phase 4 populates; Phase 2 ships CRUD) - -The `domains` table is empty in Phase 2; these methods exist so the -trait surface is complete but they do not get exercised until Phase 4 -HDBSCAN clustering runs. - -#### `list_domains` - -```rust -async fn list_domains(&self) -> MemoryStoreResult> -``` - -```rust -async fn list_domains(&self) -> MemoryStoreResult> { - let rows = sqlx::query!( - r#" - SELECT - id, - label, - centroid AS "centroid: Vector", - top_terms AS "top_terms!: Vec", - memory_count AS "memory_count!: i64", - created_at AS "created_at!: DateTime" - FROM domains - ORDER BY created_at ASC - "# - ) - .fetch_all(&self.pool) - .await?; - - Ok(rows - .into_iter() - .map(|r| Domain { - id: r.id, - label: r.label, - centroid: r.centroid.map(|v| v.to_vec()).unwrap_or_default(), - top_terms: r.top_terms, - memory_count: r.memory_count as usize, - created_at: r.created_at, - }) - .collect()) -} -``` - -#### `get_domain` - -```rust -async fn get_domain(&self, id: &str) -> MemoryStoreResult> -``` - -```rust -async fn get_domain(&self, id: &str) -> MemoryStoreResult> { - let row = sqlx::query!( - r#" - SELECT - id, - label, - centroid AS "centroid: Vector", - top_terms AS "top_terms!: Vec", - memory_count AS "memory_count!: i64", - created_at AS "created_at!: DateTime" - FROM domains - WHERE id = $1 - "#, - id, - ) - .fetch_optional(&self.pool) - .await?; - - Ok(row.map(|r| Domain { - id: r.id, - label: r.label, - centroid: r.centroid.map(|v| v.to_vec()).unwrap_or_default(), - top_terms: r.top_terms, - memory_count: r.memory_count as usize, - created_at: r.created_at, - })) -} -``` - -#### `upsert_domain` - -```rust -async fn upsert_domain(&self, domain: &Domain) -> MemoryStoreResult<()> -``` - -```rust -async fn upsert_domain(&self, domain: &Domain) -> MemoryStoreResult<()> { - let centroid = if domain.centroid.is_empty() { - None - } else { - Some(Vector::from(domain.centroid.clone())) - }; - - sqlx::query!( - r#" - INSERT INTO domains ( - id, label, centroid, top_terms, memory_count, created_at - ) - VALUES ($1, $2, $3, $4, $5, $6) - ON CONFLICT (id) DO UPDATE SET - label = EXCLUDED.label, - centroid = EXCLUDED.centroid, - top_terms = EXCLUDED.top_terms, - memory_count = EXCLUDED.memory_count - "#, - domain.id, - domain.label, - centroid as Option, - &domain.top_terms as &[String], - domain.memory_count as i64, - domain.created_at, - ) - .execute(&self.pool) - .await?; - Ok(()) -} -``` - -#### `delete_domain` - -```rust -async fn delete_domain(&self, id: &str) -> MemoryStoreResult<()> -``` - -```rust -async fn delete_domain(&self, id: &str) -> MemoryStoreResult<()> { - sqlx::query!( - "DELETE FROM domains WHERE id = $1", - id, - ) - .execute(&self.pool) - .await?; - Ok(()) -} -``` - -#### `classify` - -```rust -async fn classify(&self, embedding: &[f32]) -> MemoryStoreResult> -``` - -The Postgres backend can ship this as a single SQL query against the -empty `domains` table -- it correctly returns an empty vector in Phase -2 and starts returning real scores in Phase 4 without any code change. - -```rust -async fn classify( - &self, - embedding: &[f32], -) -> MemoryStoreResult> { - let query_vec = Vector::from(embedding.to_vec()); - let rows = sqlx::query!( - r#" - SELECT - id, - (1.0 - (centroid <=> $1)) AS "score!: f64" - FROM domains - WHERE centroid IS NOT NULL - ORDER BY score DESC - "#, - query_vec as Vector, - ) - .fetch_all(&self.pool) - .await?; - - Ok(rows.into_iter().map(|r| (r.id, r.score)).collect()) -} -``` - -### Bulk / maintenance - -#### `count` - -```rust -async fn count(&self) -> MemoryStoreResult -``` - -```rust -async fn count(&self) -> MemoryStoreResult { - let n: i64 = sqlx::query_scalar!("SELECT COUNT(*) FROM knowledge_nodes") - .fetch_one(&self.pool) - .await? - .unwrap_or(0); - Ok(n as usize) -} -``` - -#### `get_stats` - -```rust -async fn get_stats(&self) -> MemoryStoreResult -``` - -Aggregate counts across `knowledge_nodes`, `edges`, `domains`. Read the -registry inline. - -```rust -async fn get_stats(&self) -> MemoryStoreResult { - let row = sqlx::query!( - r#" - SELECT - (SELECT COUNT(*) FROM knowledge_nodes) - AS "total_memories!: i64", - (SELECT COUNT(*) FROM knowledge_nodes WHERE embedding IS NOT NULL) - AS "memories_with_embeddings!: i64", - (SELECT COUNT(*) FROM edges) - AS "total_edges!: i64", - (SELECT COUNT(*) FROM domains) - AS "total_domains!: i64", - (SELECT name FROM embedding_model WHERE id = 1) - AS "registered_model_name: String", - (SELECT dimension FROM embedding_model WHERE id = 1) - AS "registered_model_dim: i32" - "# - ) - .fetch_one(&self.pool) - .await?; - - Ok(StoreStats { - total_memories: row.total_memories as usize, - memories_with_embeddings: row.memories_with_embeddings as usize, - total_edges: row.total_edges as usize, - total_domains: row.total_domains as usize, - registered_model_name: row.registered_model_name, - registered_model_dim: row.registered_model_dim.map(|d| d as usize), - }) -} -``` - -#### `vacuum` - -```rust -async fn vacuum(&self) -> MemoryStoreResult<()> -``` - -`VACUUM` cannot run inside a transaction. sqlx wraps each `query!` -invocation in an implicit transaction when it grabs a pooled -connection, but it does not -- the pool hands out a raw connection -that runs statements in autocommit mode by default. The safe path is -to acquire a connection explicitly and `execute` each statement -separately so neither participates in a transaction. - -```rust -async fn vacuum(&self) -> MemoryStoreResult<()> { - let mut conn = self.pool.acquire().await?; - sqlx::query("VACUUM ANALYZE knowledge_nodes") - .execute(conn.as_mut()) - .await?; - sqlx::query("VACUUM ANALYZE scheduling") - .execute(conn.as_mut()) - .await?; - sqlx::query("VACUUM ANALYZE edges") - .execute(conn.as_mut()) - .await?; - Ok(()) -} -``` - -`conn.as_mut()` yields a `&mut PgConnection`, which sqlx accepts as an -executor. Using `&self.pool` here would let sqlx pick a fresh -connection per statement (still fine, but two extra acquisitions). Note -we do NOT vacuum `domains`, `edges`-related lookup tables (`users` / -`groups` etc.) -- they are either empty in Phase 2 or low-churn and the -nightly autovacuum suffices. - ---- - -## Visibility filter posture - -ADR 0002 D7 declares the future Phase 3 visibility filter (reproduced -here for clarity): - -```sql -WHERE - (visibility = 'private' AND owner_user_id = $me) - OR (visibility = 'group' - AND (owner_user_id = $me OR shared_with_groups && $my_group_ids)) - OR visibility = 'public' -``` - -**Phase 2 does NOT apply this filter.** Every body above reads and -writes the rows it touches regardless of `owner_user_id` or -`visibility` because there is exactly one user in Phase 2 mode (the -bootstrap user from `0001_init.up.sql`). The reviewer should NOT expect -`WHERE owner_user_id = $...` clauses in Phase 2 method bodies. - -Phase 3 introduces an `AuthContext` parameter on the trait methods and -threads it into each WHERE clause. That migration is purely additive -(adds a parameter, adds a clause); it does not need a schema migration -because the columns and indexes are already in place. - -The four new `MemoryRecord` fields ARE populated correctly in Phase 2 -(insert writes them, get/search read them) so that exported archives -and replicated rows round-trip the visibility intent the moment Phase -3 enables filtering. - ---- - -## Offline sqlx cache - -`sqlx::query!` and `sqlx::query_as!` validate every SQL string at -compile time by contacting a live database. To keep CI builds from -needing a Postgres on the build host, sqlx supports an offline cache -in `/.sqlx/` containing one JSON file per validated query. - -This sub-plan is where `.sqlx/` is first populated and committed. - -Workflow: - -1. Ensure a local Postgres is running with the same schema CI will see: - - ```bash - cd crates/vestige-core - export DATABASE_URL="postgres://vestige:vestige@127.0.0.1:5432/vestige_dev" - sqlx database create - sqlx migrate run --source migrations/postgres - ``` - -2. Generate the offline cache: - - ```bash - cargo sqlx prepare --workspace -- --features postgres-backend - ``` - - This walks every `sqlx::query!` invocation under the active feature - flags and writes `crates/vestige-core/.sqlx/query-.json`. The - `--workspace` flag is needed because `vestige-mcp` enables the - `postgres-backend` feature transitively in `0002b-pool-and-config.md`. - -3. Stage and commit the cache directory: - - ```bash - git add crates/vestige-core/.sqlx/ - git commit -m "store: populate sqlx offline cache for postgres backend" - ``` - -4. Add to repo `.gitignore` adjustments (only if entries already deny - `target/` or similar globs): leave `.sqlx/` excluded from any - ignore globs. Specifically the workspace root `.gitignore` does NOT - contain a `.sqlx` line; if a future PR adds one, this sub-plan's - commit reverts it. - -5. CI runs `SQLX_OFFLINE=true cargo check --features postgres-backend`. - sqlx falls back to the JSON cache when `SQLX_OFFLINE=true` is set, - so CI does not need network access to a Postgres. - -Every time a `query!` invocation changes -- add a column, change a -WHERE clause, rename a binding -- re-run `cargo sqlx prepare` and -commit the updated `.sqlx/` files. The agent implementing this sub-plan -runs `cargo sqlx prepare` as the last step before opening the PR. - ---- - -## Verification - -Three layers of verification before merging this sub-plan. - -### 1. Compile and lint - -```bash -cargo check --workspace --features postgres-backend -cargo build --workspace --features postgres-backend -cargo clippy --workspace --features postgres-backend -- -D warnings - -# SQLite-only build still works (mutual compilability per CLAUDE.md): -cargo check --workspace --no-default-features --features embeddings,vector-search -``` - -### 2. Offline cache builds - -```bash -SQLX_OFFLINE=true cargo check --workspace --features postgres-backend -``` - -This is what CI will run. If it fails, `cargo sqlx prepare` was not -re-run after the last query change. - -### 3. Integration round-trip test (testcontainers) - -New test file: -`crates/vestige-core/tests/postgres_round_trip.rs`. Skipped unless the -`postgres-backend` feature is active and Docker / Podman is available. - -```rust -#![cfg(feature = "postgres-backend")] - -use chrono::Utc; -use testcontainers::{clients, GenericImage}; -use uuid::Uuid; -use vestige_core::storage::memory_store::{ - LocalMemoryStore, MemoryEdge, MemoryRecord, SchedulingState, Visibility, - LOCAL_USER_ID, -}; -use vestige_core::storage::postgres::PgMemoryStore; - -#[tokio::test] -async fn round_trip_crud_search_scheduling_edges() { - let docker = clients::Cli::default(); - let image = GenericImage::new("pgvector/pgvector", "pg18") - .with_env_var("POSTGRES_PASSWORD", "test") - .with_env_var("POSTGRES_DB", "vestige_test") - .with_exposed_port(5432); - let container = docker.run(image); - let port = container.get_host_port_ipv4(5432); - let url = format!("postgres://postgres:test@127.0.0.1:{port}/vestige_test"); - - let store = PgMemoryStore::connect(&url, 5).await.expect("connect"); - - // Register the model (typmod stamp). - store.register_model(&fixture_signature(384)).await.expect("register"); - - // insert -> get -> update -> delete. - let mut rec = fixture_record(); - let id = store.insert(&rec).await.expect("insert"); - let fetched = store.get(id).await.expect("get").expect("present"); - assert_eq!(fetched.content, rec.content); - assert_eq!(fetched.owner_user_id, LOCAL_USER_ID); - assert_eq!(fetched.visibility, Visibility::Private); - - rec.content = "edited".to_string(); - store.update(&rec).await.expect("update"); - assert_eq!(store.get(id).await.unwrap().unwrap().content, "edited"); - - // fts_search. - let hits = store.fts_search("edited", 10).await.expect("fts"); - assert!(hits.iter().any(|h| h.record.id == id)); - - // vector_search. - let emb = rec.embedding.clone().unwrap(); - let vhits = store.vector_search(&emb, 10).await.expect("vector"); - assert!(vhits.iter().any(|h| h.record.id == id)); - - // scheduling round-trip. - let sched = store.get_scheduling(id).await.unwrap().expect("seeded"); - let new_state = SchedulingState { - memory_id: id, - stability: 5.5, - difficulty: 0.2, - retrievability: 0.95, - last_review: Some(Utc::now()), - next_review: Some(Utc::now() + chrono::Duration::days(3)), - reps: sched.reps + 1, - lapses: sched.lapses, - }; - store.update_scheduling(&new_state).await.expect("update sched"); - let after = store.get_scheduling(id).await.unwrap().unwrap(); - assert_eq!(after.reps, new_state.reps); - - // edges. - let other = fixture_record(); - let other_id = store.insert(&other).await.unwrap(); - let edge = MemoryEdge { - source_id: id, - target_id: other_id, - edge_type: "semantic".to_string(), - weight: 0.8, - created_at: Utc::now(), - }; - store.add_edge(&edge).await.expect("add_edge"); - let edges = store.get_edges(id, None).await.unwrap(); - assert_eq!(edges.len(), 1); - let neighbors = store.get_neighbors(id, 1).await.unwrap(); - assert!(neighbors.iter().any(|(r, _)| r.id == other_id)); - store.remove_edge(id, other_id).await.expect("remove_edge"); - assert!(store.get_edges(id, None).await.unwrap().is_empty()); - - // delete -> cascade. - store.delete(id).await.expect("delete"); - assert!(store.get(id).await.unwrap().is_none()); - assert!(store.get_scheduling(id).await.unwrap().is_none()); -} - -fn fixture_record() -> MemoryRecord { - MemoryRecord { - id: Uuid::new_v4(), - domains: vec![], - domain_scores: Default::default(), - content: "the quick brown fox jumps over the lazy dog".into(), - node_type: "fact".into(), - tags: vec!["test".into()], - embedding: Some(vec![0.1_f32; 384]), - created_at: Utc::now(), - updated_at: Utc::now(), - metadata: serde_json::json!({}), - owner_user_id: LOCAL_USER_ID, - visibility: Visibility::Private, - shared_with_groups: vec![], - codebase: Some("vestige".to_string()), - } -} - -fn fixture_signature(dim: usize) -> vestige_core::storage::memory_store::ModelSignature { - vestige_core::storage::memory_store::ModelSignature { - name: "test/model".to_string(), - dimension: dim, - hash: "0".repeat(64), - } -} -``` - -Add `testcontainers = "0.20"` to `[dev-dependencies]` under -`#[cfg(feature = "postgres-backend")]` gating. The test is the slowest -in the suite (spawns a Docker container, ~5 s startup); annotate with -`#[ignore]` if CI runtime budget requires opt-in execution. - -### 4. Manual smoke (optional but recommended) - -```bash -# Tear down any prior database. -make postgres-down ; make postgres-up -DATABASE_URL=$(make postgres-url) cargo test \ - -p vestige-core --features postgres-backend -- --include-ignored -``` - -The `postgres-up` / `postgres-down` / `postgres-url` make targets are -defined in `docs/plans/local-dev-postgres-setup.md`. - ---- - -## Acceptance criteria - -This sub-plan is complete when ALL of the following hold: - -1. `cargo build --workspace --features postgres-backend` succeeds with - zero warnings. -2. `cargo clippy --workspace --features postgres-backend -- -D warnings` - succeeds. -3. `cargo build --workspace --no-default-features --features embeddings,vector-search` - still succeeds (the SQLite-only build is not regressed). -4. `SQLX_OFFLINE=true cargo check --workspace --features postgres-backend` - succeeds. `crates/vestige-core/.sqlx/` exists and contains one JSON - file per `sqlx::query!` / `sqlx::query_as!` invocation in the - Postgres backend. -5. Zero `todo!()` macros remain in - `crates/vestige-core/src/storage/postgres/mod.rs`. The only - exception is the body of the trait method `search` -- that method - stays `todo!()` until `0002e-hybrid-search.md` lands. -6. `crates/vestige-core/src/storage/postgres/registry.rs` exists with - the three functions described above - (`fetch_registry`, `ensure_registry`, `update_registry_for_reembed`). -7. `MemoryRecord` carries the four new fields - (`owner_user_id`, `visibility`, `shared_with_groups`, `codebase`) - and the `Visibility` enum is exported alongside it. The SQLite - backend reads and writes the same four fields. -8. The `tests/postgres_round_trip.rs` integration test passes against - a `pgvector/pgvector:pg18` container (insert / get / update / delete - / fts_search / vector_search / get_scheduling / update_scheduling - / add_edge / get_edges / remove_edge / get_neighbors / cascade - delete). -9. No visibility filter clause is present in any Phase 2 method body. - `WHERE owner_user_id = ...`, `WHERE visibility = ...`, and - `shared_with_groups && ...` do not appear anywhere in - `crates/vestige-core/src/storage/postgres/`. -10. `cargo sqlx prepare` was the last command run before commit; the - diff includes `.sqlx/` changes if any query changed. diff --git a/docs/plans/0002e-hybrid-search.md b/docs/plans/0002e-hybrid-search.md deleted file mode 100644 index 1f45174..0000000 --- a/docs/plans/0002e-hybrid-search.md +++ /dev/null @@ -1,825 +0,0 @@ -# Phase 2 Sub-Plan 0002e -- Hybrid RRF Search - -**Status**: Ready -**Depends on**: -- `0002a-skeleton-and-feature-gate.md` (the `postgres-backend` feature flag - exists and `PgMemoryStore` compiles with `todo!()` bodies). -- `0002b-pool-and-config.md` (a working `PgPool` reaches the backend). -- `0002c-migrations.md` (migration `0001_init` has created the `knowledge_nodes` - table with the D7 columns -- `owner_user_id`, `visibility`, - `shared_with_groups` -- and the D8 column `codebase`; migration `0002_hnsw` - has built the HNSW index). -- `0002d-store-impl-bodies.md` (real CRUD bodies exist so the integration - tests below can seed data through the trait surface rather than raw SQL). - -This sub-plan covers master plan 0002 deliverable D5: the hybrid RRF search -query implementation in `crates/vestige-core/src/storage/postgres/search.rs`, -plus the `search`, `fts_search`, and `vector_search` method bodies in -`crates/vestige-core/src/storage/postgres/mod.rs` that delegate into it. - ---- - -## Context - -This is one of the more performance-sensitive sub-plans in Phase 2. Every -search call from the cognitive engine -- the 7-stage retrieval pipeline, -`session_context`, `predict`, `deep_reference`, the dashboard -- bottoms out -in `MemoryStore::search`. The Postgres backend has to keep up with the -existing SQLite hybrid path, which combines BM25 over FTS5 with USearch HNSW -in two separate round trips and fuses the rankings in Rust. - -The shape of the win on Postgres is that both branches and the fusion run -inside one statement. The planner sees both CTEs together, the round trip is -single, and the rerank stage runs over a cleanly overfetched candidate set. - -Latency targets live in `0002h-testing-and-benches.md`. This sub-plan is -responsible for producing a correct, schema-stable query that the benches -can drive against. Do not optimise here; correctness and structure first. - -Master plan 0002 D5 (around lines 522-628 of -`docs/plans/0002-phase-2-postgres-backend.md`) sketches the SQL. That -sketch is the starting point, not the finished product. The schema after -the D7 and D8 amendments has more columns than the sketch enumerates, and -the SQLite `search` method (around line 6503 of -`crates/vestige-core/src/storage/sqlite.rs` in the Phase 1 worktree) -documents the semantics this implementation must stay compatible with: - -- Empty `query.limit` defaults to 10. -- `query.text == Some("")` is treated as no text query (degrade to vector). -- `query.embedding == None` is treated as no vector query (degrade to FTS). -- Both empty returns `Ok(vec![])`; not an error. -- The `MemoryRecord` in each `SearchResult` must be populated with all - fields the trait promises, including `domains` and `domain_scores` (Phase - 4 will fill these in; Phase 2 returns the stored values, which may be - empty arrays / empty objects). - ---- - -## Constants - -```rust -/// Reciprocal Rank Fusion smoothing constant from Cormack, Clarke and Buettcher -/// 2009 ("Reciprocal Rank Fusion outperforms Condorcet and individual rank -/// learning methods"). 60 is the canonical default and is robust across most -/// fusion regimes. Do not tune this without a paper-citation-grade reason. -const RRF_K: i32 = 60; - -/// Each branch (FTS, vector) is allowed to return OVERFETCH_MULT x final_limit -/// rows before fusion. Three matches the Phase 1 SQLite overfetch and gives -/// the fusion enough candidates to recover from any single branch's bad -/// recall on a given query. -const OVERFETCH_MULT: i64 = 3; -``` - -These live at module scope in -`crates/vestige-core/src/storage/postgres/search.rs`. They are `pub(crate)` -only if `0002h-testing-and-benches.md` needs to reference them from the -integration tests; otherwise private. - ---- - -## Public API - -```rust -#![cfg(feature = "postgres-backend")] - -use pgvector::Vector; -use sqlx::PgPool; - -use crate::storage::memory_store::{ - MemoryStoreResult, SearchQuery, SearchResult, -}; - -/// Hybrid RRF search over Postgres FTS and pgvector cosine distance. -/// -/// Branch behavior: -/// - empty text + null embedding -> Ok(vec![]) -/// - empty text + Some(embedding) -> pure vector search (FTS CTE returns -/// zero rows; fusion equals the vector -/// branch) -/// - Some(text) + null embedding -> pure FTS search -/// - Some(text) + Some(embedding) -> full RRF fusion -/// -/// `query.limit == 0` is treated as 10 (matches the SQLite default). -pub(crate) async fn rrf_search( - pool: &PgPool, - query: &SearchQuery, -) -> MemoryStoreResult>; - -/// FTS-only convenience search. Equivalent to calling `rrf_search` with -/// `query.embedding = None`, but uses a dedicated single-branch query that -/// avoids the FULL OUTER JOIN and the params CTE; faster by one planner pass -/// per call. -pub(crate) async fn fts_only( - pool: &PgPool, - text: &str, - limit: usize, -) -> MemoryStoreResult>; - -/// Vector-only convenience search. Dedicated single-branch query for the same -/// latency reason as `fts_only`. -pub(crate) async fn vector_only( - pool: &PgPool, - embedding: &[f32], - limit: usize, -) -> MemoryStoreResult>; -``` - -### Parameter handling - -In `rrf_search`: - -```rust -let final_limit: i32 = if query.limit == 0 { 10 } else { query.limit as i32 }; -let overfetch: i32 = (final_limit as i64 * OVERFETCH_MULT) - .min(i32::MAX as i64) as i32; - -let q_text: &str = query.text.as_deref().unwrap_or(""); -let q_vec: Option = query.embedding.as_ref() - .map(|v| Vector::from(v.clone())); - -let dom_filter: Option<&[String]> = query.domains.as_deref(); -let nt_filter: Option<&[String]> = query.node_types.as_deref(); -let tag_filter: Option<&[String]> = query.tags.as_deref(); - -let min_retr: Option = query.min_retrievability; -``` - -Both branches empty -- `q_text` is empty and `q_vec` is `None` -- returns -`Ok(vec![])` without hitting the database. The SQLite backend has the same -behavior and tests rely on it. - -```rust -if q_text.is_empty() && q_vec.is_none() { - return Ok(Vec::new()); -} -``` - -### `search` method body in `postgres/mod.rs` - -```rust -#[async_trait::async_trait] // or trait_variant after the Phase 1 amendment -impl MemoryStore for PgMemoryStore { - async fn search(&self, query: &SearchQuery) - -> MemoryStoreResult> - { - crate::storage::postgres::search::rrf_search(&self.pool, query).await - } - - async fn fts_search(&self, text: &str, limit: usize) - -> MemoryStoreResult> - { - crate::storage::postgres::search::fts_only(&self.pool, text, limit) - .await - } - - async fn vector_search(&self, embedding: &[f32], limit: usize) - -> MemoryStoreResult> - { - crate::storage::postgres::search::vector_only(&self.pool, embedding, limit) - .await - } -} -``` - -Everything below specifies the inside of those three free functions. - ---- - -## SQL: the hybrid RRF query - -The query is built as one `&'static str` (or `OnceCell`; see "Use -of sqlx::query!" below) and reused. Bound parameters are kept to seven -through a `params` CTE that the rest of the query references by name -- -this keeps the SQL readable and stops the bound-parameter count growing -with each filter clause. - -Bound parameters: - -- `$1`: text query (TEXT, may be empty) -- `$2`: embedding (pgvector::Vector, may be NULL) -- `$3`: overfetch limit per branch (INT) -- `$4`: final limit (INT) -- `$5`: domain filter (TEXT[] or NULL) -- `$6`: node_type filter (TEXT[] or NULL) -- `$7`: tag filter (TEXT[] or NULL) - -If `min_retrievability.is_some()` the outer SELECT adds a JOIN on -`scheduling` and a WHERE clause; that path uses a different prepared -statement (see "min_retrievability filter" below) so the simple-path query -stays free of the join. - -```sql -WITH params AS ( - SELECT - $1::text AS q_text, - $2::vector AS q_vec, - $3::int AS overfetch, - $4::int AS final_limit, - $5::text[] AS dom_filter, - $6::text[] AS nt_filter, - $7::text[] AS tag_filter -), -fts AS ( - SELECT - m.id, - ts_rank_cd( - m.search_vec, - websearch_to_tsquery('english', p.q_text) - ) AS score, - ROW_NUMBER() OVER ( - ORDER BY ts_rank_cd( - m.search_vec, - websearch_to_tsquery('english', p.q_text) - ) DESC - ) AS rank - FROM knowledge_nodes m - CROSS JOIN params p - WHERE p.q_text <> '' - AND m.search_vec @@ websearch_to_tsquery('english', p.q_text) - AND (p.dom_filter IS NULL OR m.domains && p.dom_filter) - AND (p.nt_filter IS NULL OR m.node_type = ANY(p.nt_filter)) - AND (p.tag_filter IS NULL OR m.tags && p.tag_filter) - ORDER BY score DESC - LIMIT (SELECT overfetch FROM params) -), -vec AS ( - SELECT - m.id, - 1.0 - (m.embedding <=> p.q_vec) AS score, - ROW_NUMBER() OVER ( - ORDER BY m.embedding <=> p.q_vec - ) AS rank - FROM knowledge_nodes m - CROSS JOIN params p - WHERE m.embedding IS NOT NULL - AND p.q_vec IS NOT NULL - AND (p.dom_filter IS NULL OR m.domains && p.dom_filter) - AND (p.nt_filter IS NULL OR m.node_type = ANY(p.nt_filter)) - AND (p.tag_filter IS NULL OR m.tags && p.tag_filter) - ORDER BY m.embedding <=> p.q_vec - LIMIT (SELECT overfetch FROM params) -), -fused AS ( - SELECT - COALESCE(f.id, v.id) AS id, - COALESCE(1.0 / (60 + f.rank), 0.0) - + COALESCE(1.0 / (60 + v.rank), 0.0) AS rrf_score, - f.score AS fts_score, - v.score AS vector_score - FROM fts f - FULL OUTER JOIN vec v ON f.id = v.id -) -SELECT - m.id AS "id!: uuid::Uuid", - m.owner_user_id AS "owner_user_id!: uuid::Uuid", - m.visibility AS "visibility!: String", - m.shared_with_groups AS "shared_with_groups!: Vec", - m.codebase AS "codebase: String", - m.domains AS "domains!: Vec", - m.domain_scores AS "domain_scores!: serde_json::Value", - m.content AS "content!: String", - m.node_type AS "node_type!: String", - m.tags AS "tags!: Vec", - m.embedding AS "embedding: pgvector::Vector", - m.metadata AS "metadata!: serde_json::Value", - m.created_at AS "created_at!: chrono::DateTime", - m.updated_at AS "updated_at!: chrono::DateTime", - fused.rrf_score AS "rrf_score!: f64", - fused.fts_score AS "fts_score: f64", - fused.vector_score AS "vector_score: f64" -FROM fused -JOIN knowledge_nodes m ON m.id = fused.id -ORDER BY fused.rrf_score DESC -LIMIT (SELECT final_limit FROM params); -``` - -Notes on the SELECT column list. The D7 columns (`owner_user_id`, -`visibility`, `shared_with_groups`) and the D8 column (`codebase`) are -selected even though Phase 2 does not filter on them yet, so: - -1. The `MemoryRecord` returned to the trait can be populated with the - stored values from day one. Phase 3 will start writing real - `owner_user_id` / `visibility` values; Phase 2 always writes the - single-user defaults (`'00000000-...-0001'`, `'private'`, `'{}'`). The - `MemoryRecord` returned in Phase 2 simply carries those defaults. -2. The schema-drift integration tests (see "Verification") catch the case - where someone adds a NOT NULL column to `knowledge_nodes` without updating - this query. - -Notes on the body: - -- `CROSS JOIN params p` is used instead of the master-plan sketch's - `FROM knowledge_nodes m, params p`. Same plan, clearer intent. -- The `ORDER BY ... LIMIT` inside each branch CTE is there so the planner - can stop early once it has `overfetch` rows; without it the LIMIT is - applied after a full sort over all matches. -- `1.0 - (m.embedding <=> p.q_vec)` converts pgvector's cosine *distance* - to cosine *similarity* in [0, 1] for the `vector_score` output. RRF - itself does not need the similarity -- it uses ranks -- but the trait - surface exposes `vector_score: Option` for caller introspection. -- `RRF_K = 60` is inlined as `60` in the SQL string. A `const` formatter - feels tidier but `60` is a literature constant; spell it out and leave a - comment in the Rust source: `// 60 == RRF_K (Cormack 2009)`. -- `FULL OUTER JOIN` is required: a row that the FTS branch finds and the - vector branch does not must still appear in `fused`, and vice versa. -- `COALESCE(..., 0.0)` on each `1.0 / (60 + rank)` term handles the - no-match-from-this-branch case. The fusion score for a row that only the - FTS branch ranks is `1/(60 + f.rank)` exactly. -- `m.search_vec` is the generated `tsvector` column created in migration - `0001_init` (see D4 of the master plan). - ---- - -## Result row mapping - -`sqlx::query_as::<_, SearchRow>` reads each row into a private struct that -owns the column types exactly as they come back from Postgres. The struct -is converted into a `SearchResult` after fetch. - -```rust -#[derive(sqlx::FromRow)] -struct SearchRow { - id: uuid::Uuid, - owner_user_id: uuid::Uuid, - visibility: String, - shared_with_groups: Vec, - codebase: Option, - domains: Vec, - domain_scores: serde_json::Value, - content: String, - node_type: String, - tags: Vec, - embedding: Option, - metadata: serde_json::Value, - created_at: chrono::DateTime, - updated_at: chrono::DateTime, - rrf_score: f64, - fts_score: Option, - vector_score: Option, -} - -impl SearchRow { - fn into_result(self) -> SearchResult { - use crate::storage::memory_store::MemoryRecord; - use std::collections::HashMap; - - // domain_scores is JSONB; the column always exists, but may be the - // empty object {} when Phase 4 has not classified this memory yet. - let domain_scores: HashMap = - serde_json::from_value(self.domain_scores).unwrap_or_default(); - - let record = MemoryRecord { - id: self.id, - domains: self.domains, - domain_scores, - content: self.content, - node_type: self.node_type, - tags: self.tags, - // pgvector::Vector -> Vec - embedding: self.embedding.map(|v| v.to_vec()), - created_at: self.created_at, - updated_at: self.updated_at, - metadata: self.metadata, - // owner_user_id / visibility / shared_with_groups / codebase - // do not appear on MemoryRecord yet. Phase 3 will decide whether - // to extend MemoryRecord or surface them via a side channel. - // For Phase 2 they are read but discarded here. - }; - - SearchResult { - record, - score: self.rrf_score, - fts_score: self.fts_score, - vector_score: self.vector_score, - } - } -} -``` - -Type mapping summary: - -| SQL type | Rust type | Notes | -|-------------------|--------------------------------------|------------------------------------------------| -| UUID | `uuid::Uuid` | requires sqlx `uuid` feature | -| TEXT | `String` | | -| TEXT NULL | `Option` | used for `codebase` | -| TEXT[] | `Vec` | for `tags`, `domains` | -| UUID[] | `Vec` | for `shared_with_groups` | -| JSONB | `serde_json::Value` | for `metadata`, `domain_scores` | -| TIMESTAMPTZ | `chrono::DateTime` | requires sqlx `chrono` feature | -| VECTOR(N) NULL | `Option` | `.map(|v| v.to_vec())` to `Option>` | -| FLOAT8 | `f64` | | -| FLOAT8 NULL | `Option` | for `fts_score`, `vector_score` | - -If `MemoryRecord` is extended in Phase 3 to carry `owner_user_id`, -`visibility`, `shared_with_groups`, and `codebase`, the conversion above -gets four more fields. Phase 2 reads them so the integration tests can -assert on them via SQL, but the trait surface does not expose them yet. - ---- - -## `fts_only` and `vector_only` -- dedicated single-branch queries - -The master plan offers two options for the convenience methods: reuse -`rrf_search` with one branch nulled, or write dedicated queries. The -dedicated queries win: - -- One CTE instead of three. Planner picks the obvious plan. -- No FULL OUTER JOIN. -- No `params` indirection -- bound parameters used directly. -- The output `score` is the branch's native score (BM25-ish `ts_rank_cd` / - cosine similarity), not an RRF fusion score over one branch. Callers of - `fts_search` and `vector_search` get an intuitive score back. - -### `fts_only` - -Bound parameters: - -- `$1`: text query (TEXT, must be non-empty; the caller guards `text.is_empty()`) -- `$2`: limit (INT) - -```sql -SELECT - m.id AS "id!: uuid::Uuid", - m.owner_user_id AS "owner_user_id!: uuid::Uuid", - m.visibility AS "visibility!: String", - m.shared_with_groups AS "shared_with_groups!: Vec", - m.codebase AS "codebase: String", - m.domains AS "domains!: Vec", - m.domain_scores AS "domain_scores!: serde_json::Value", - m.content AS "content!: String", - m.node_type AS "node_type!: String", - m.tags AS "tags!: Vec", - m.embedding AS "embedding: pgvector::Vector", - m.metadata AS "metadata!: serde_json::Value", - m.created_at AS "created_at!: chrono::DateTime", - m.updated_at AS "updated_at!: chrono::DateTime", - ts_rank_cd(m.search_vec, websearch_to_tsquery('english', $1)) - AS "fts_score!: f64" -FROM knowledge_nodes m -WHERE m.search_vec @@ websearch_to_tsquery('english', $1) -ORDER BY ts_rank_cd(m.search_vec, websearch_to_tsquery('english', $1)) DESC -LIMIT $2; -``` - -The Rust caller maps each row to a `SearchResult` with: - -```rust -SearchResult { - record, - score: fts_score, - fts_score: Some(fts_score), - vector_score: None, -} -``` - -If `text.is_empty()` the caller returns `Ok(Vec::new())` before hitting -the database. `websearch_to_tsquery('english', '')` returns an empty -tsquery that matches nothing; the round-trip is wasted work otherwise. - -### `vector_only` - -Bound parameters: - -- `$1`: embedding (pgvector::Vector) -- `$2`: limit (INT) - -```sql -SELECT - m.id AS "id!: uuid::Uuid", - m.owner_user_id AS "owner_user_id!: uuid::Uuid", - m.visibility AS "visibility!: String", - m.shared_with_groups AS "shared_with_groups!: Vec", - m.codebase AS "codebase: String", - m.domains AS "domains!: Vec", - m.domain_scores AS "domain_scores!: serde_json::Value", - m.content AS "content!: String", - m.node_type AS "node_type!: String", - m.tags AS "tags!: Vec", - m.embedding AS "embedding: pgvector::Vector", - m.metadata AS "metadata!: serde_json::Value", - m.created_at AS "created_at!: chrono::DateTime", - m.updated_at AS "updated_at!: chrono::DateTime", - 1.0 - (m.embedding <=> $1) AS "vector_score!: f64" -FROM knowledge_nodes m -WHERE m.embedding IS NOT NULL -ORDER BY m.embedding <=> $1 -LIMIT $2; -``` - -The Rust caller maps each row to: - -```rust -SearchResult { - record, - score: vector_score, - fts_score: None, - vector_score: Some(vector_score), -} -``` - -Both convenience methods ignore `SearchQuery.domains` / `tags` / -`node_types` / `min_retrievability` -- they take `&str` and `&[f32]` -respectively, not a `SearchQuery`. Callers that want filters on a -single-branch search should call `search` with the other branch input -left at its degrade-to-zero default. - ---- - -## `min_retrievability` filter - -`SearchQuery::min_retrievability: Option` is applied as a final -filter after fusion by joining on the `scheduling` table: - -```sql --- with-min-retrievability variant: identical CTEs to the base query, only --- the final SELECT changes. -SELECT - ... (same column list as the base query) ... -FROM fused -JOIN knowledge_nodes m ON m.id = fused.id -JOIN scheduling s ON s.memory_id = m.id -WHERE s.retrievability >= $8 -ORDER BY fused.rrf_score DESC -LIMIT (SELECT final_limit FROM params); -``` - -This is a separate prepared statement -- the eight-parameter variant -- -held alongside the seven-parameter base. The Rust dispatch: - -```rust -if let Some(min_r) = query.min_retrievability { - sqlx::query_as::<_, SearchRow>(QUERY_WITH_MIN_R) - .bind(q_text) - .bind(q_vec) - .bind(overfetch) - .bind(final_limit) - .bind(dom_filter) - .bind(nt_filter) - .bind(tag_filter) - .bind(min_r) - .fetch_all(pool).await? -} else { - sqlx::query_as::<_, SearchRow>(QUERY_BASE) - .bind(q_text) - .bind(q_vec) - .bind(overfetch) - .bind(final_limit) - .bind(dom_filter) - .bind(nt_filter) - .bind(tag_filter) - .fetch_all(pool).await? -} -``` - -Why not unconditionally join: the `scheduling` join is expensive enough on -a large `knowledge_nodes` table that adding it to every search call regresses the -common path. `min_retrievability` is set by the cognitive engine's -accessibility filter and is `None` in most direct callers. - -The same two-variant pattern repeats for `fts_only` and `vector_only`; in -practice callers of those methods rarely set `min_retrievability` (it is -not part of their argument list), so only the base variant is needed -unless the trait surface grows. - ---- - -## Domain / tag / node_type filters - -Each filter is expressed as a NULL-conditional clause inside both branch -CTEs, written using PostgreSQL array operators: - -```sql -AND (p.dom_filter IS NULL OR m.domains && p.dom_filter) -AND (p.nt_filter IS NULL OR m.node_type = ANY(p.nt_filter)) -AND (p.tag_filter IS NULL OR m.tags && p.tag_filter) -``` - -- `&&` is the PostgreSQL "arrays overlap" operator. Matches if any - element in `m.domains` is in the filter array. Index-friendly with a - GIN index on `m.domains` (created in `0001_init`). -- `= ANY(...)` matches `m.node_type` (a scalar) against any element of - the filter array. Index-friendly with a B-tree on `m.node_type`. -- `&&` is used again on `m.tags` (a `TEXT[]`). - -The NULL-conditional form is critical: when the filter parameter is -`NULL`, the clause short-circuits to `TRUE` and contributes nothing to -the WHERE. This keeps a single query reusable across "no filter" and -"filter set" cases without rewriting SQL. - -When the Rust caller passes `None` for a filter, sqlx binds it as `NULL` -of the column type (`text[]`). The cast `$5::text[]` in the `params` CTE -is what tells sqlx the binding type. - -The master plan's draft has each filter clause duplicated across both -branch CTEs. That duplication is correct -- the planner cannot push a -WHERE clause across a FULL OUTER JOIN into both sides automatically; we -do it manually. - ---- - -## Empty-string text query handling - -The base query guards the FTS branch with `WHERE p.q_text <> ''`. - -`websearch_to_tsquery('english', '')` returns an empty tsquery. An empty -tsquery has no lexemes and matches no document; the `@@` operator returns -false for every row. Without the guard, the FTS branch would still run -- -sequential scan, tokenisation per row, comparison -- and return zero -rows. The guard short-circuits at planning time. - -The guard does not affect the FULL OUTER JOIN: when the FTS branch -returns zero rows, the join degenerates to "every row that the vector -branch returned, with `f.id IS NULL` and `f.rank IS NULL`". The -`COALESCE(1.0 / (60 + f.rank), 0.0)` then evaluates to `0.0`, and the -fused score reduces to the vector branch's RRF term alone. This is the -"pure vector search" degrade path. - -Symmetrically, the vector branch guards itself with -`WHERE m.embedding IS NOT NULL AND p.q_vec IS NOT NULL`, which gives the -"pure FTS search" degrade path when the caller passes no embedding. - -The both-empty case (`q_text == ''` and `q_vec IS NULL`) is intercepted -in Rust before the query runs and returns `Ok(vec![])`. Returning empty -rather than error matches the SQLite behavior and is what the Phase 1 -ingest pipeline relies on for "no signal, no results" fallback. - ---- - -## Use of `sqlx::query!` versus `sqlx::query_as` - -`sqlx::query!` and `sqlx::query_as!` are compile-time-checked: the SQL is -sent to a live Postgres at build time, the result schema is validated, and -the generated Rust struct fields are derived. That checking is the -default for every other query in `0002d-store-impl-bodies.md`. - -For the RRF query, the macro path is impractical for two reasons: - -1. **Two structurally different queries** -- the base (seven parameters, - no `scheduling` join) and the `min_retrievability` variant (eight - parameters, with the join). The macro would force two macro - invocations, each producing its own anonymous result struct, and the - result types would not unify. Manual `From` impls would be needed in - both directions. -2. **The dedicated `fts_only` and `vector_only` queries have a different - output column set** (`fts_score!` instead of `rrf_score! + fts_score? + - vector_score?`). Three macro invocations, three structs, three - conversion helpers. - -The chosen pattern is `sqlx::query_as::<_, SearchRow>(SQL_CONST)` with a -single `SearchRow` struct that owns the column types and a single -`SearchRow::into_result` helper. The SQL is held in module-scope `&'static -str` constants: - -```rust -const QUERY_BASE: &str = include_str!("search.rrf.sql"); -const QUERY_WITH_MIN_R: &str = include_str!("search.rrf.min_retr.sql"); -const QUERY_FTS_ONLY: &str = include_str!("search.fts.sql"); -const QUERY_VECTOR_ONLY: &str = include_str!("search.vector.sql"); -``` - -`include_str!` keeps the SQL out of the Rust source. The four `.sql` -files live next to `search.rs` in -`crates/vestige-core/src/storage/postgres/`. - -The cost: schema drift (someone renames `m.codebase` to `m.repo_name`) -will not break the build. The integration tests in "Verification" below -are the safety net. This is a deliberate trade -- it is the one sub-plan -in Phase 2 where runtime flexibility beats compile-time checking. - -If a future contributor wants compile-time checking back for the simple -case, the right move is to introduce a `#[cfg(test)]`-only macro-checked -variant of `QUERY_BASE` and assert at test build time that the macro -agrees with the string. That belongs in `0002h-testing-and-benches.md` if -anywhere. - ---- - -## Verification - -Integration tests live in -`crates/vestige-core/tests/postgres_search.rs`, gated by -`#[cfg(feature = "postgres-backend")]` and `#[ignore]` by default (the -test runner CI workflow in `0002h-testing-and-benches.md` runs them with -`--ignored` against a live Postgres). - -Common harness for every test: - -1. Spin up Postgres via `sqlx::PgPool::connect` against the test URL. -2. Run `sqlx::migrate!("./migrations/postgres").run(&pool)` to bring the - schema up. -3. Register a deterministic test embedder via `register_model` so - `embedding` columns can be written. -4. Seed 50 mixed memories through `MemoryStore::insert` -- mixed - `node_type` (`fact`, `concept`, `event`, `decision`), mixed `tags` - (`rust`, `postgres`, `search`, `dream`, `bug-fix`), mixed `codebase`, - embeddings drawn from three small clusters so vector recall has - structure to find. - -Test cases: - -**T1. Full RRF returns the seeded target.** -Insert a known memory with `content = "FSRS-6 spaced repetition cadence"` -and an embedding from cluster A. Query with -`text = Some("FSRS spaced repetition")` and an embedding near cluster A. -Assert the target memory is in the top 3 of the returned `SearchResult`s -and that both `fts_score` and `vector_score` are `Some` for it. - -**T2. Pure FTS degrade.** -Same target as T1. Query with `text = Some("FSRS spaced repetition")` and -`embedding = None`. Assert the target appears, all results have -`vector_score == None`, `fts_score == Some(_)`, and `score` equals the -fused RRF score (which collapses to one branch's `1.0/(60 + rank)`). - -**T3. Pure vector degrade.** -Same target as T1. Query with `text = Some("")` and -`embedding = Some(cluster_A_vector)`. Assert the target appears, all -results have `fts_score == None`, `vector_score == Some(_)`. - -**T4. Both empty returns `Ok(vec![])`.** -Query with `text = Some("")` and `embedding = None`. Assert exactly an -empty result vector and that no SQL was executed (assert via a -`sqlx::PgPool` query-count handle if convenient; otherwise document that -the short-circuit lives in Rust). - -**T5. `domains` filter.** -Insert one memory with `domains = vec!["domain-x"]` and 49 others without -it. Query with `domains = Some(vec!["domain-x"])` and a matching text. -Assert exactly one result is returned and it is the seeded memory. - -**T6. `tags` filter.** -Same pattern as T5 with `tags = Some(vec!["bug-fix"])`. - -**T7. `node_types` filter.** -Same pattern as T5 with `node_types = Some(vec!["decision"])`. - -**T8. `min_retrievability` filter.** -Seed two memories with the same content + embedding. Write -`scheduling` rows so that one has `retrievability = 0.9` and the other -`0.1`. Query with `min_retrievability = Some(0.5)`. Assert exactly the -high-retrievability memory is returned. - -**T9. `query.limit == 0` defaults to 10.** -Seed 30 matching memories. Query with `limit = 0`. Assert the result -contains exactly 10 entries. - -**T10. `fts_only` and `vector_only` parity.** -For the same target memory, call `fts_only` and `vector_only` directly -and compare against `search` with the corresponding branch zeroed. The -top-1 result must match by id; the scores need only be of the same sign -and magnitude (not bit-identical, because RRF fusion changes the -absolute score). - -**T11. Schema-drift canary.** -Run the base query against an empty `knowledge_nodes` table and `fetch_all` -into `Vec`. Any added NOT NULL column on `knowledge_nodes` that is -not in the SELECT will fail the test at the `try_get` boundary with a -clear error. This is the test that compensates for not using -`sqlx::query!`. - -**T12. Hostile inputs.** -Query with `text = Some("'; DROP TABLE knowledge_nodes; --")` and a normal -embedding. Assert no panic, results returned cleanly, `knowledge_nodes` table -still present. This is symbolic; `websearch_to_tsquery` is parameter- -bound and SQL injection is not actually possible, but the test is cheap -and the assertion is real. - ---- - -## Acceptance criteria - -A reviewer of the implementation PR should be able to confirm: - -1. `crates/vestige-core/src/storage/postgres/search.rs` exists and is - compiled only when `feature = "postgres-backend"` is on. -2. The four `.sql` files (`search.rrf.sql`, - `search.rrf.min_retr.sql`, `search.fts.sql`, `search.vector.sql`) - exist in the same directory and are `include_str!`-ed into module- - scope `&'static str` constants. -3. `RRF_K = 60` and `OVERFETCH_MULT = 3` are defined as constants at - module scope with the Cormack 2009 citation in a comment. -4. The seven-parameter base query is one statement and uses a `params` - CTE; the eight-parameter `min_retrievability` variant adds exactly - one JOIN and one WHERE clause on top of the base. -5. Empty text degrades to pure vector; null embedding degrades to pure - FTS; both empty short-circuits to `Ok(vec![])` in Rust before the - query runs. -6. The SELECT column list in every query includes `owner_user_id`, - `visibility`, `shared_with_groups`, and `codebase` even though Phase 2 - does not filter on them. -7. `SearchRow::into_result` populates a `MemoryRecord` with every field - the trait requires, including `domains` and `domain_scores` decoded - from JSONB. -8. `PgMemoryStore::search`, `PgMemoryStore::fts_search`, and - `PgMemoryStore::vector_search` each delegate to the corresponding - free function with one line of body. -9. All twelve integration tests (`T1` through `T12`) pass against a live - Postgres with the `0001_init` + `0002_hnsw` migrations applied. -10. `cargo build -p vestige-core` succeeds with - `--features postgres-backend` and with the feature off. -11. `cargo clippy -p vestige-core --features postgres-backend -- -D warnings` - is clean. - -When all eleven are true, this sub-plan is done and -`0002f-migrate-cli.md` is unblocked. diff --git a/docs/plans/0002f-migrate-cli.md b/docs/plans/0002f-migrate-cli.md deleted file mode 100644 index 457de70..0000000 --- a/docs/plans/0002f-migrate-cli.md +++ /dev/null @@ -1,1045 +0,0 @@ -# Phase 2 Sub-Plan 0002f -- SQLite-to-Postgres Migrate CLI - -**Status**: Ready -**Depends on**: -- `0002a-skeleton-and-feature-gate.md` (the `postgres-backend` Cargo feature - and the `crates/vestige-core/src/storage/postgres/` module skeleton). -- `0002b-pool-and-config.md` (`PgPool` construction and `PostgresConfig`). -- `0002c-migrations.md` (the `postgres/migrations/0001_init.up.sql` schema, - including the D7 tenancy columns/tables and the D8 `codebase` column). -- `0002d-store-impl-bodies.md` (real bodies for `PgMemoryStore` trait methods: - `insert`, `register_model`, `add_edge`, `update_scheduling`, etc.; and the - matching source-side reader bodies on `SqliteMemoryStore`, in particular a - windowed-stream API ordered by `(created_at, id)`). - -This sub-plan covers Phase 2 master-plan deliverables D8 (the streaming copy -in `crates/vestige-core/src/storage/postgres/migrate_cli.rs`) and D10 (the -`vestige migrate copy ...` clap subcommand in -`crates/vestige-mcp/src/bin/cli.rs`). Sub-plan `0002g-reembed.md` covers the -`vestige migrate reembed ...` subcommand body; this sub-plan only declares the -`Reembed` clap variant alongside `Copy` so the subcommand layout is final. - -The success criterion is: - -``` -vestige migrate copy --from sqlite --to postgres \ - --sqlite-path ~/.vestige/vestige.db \ - --postgres-url postgresql://localhost/vestige -``` - -streams every row from a Phase 1 SQLite database into a fresh (or partially -populated) Phase 2 Postgres database. Re-running the same command is a no-op -on already-present rows. A `--dry-run` flag prints per-table counts without -writing anything. - ---- - -## Context - -ADR 0002 D2 settled that `PgMemoryStore::connect` mirrors -`SqliteMemoryStore::new`: no `Embedder` in the constructor; the model -signature is stamped by a separate call to `register_model`. The migrator -inherits this symmetry. It opens both backends, validates that the source's -`embedding_model` registry agrees with the destination's (or with the -embedder the user supplied for the destination), and then streams rows. - -ADR 0002 D7 reserved multi-tenancy columns on `knowledge_nodes` (`owner_user_id`, -`visibility`, `shared_with_groups`) and three tables (`users`, `groups`, -`group_memberships`). Phase 2 single-user defaults are the bootstrap row -`'00000000-0000-0000-0000-000000000001'` (`'local'`), `visibility = 'private'`, -empty `shared_with_groups`. The migrator preserves whatever values the source -SQLite holds; it does NOT rewrite owner_user_id from real values to the -bootstrap user. If a Phase 3-aware source has real user rows, those are -copied first (step 5 below) and the foreign key in `knowledge_nodes.owner_user_id` -resolves to the same UUID on the destination. - -ADR 0002 D8 promoted `codebase` to a first-class `TEXT` column. The migrator -reads it as a column on the source side (the Phase 1 amendment's V15 SQLite -migration ensures the column exists; for pre-V15 SQLite the migrator must -detect and fall back to extracting from `metadata->>'codebase'`, see "Source -schema variants" below). - -The Phase 1 `SqliteMemoryStore` is the source backend. `0002d-store-impl-bodies.md` -extends it (and the trait) with a windowed reader ordered by `(created_at, id)` -so the migrator can stream rows in deterministic batches without holding the -full result set in RAM. The migrator assumes that reader exists and produces -`MemoryRecord` instances with all D7+D8 columns populated. - ---- - -## File layout - -``` -crates/vestige-core/src/storage/postgres/migrate_cli.rs -- D8 body -crates/vestige-mcp/src/bin/cli.rs -- D10 clap wiring -tests/phase_2/migrate_test.rs -- integration test -``` - -The migrator lives behind `#[cfg(feature = "postgres-backend")]`. The -`Migrate` clap variant in the CLI is similarly gated. Without the feature, -`vestige` builds and runs exactly as in Phase 1 -- the `migrate` subcommand -simply does not exist. - ---- - -## Plan struct - -```rust -#![cfg(feature = "postgres-backend")] - -use std::path::PathBuf; -use std::sync::Arc; - -use uuid::Uuid; - -use crate::embedder::Embedder; -use crate::storage::memory_store::MemoryStoreError; - -#[derive(Debug, Clone)] -pub struct SqliteToPostgresPlan { - /// Filesystem path to the source SQLite database. Opened read-only. - pub sqlite_path: PathBuf, - - /// libpq-style URL for the destination Postgres database. - pub postgres_url: String, - - /// sqlx pool size for the destination. Default 4. The migrator is - /// single-writer per table for ordering reasons; extra connections are - /// only used for the embedding-model registry probe and for the dry-run - /// COUNT queries that run in parallel with the row scan. - pub max_connections: u32, - - /// Number of rows per Postgres transaction. Default 500. Larger batches - /// reduce commit overhead but increase the amount of work a crash - /// re-runs. - pub batch_size: usize, - - /// If true, count rows per table and emit a report without writing - /// anything to Postgres. - pub dry_run: bool, -} - -impl Default for SqliteToPostgresPlan { - fn default() -> Self { - Self { - sqlite_path: PathBuf::new(), - postgres_url: String::new(), - max_connections: 4, - batch_size: 500, - dry_run: false, - } - } -} -``` - -The struct is public so a future programmatic driver (Rhai script, hero -service, in-process test harness) can call `run_sqlite_to_postgres` without -touching clap. - ---- - -## Report struct - -```rust -#[derive(Debug, Default)] -pub struct MigrationReport { - pub memories_copied: u64, - pub scheduling_rows: u64, - pub edges_copied: u64, - pub review_events_copied: u64, - pub domains_copied: u64, - pub users_copied: u64, - pub groups_copied: u64, - pub group_memberships_copied: u64, - - /// Per-row failures that did not abort the migrator. Each entry pairs - /// the source row id (where derivable) with the error that caused it to - /// be skipped. Rows whose UUID cannot be parsed are reported with - /// `Uuid::nil()` and a descriptive `MemoryStoreError::InvalidInput`. - pub errors: Vec<(Uuid, MemoryStoreError)>, -} -``` - -`errors.is_empty()` is the "clean migration" check. The CLI prints -`errors.len()` at the end and exits non-zero if it is positive. - -Counts are the number of rows the migrator either inserted or skipped due to -ON CONFLICT. They reflect what the source presented, not what the destination -ended up with -- that distinction matters for re-runs: a re-run of a finished -migration reports the same counts but writes zero new rows. - ---- - -## Driver fn - -```rust -pub async fn run_sqlite_to_postgres( - plan: SqliteToPostgresPlan, - embedder: Arc, -) -> MemoryStoreResult; -``` - -Algorithm, step by step: - -### Step 1. Open source SQLite read-only - -Build a SQLite URL with `?mode=ro` so the migrator cannot mutate the source -even by accident: - -```rust -let src_url = format!( - "sqlite://{}?mode=ro", - plan.sqlite_path.display(), -); -let src = SqliteMemoryStore::open_url(&src_url).await?; -``` - -`SqliteMemoryStore::open_url` is added by `0002d-store-impl-bodies.md` as a -small wrapper over the existing `new` that accepts a fully-formed URL. If the -file does not exist, `MemoryStoreError::Init` propagates. - -The source store still runs its own startup-time migrations in `?mode=ro`? -No -- read-only mode rejects writes. The migrator therefore opens the source -twice if the live source DB is older than V15: once writable to bring its -schema forward to V15 (so the D7+D8 columns are present), then re-opens -read-only. Detection: query `user_version` on the source DB before opening -the read-only handle. If it is below V15 and `--allow-source-upgrade` is set, -open writable, run `SqliteMemoryStore::new` (which runs migrations), close, -and re-open read-only. If `--allow-source-upgrade` is not set, fail with a -clear error pointing at the flag. Default: not set; the user must opt in to -modifying their source. - -### Step 2. Embedding model registry compatibility check - -Read both registries: - -```rust -let src_sig = src.registered_model().await?; -let actual = embedder.model_signature(); // ModelSignature -``` - -If `src_sig` is `Some` and disagrees with `actual` (any of `name`, -`dimension`, `hash`), return: - -```rust -MemoryStoreError::ModelMismatch { - registered_name: src_sig.name, - registered_dim: src_sig.dimension, - registered_hash: src_sig.hash, - actual_name: actual.name, - actual_dim: actual.dimension, - actual_hash: actual.hash, -} -``` - -The CLI translates this into a message that mentions `0002g`'s `--reembed` -command as the recovery path. Do NOT silently re-encode here; that is a -separate concern with its own flag set, performance profile, and HNSW -rebuild. - -If `src_sig` is `None` (source never had an embedding model -- empty DB or -pre-Phase-1), use the actual embedder's signature for the destination -registry. Memory rows whose `embedding` column is NULL stay NULL on the -destination side. - -### Step 3. Open destination Postgres - -```rust -let dst = PgMemoryStore::connect(&plan.postgres_url, plan.max_connections).await?; -``` - -`PgMemoryStore::connect` (per `0002d-store-impl-bodies.md`) runs the -`sqlx::migrate!` macro internally, which idempotently applies `0001_init` -and `0002_hnsw`. Re-running the migrator against an already-initialised -destination is fine. - -Stamp the registry on the destination: - -```rust -let sig = src_sig.unwrap_or_else(|| embedder.model_signature()); -dst.register_model(&sig).await?; -``` - -`register_model` is idempotent in the Postgres backend: it upserts the single -registry row, and (per ADR 0002 D2) it runs the `ALTER TABLE knowledge_nodes -ALTER COLUMN embedding TYPE vector($N)` typmod stamp inside its body. The -ALTER is itself idempotent: pgvector accepts the same typmod twice as a no-op. - -### Step 4. Verify schema - -Not really a separate step -- `PgMemoryStore::connect` already calls -`sqlx::migrate!` and the `register_model` call already stamps the typmod. -Listed here for documentation: this is the point at which the destination is -known to be schema-correct for the source's embedding dimension. - -### Step 5. Copy `users`, `groups`, `group_memberships` first - -These tables exist for both pre-Phase-3 and Phase-3-aware sources because -ADR 0002 D7 reserved them in V15 of the SQLite schema. Phase 2 single-user -deployments have exactly one user row (`local`) and zero groups, but the -migrator does not assume that: it copies whatever is present. - -The bootstrap user `00000000-0000-0000-0000-000000000001` is inserted by -`0001_init.up.sql` on the destination. The source's bootstrap row collides -with the destination's; `ON CONFLICT (id) DO NOTHING` resolves the collision -silently. - -Pseudocode: - -```rust -let mut tx = dst.pool().begin().await?; -let mut report = MigrationReport::default(); - -for batch in src.stream_users(plan.batch_size).await? { - for u in batch? { - sqlx::query!( - "INSERT INTO users (id, handle, display_name, created_at, metadata) \ - VALUES ($1, $2, $3, $4, $5) \ - ON CONFLICT (id) DO NOTHING", - u.id, u.handle, u.display_name, u.created_at, u.metadata, - ).execute(&mut *tx).await?; - report.users_copied += 1; - } -} -tx.commit().await?; -``` - -Repeat the same shape for `groups` and `group_memberships`. The membership -table has a composite primary key `(user_id, group_id)`: - -```rust -"INSERT INTO group_memberships (user_id, group_id, role, joined_at) \ - VALUES ($1, $2, $3, $4) \ - ON CONFLICT (user_id, group_id) DO NOTHING", -``` - -The `stream_users` / `stream_groups` / `stream_memberships` reader methods on -`SqliteMemoryStore` are introduced by `0002d-store-impl-bodies.md`. They -return `BoxStream>>` to keep the migrator -backend-agnostic. - -If the source SQLite predates V15 -- the V15 migration is the one that -introduces these tables -- they simply do not exist. The reader detects -their absence at open time and returns an empty stream. See "Source schema -variants" below. - -### Step 6. Copy `knowledge_nodes` in batches - -Stream the source ordered by `(created_at, id)`. The cursor key is the -last-seen `(created_at, id)` pair; the reader uses keyset pagination so -restarts pick up where the previous run left off: - -```sql -SELECT ... -FROM knowledge_nodes -WHERE (created_at, id) > ($cursor_ts, $cursor_id) -ORDER BY created_at, id -LIMIT $batch_size -``` - -For each batch: - -```rust -let mut tx = dst.pool().begin().await?; -for record in batch { - // D7 + D8 columns are all on MemoryRecord by Phase 2. - let groups: Vec = record.shared_with_groups.clone(); - - let result = sqlx::query!( - "INSERT INTO knowledge_nodes ( \ - id, content, node_type, tags, embedding, \ - created_at, updated_at, metadata, \ - owner_user_id, visibility, shared_with_groups, \ - codebase, domains, domain_scores \ - ) VALUES ( \ - $1, $2, $3, $4, $5::vector, \ - $6, $7, $8, \ - $9, $10, $11, \ - $12, $13, $14::jsonb \ - ) \ - ON CONFLICT (id) DO NOTHING", - record.id, - record.content, - record.node_type, - &record.tags, - record.embedding.as_deref().map(pgvector::Vector::from), - record.created_at, - record.updated_at, - record.metadata, - record.owner_user_id, - record.visibility, - &groups, - record.codebase, - &record.domains, - serde_json::to_value(&record.domain_scores) - .unwrap_or(serde_json::Value::Object(Default::default())), - ) - .execute(&mut *tx) - .await; - - match result { - Ok(_) => report.memories_copied += 1, - Err(e) => report - .errors - .push((record.id, MemoryStoreError::from(e))), - } -} -tx.commit().await?; -``` - -Notes: - -- `embedding` is `Option>` on `MemoryRecord`. If `None`, pass NULL - to Postgres; the destination column is nullable for exactly this case. -- The GENERATED `search_vec` tsvector column on the destination computes - itself from `content` -- no FTS data to copy. -- Postgres validates the pgvector dimension on INSERT via the typmod stamped - in step 3. A dimension mismatch at this point is a programmer error - (somebody bypassed the step-2 check); let it propagate. - -Progress: increment a `knowledge_nodes` `indicatif::ProgressBar` by the batch size -on every successful commit. Log INFO every 1000 rows via `tracing`: - -```rust -if report.memories_copied % 1000 == 0 { - tracing::info!( - memories_copied = report.memories_copied, - "migrate: knowledge_nodes batch committed", - ); -} -``` - -### Step 7. Copy `scheduling` - -One row per memory. Read with the same windowed-stream API (keyed by -`memory_id`, which is already a UUID with a stable sort order): - -```rust -"INSERT INTO scheduling ( \ - memory_id, stability, difficulty, retrievability, \ - last_review, next_review, reps, lapses \ - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) \ - ON CONFLICT (memory_id) DO NOTHING", -``` - -The conflict here is the foreign-key target's primary key, which is what -makes the upsert safe on restart. Increment `report.scheduling_rows`. - -### Step 8. Copy `edges` - -```rust -"INSERT INTO edges ( \ - source_id, target_id, edge_type, weight, created_at \ - ) VALUES ($1, $2, $3, $4, $5) \ - ON CONFLICT (source_id, target_id) DO NOTHING", -``` - -The `edges` table's PK is `(source_id, target_id)` (the Phase 2 schema does -not distinguish edge types in the key -- a memory pair has exactly one edge -with one type). Increment `report.edges_copied`. - -### Step 9. Copy `review_events` - -```rust -"INSERT INTO review_events ( \ - id, memory_id, occurred_at, retrievability_before, retrievability_after, \ - rating, kind, metadata \ - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) \ - ON CONFLICT (id) DO NOTHING", -``` - -`review_events` is an append-only log. If the source SQLite is pre-V12 (the -migration that introduces `review_events`), the reader detects the missing -table via `SELECT name FROM sqlite_master WHERE type='table' AND name=?` -returning empty and yields an empty stream. The migrator increments -`report.review_events_copied` only when rows actually arrive. - -### Step 10. Copy `domains` - -Phase 4 table. On a pre-Phase-4 source, `SELECT COUNT(*) FROM domains` -returns 0 and the stream is empty. The migrator does not skip the table; -it iterates and finds nothing. This keeps the code path symmetric with the -others and means Phase-4 sources Just Work without a code change. - -```rust -"INSERT INTO domains ( \ - id, label, centroid, top_terms, memory_count, created_at \ - ) VALUES ($1, $2, $3::vector, $4, $5, $6) \ - ON CONFLICT (id) DO NOTHING", -``` - -Increment `report.domains_copied`. - -### Step 11. Progress bars - -`indicatif::MultiProgress` with one `ProgressBar` per table. Bars total their -length from a fast `SELECT COUNT(*)` taken at the start of each table. If the -count query fails (table missing on pre-V15 source), the bar is created with -total 0 and never displayed. - -Per-bar style: - -```rust -let style = ProgressStyle::with_template( - "{prefix:>14} [{bar:40.cyan/blue}] {pos}/{len} ({per_sec}, eta {eta})", -) -.unwrap() -.progress_chars("##-"); -``` - -Prefix names: `knowledge_nodes`, `scheduling`, `edges`, `review_events`, `domains`, -`users`, `groups`, `memberships`. - -### Step 12. Dry-run path - -If `plan.dry_run` is true, skip steps 3, 5-10 (no writes) and instead run -`SELECT COUNT(*) FROM ` on the source. Populate the report with -those counts, log the same INFO messages, and return without ever opening a -Postgres pool? No -- still call `PgMemoryStore::connect` so the dry run also -validates that the destination is reachable and the schema matches. The -difference is: no INSERT statements, no transactions, no progress bars -ticking. Print the report at the end and exit. - ---- - -## Idempotency - -Re-running `vestige migrate copy ...` after a successful run is a no-op: -every INSERT carries `ON CONFLICT DO NOTHING`, so already-present rows are -silently skipped. The report counts grow by zero; the destination is -unchanged. - -Re-running after a crash mid-batch is safe in the same way. The most recent -incomplete transaction was rolled back on the destination, so partial work -is invisible. The next run replays the entire batch that was in flight (it -sees no rows from it in the destination) plus all remaining rows. - -If a single row is corrupted on the source (e.g., a UUID column with a -non-UUID string, malformed `metadata` JSON, etc.), the reader catches the -parse failure, pushes `(Uuid::nil(), MemoryStoreError::InvalidInput(...))` -to `report.errors`, and continues. The migrator never aborts on a single bad -row. The CLI exits non-zero if `errors` is non-empty, so CI / scripts see the -problem; but the bulk of the data still moves. - -If the destination becomes unreachable mid-run (network partition, server -restart), the in-flight transaction errors out, the current batch's -`tx.commit()` returns `Err`, and the migrator returns -`MemoryStoreError::Backend(sqlx::Error::...)`. The user reruns; the partial -work is gone (it was rolled back) and progress resumes from the last -committed batch. - ---- - -## Embedding model match check - -Read both registries up front (step 2). The check is exact: name AND -dimension AND hash must match. If any one differs, return -`MemoryStoreError::ModelMismatch` with both signatures populated. - -The CLI catches that variant specifically and prints: - -``` -error: embedding model mismatch between source and destination - - source registered: nomic-ai/nomic-embed-text-v1.5 (dim 768, hash abcd...) - embedder presented: BAAI/bge-large-en-v1.5 (dim 1024, hash 1234...) - -Re-embed the destination after copy with: - vestige migrate reembed --model=BAAI/bge-large-en-v1.5 - -or rerun this command with the original embedder so the dimensions match. -``` - -The migrator does NOT call into the embedder during copy. Vectors flow from -SQLite BLOB to Postgres `vector` unchanged. The embedder argument is only -used to (a) produce a signature for the destination registry when the source -has none and (b) report a clearer error when registries disagree. - -Re-embedding lives in `0002g-reembed.md`. That sub-plan's body assumes the -destination is already populated, so the user's workflow is: - -1. `vestige migrate copy ...` (this sub-plan; may fail with `ModelMismatch`) -2. `vestige migrate copy --reembed-after ...` -- not added in Phase 2; the - user runs the two commands in sequence -3. `vestige migrate reembed --model=...` (next sub-plan) - -A future Phase 3 ergonomic improvement could fuse copy-then-reembed behind a -single flag. Not in Phase 2 scope. - ---- - -## CLI wiring - -Edit `crates/vestige-mcp/src/bin/cli.rs`. Add a feature-gated `Migrate` -variant to the existing `Commands` enum. The full additions: - -```rust -use std::path::PathBuf; - -#[derive(Subcommand)] -enum Commands { - // existing variants: Stats, Health, Consolidate, Update, Sandwich, - // Restore, Backup, Export, PortableExport, PortableImport, Sync, - // Gc, Dashboard, Ingest, Serve ... - - /// Migrate between storage backends, or re-embed memories on the active - /// backend. Available when compiled with --features postgres-backend. - #[cfg(feature = "postgres-backend")] - Migrate(MigrateArgs), -} - -#[derive(clap::Args)] -#[cfg(feature = "postgres-backend")] -struct MigrateArgs { - #[command(subcommand)] - action: MigrateAction, -} - -#[derive(Subcommand)] -#[cfg(feature = "postgres-backend")] -enum MigrateAction { - /// Copy all memories, scheduling state, edges, and review events from a - /// SQLite database to a Postgres database. Idempotent. - Copy { - /// Source backend name. Currently only "sqlite" is accepted. - #[arg(long)] - from: String, - - /// Destination backend name. Currently only "postgres" is accepted. - #[arg(long)] - to: String, - - /// Path to the source SQLite database file. - #[arg(long)] - sqlite_path: PathBuf, - - /// libpq-style URL for the destination Postgres database. - #[arg(long)] - postgres_url: String, - - /// Rows per Postgres transaction. - #[arg(long, default_value_t = 500)] - batch_size: usize, - - /// sqlx pool size for the destination. - #[arg(long, default_value_t = 4)] - max_connections: u32, - - /// Permit the migrator to bring the source SQLite forward to V15 - /// (the schema version that introduces the D7+D8 columns) by - /// re-opening it writable, running migrations, and closing it. - /// Without this flag, a pre-V15 source fails fast. - #[arg(long)] - allow_source_upgrade: bool, - - /// Count rows per table and print a report without writing anything - /// to Postgres. - #[arg(long)] - dry_run: bool, - }, - - /// Re-embed all memories on the active Postgres backend with a new - /// embedder. See sub-plan 0002g for the body. - Reembed { - /// Embedder name (e.g., "BAAI/bge-large-en-v1.5"). Resolved via - /// the Phase 1 embedder factory. - #[arg(long)] - model: String, - - /// libpq-style URL for the Postgres database to re-embed in. - #[arg(long)] - postgres_url: String, - - /// Rows per embedder batch. - #[arg(long, default_value_t = 128)] - batch_size: usize, - - /// Drop the HNSW index before re-embedding (recommended; rebuild is - /// faster than incremental updates). - #[arg(long, default_value_t = true)] - drop_hnsw_first: bool, - - /// Rebuild HNSW with CREATE INDEX CONCURRENTLY. Slower but does not - /// hold AccessExclusiveLock. - #[arg(long)] - concurrent_index: bool, - - /// sqlx pool size for the destination. - #[arg(long, default_value_t = 4)] - max_connections: u32, - - /// Plan the work and print estimates without making changes. - #[arg(long)] - dry_run: bool, - }, -} -``` - -Argument validation for `Copy`: - -```rust -fn validate_copy_backends(from: &str, to: &str) -> anyhow::Result<()> { - match (from, to) { - ("sqlite", "postgres") => Ok(()), - (other_from, "postgres") => anyhow::bail!( - "unsupported source backend: {}. Only 'sqlite' is accepted as --from in Phase 2.", - other_from, - ), - ("sqlite", other_to) => anyhow::bail!( - "unsupported destination backend: {}. Only 'postgres' is accepted as --to in Phase 2.", - other_to, - ), - (other_from, other_to) => anyhow::bail!( - "unsupported migration direction: {} -> {}. Only 'sqlite' -> 'postgres' is accepted in Phase 2.", - other_from, other_to, - ), - } -} -``` - -Wire the new variant in the `main` match: - -```rust -match cli.command { - // ... existing arms ... - - #[cfg(feature = "postgres-backend")] - Commands::Migrate(args) => match args.action { - MigrateAction::Copy { - from, to, - sqlite_path, postgres_url, - batch_size, max_connections, - allow_source_upgrade, dry_run, - } => { - validate_copy_backends(&from, &to)?; - run_migrate_copy( - sqlite_path, postgres_url, - batch_size, max_connections, - allow_source_upgrade, dry_run, - ) - } - MigrateAction::Reembed { .. } => { - // Body implemented in sub-plan 0002g. - run_migrate_reembed(/* ... */) - } - }, -} -``` - -`run_migrate_copy` is a thin wrapper that: - -1. Builds a `SqliteToPostgresPlan` from the clap args. -2. Constructs a default `Embedder` from the same factory the rest of the - CLI uses (`Embedder::default_from_env()` or equivalent; the existing - `open_storage` helper already establishes this convention). -3. Starts a tokio runtime if one is not already running. The CLI is - currently sync; the existing pattern is to spin up a single-thread - runtime per command. Reuse that. -4. Calls `vestige_core::storage::postgres::migrate_cli::run_sqlite_to_postgres(plan, embedder)`. -5. Prints the report and exits with the appropriate status code. - -Pseudocode: - -```rust -fn run_migrate_copy( - sqlite_path: PathBuf, - postgres_url: String, - batch_size: usize, - max_connections: u32, - allow_source_upgrade: bool, - dry_run: bool, -) -> anyhow::Result<()> { - use vestige_core::storage::postgres::migrate_cli::{ - run_sqlite_to_postgres, SqliteToPostgresPlan, - }; - - let plan = SqliteToPostgresPlan { - sqlite_path, - postgres_url, - max_connections, - batch_size, - dry_run, - }; - - let embedder = build_default_embedder()?; - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build()?; - - let report = runtime.block_on(run_sqlite_to_postgres(plan, embedder)) - .context("migrate copy failed")?; - - print_migration_report(&report); - - if report.errors.is_empty() { - Ok(()) - } else { - anyhow::bail!("migrate copy completed with {} row errors", report.errors.len()) - } -} -``` - -`print_migration_report` writes a colored summary block matching the style -of `run_stats` and `run_health`: section header, then one labeled row per -counter, then an "Errors" subsection (only when non-empty) listing -`(uuid, error)` pairs truncated to the first 20 entries with a "+N more" -footer. - ---- - -## Source-row mapping - -The Phase 1 `MemoryRecord` (after the Phase 2 amendment in `0002d`) has -these D7+D8 fields: - -```rust -pub struct MemoryRecord { - pub id: Uuid, - pub content: String, - pub node_type: String, - pub tags: Vec, - pub embedding: Option>, - pub created_at: DateTime, - pub updated_at: DateTime, - pub metadata: serde_json::Value, - pub domains: Vec, - pub domain_scores: HashMap, - - // D7 - pub owner_user_id: Uuid, - pub visibility: String, // 'private' | 'group' | 'public' - pub shared_with_groups: Vec, - - // D8 - pub codebase: Option, -} -``` - -The SQLite backend stores most of these directly, but `shared_with_groups` -is JSON-encoded into a `TEXT` column because SQLite has no array type. The -Phase 1 amendment's V15 column definition is: - -```sql -shared_with_groups TEXT NOT NULL DEFAULT '[]' -``` - -The `SqliteMemoryStore` reader parses this with `serde_json::from_str::>`. -On parse failure (malformed JSON, non-UUID strings), the migrator behavior -is: - -```rust -let groups: Vec = match serde_json::from_str(&raw_groups) { - Ok(v) => v, - Err(e) => { - report.errors.push(( - row.id, - MemoryStoreError::InvalidInput(format!( - "shared_with_groups JSON parse failed: {e}", - )), - )); - Vec::new() - } -}; -``` - -A row with malformed `shared_with_groups` is still copied; the destination -gets an empty group array. This keeps the migrator on the side of "best -effort, never lose memories". - -The `visibility` column is `TEXT NOT NULL DEFAULT 'private'` on both sides. -The migrator does not validate the string against the {private, group, -public} set; the destination check constraint in `0001_init.up.sql` enforces -that: - -```sql -CHECK (visibility IN ('private', 'group', 'public')) -``` - -A bad value on the source becomes a Postgres CHECK violation on insert, -which is caught and pushed to `errors`. - -`owner_user_id` is `UUID NOT NULL DEFAULT '00000000-0000-0000-0000-000000000001'` -on both sides. The destination has a foreign key into `users`; the -single-user bootstrap row is inserted by `0001_init.up.sql`. Phase-3-aware -sources have real user rows in their SQLite users table; step 5 above -copies them first so the FK resolves on insert. - -`codebase` is nullable `TEXT` on both sides. Direct copy, no special -handling. - -`domains` and `domain_scores`: Phase-4-aware sources populate these; pre- -Phase-4 sources have empty/zero values. Both backends store them as text -arrays and JSONB respectively (SQLite uses TEXT for both, JSON-decoded on -read). Direct copy. - -`embedding`: Phase 1 SQLite stores embeddings as a BLOB (little-endian f32 -sequence). The Phase 1 reader decodes to `Vec`. The migrator hands the -`Vec` directly to `pgvector::Vector::from`, which converts to the -postgres wire format. No precision loss. - -`metadata`: SQLite TEXT containing JSON. The reader parses to -`serde_json::Value`. The migrator passes it through to a JSONB column. -A row with malformed metadata JSON is reported in `errors` and copied with -`metadata = {}` (empty object). - -### Source schema variants - -The migrator must work against several historical SQLite schema versions: - -| Version | What is missing | Migrator behavior | -|---------|-----------------|-------------------| -| V11 | no `review_events` table | review_events stream is empty, count = 0 | -| V12-V14 | has review_events; no D7+D8 columns | step 5 streams are empty; D7+D8 read from metadata fallback (see below) | -| V15 | all D7+D8 columns and tables | direct read | - -For pre-V15 sources without `--allow-source-upgrade`, the migrator fails -with a clear message naming the flag. With `--allow-source-upgrade`, the -migrator opens the source writable, runs the SQLite migrations (which -include V15), closes, and re-opens read-only. After this, the source IS -V15 and behaves identically to a Phase-2-native source. - -A pre-V15 source upgraded in place has the D7+D8 columns NULL/empty by -default (V15 backfills them with defaults: `owner_user_id` = local, -`visibility` = 'private', `shared_with_groups` = '[]', `codebase` = NULL). -The migrator copies those defaults to the destination unchanged. - ---- - -## Tracing / logs - -Emit INFO logs at three points: - -1. Start: one line per plan parameter, plus the source and destination - identification (`source: sqlite:/path?mode=ro, destination: postgres://...`). -2. Mid-flight: every 1000 rows on the `knowledge_nodes` table only. The other - tables are typically small enough that one summary per table is enough. -3. End: print the full `MigrationReport` at INFO level, plus duration. - -```rust -let started = Instant::now(); -tracing::info!( - sqlite_path = %plan.sqlite_path.display(), - postgres_url = %obfuscate_password(&plan.postgres_url), - batch_size = plan.batch_size, - dry_run = plan.dry_run, - "migrate: starting sqlite -> postgres copy", -); - -// ... per-table sections ... - -tracing::info!( - memories = report.memories_copied, - scheduling = report.scheduling_rows, - edges = report.edges_copied, - review_events = report.review_events_copied, - domains = report.domains_copied, - users = report.users_copied, - groups = report.groups_copied, - memberships = report.group_memberships_copied, - errors = report.errors.len(), - duration_ms = started.elapsed().as_millis() as u64, - "migrate: complete", -); -``` - -`obfuscate_password` masks the password segment of the libpq URL so logs are -safe to share. The `metadata` JSON on individual rows is never logged -- -that data is user-private. - -Per-row errors are logged at WARN with the row id and the error string. The -counts in the final INFO line tell the user how many to expect. - ---- - -## Verification - -Integration test under `tests/phase_2/migrate_test.rs`. Add it next to -`tests/phase_2/common/mod.rs` (the testcontainer harness from `0002h`). - -The test: - -1. Creates an in-memory `SqliteMemoryStore` at a tempfile path. Runs - migrations to V15. -2. Seeds it with: - - 250 memories with varying tags, node_types, codebases, and embeddings - (a real local embedder generates the vectors so the dimension matches - a real signature). - - 250 scheduling rows (one per memory). - - 50 edges between random memory pairs. - - 50 review events. - - Optional: 3 user rows + 2 groups + 4 memberships to exercise the D7 - path. - - Optional: 5 domain rows to exercise the Phase 4 path. -3. Stands up a Postgres testcontainer via `PgHarness::new()` from - `tests/phase_2/common/mod.rs`. -4. Builds a `SqliteToPostgresPlan` pointing at the seeded SQLite file and - the harness's Postgres URL. -5. Calls `run_sqlite_to_postgres(plan, embedder).await`. -6. Asserts: - - `report.memories_copied == 250` - - `report.scheduling_rows == 250` - - `report.edges_copied == 50` - - `report.review_events_copied == 50` - - `report.users_copied == 4` (3 plus bootstrap) - - `report.groups_copied == 2` - - `report.group_memberships_copied == 4` - - `report.domains_copied == 5` - - `report.errors.is_empty()` -7. Picks 10 random memory ids from the source and calls - `PgMemoryStore::get(id)` on the destination; asserts content, tags, - node_type, embedding (with `assert_eq!` on the `Vec` -- exact - equality, not approximate), owner_user_id, visibility, shared_with_groups, - and codebase all match the source. -8. Re-runs the migrator with the same plan. Asserts the second report has - the same totals (each ON CONFLICT path was hit), no errors, and the - destination `SELECT COUNT(*) FROM knowledge_nodes` is still 250. -9. Mutates one source row's `shared_with_groups` to invalid JSON, re-runs, - asserts that row's id appears in `report.errors` and the destination - row's `shared_with_groups` is `{}` (empty). -10. Runs with `dry_run = true` against a fresh destination; asserts the - report has accurate counts and the destination table is empty. - -Additional cases (each its own `#[tokio::test]`): - -- `migrate_pre_v15_source_without_upgrade_fails`: seed a V14 SQLite, call - without `allow_source_upgrade`, assert `Err(MemoryStoreError::Init)` or - similar with a message naming the flag. -- `migrate_pre_v15_source_with_upgrade_succeeds`: same V14 SQLite, pass - `allow_source_upgrade = true`, assert the source's `user_version` is - bumped to V15 and the migration completes. -- `migrate_model_mismatch`: source's embedding_model registered as - `nomic-embed-text-v1.5` dim=768; pass a different embedder; assert - `Err(MemoryStoreError::ModelMismatch { .. })` with both signatures - populated. - -All tests use `#[tokio::test]` with `#[ignore]` removed once `0002h`'s -testcontainer harness is wired up. CI runs them in the -`postgres-backend` feature matrix only. - ---- - -## Acceptance criteria - -The sub-plan is complete when: - -1. `cargo build --features postgres-backend -p vestige-core` succeeds. -2. `cargo build --features postgres-backend -p vestige-mcp` succeeds. -3. `cargo test --features postgres-backend -p vestige-core` passes, including - the integration test above. -4. `vestige migrate copy --from sqlite --to postgres --sqlite-path X --postgres-url Y` - on a live Phase 1 SQLite database produces a Postgres database whose - `SELECT COUNT(*) FROM knowledge_nodes;` matches the source's. Manual smoke test - against the user's own `~/.vestige/vestige.db` is the gold-standard check. -5. Re-running the same command produces zero new rows and zero errors. -6. `vestige migrate copy --from sqlite --to postgres ... --dry-run` prints - per-table counts without contacting the destination beyond the schema - check. -7. `vestige migrate copy --from --to postgres ...` rejects with a - clear message naming the supported pairs. -8. `vestige migrate copy ...` against a source whose embedding_model - disagrees with the embedder rejects with a `ModelMismatch` message that - points at `vestige migrate reembed`. -9. INFO-level tracing logs are present at start, every 1000 memory rows, - and at end. Passwords in URLs are not logged in cleartext. -10. The `Reembed` clap variant compiles with `todo!()` or a stub body and - is filled in by `0002g-reembed.md`. diff --git a/docs/plans/0002g-reembed.md b/docs/plans/0002g-reembed.md deleted file mode 100644 index 3053af3..0000000 --- a/docs/plans/0002g-reembed.md +++ /dev/null @@ -1,843 +0,0 @@ -# Sub-plan 0002g -- Re-embed driver and `vestige migrate reembed` CLI - -**Status**: Draft -**Master plan**: [0002-phase-2-postgres-backend.md](0002-phase-2-postgres-backend.md) -**ADR**: [0002-phase-2-execution.md](../adr/0002-phase-2-execution.md) -**Predecessor**: [0002f-migrate-cli.md](0002f-migrate-cli.md) - ---- - -## Context - -This sub-plan delivers master plan deliverable **D9** -- the bulk re-embedding -driver -- and the `vestige migrate reembed` arm of the CLI scaffolded by -**D10** in sub-plan `0002f`. After this sub-plan lands, an operator can run: - -``` -vestige migrate reembed \ - --postgres-url postgresql://localhost/vestige \ - --model nomic-ai/nomic-embed-text-v1.5 \ - --dimension 768 -``` - -and the running Postgres backend will: - -1. Stream every row out of `knowledge_nodes`. -2. Re-encode `content` with the requested `Embedder`. -3. Write the new vectors back. -4. Adjust the pgvector typmod if the new dimension differs from the old. -5. Rebuild the HNSW index. -6. Update the `embedding_model` registry row with the new - `(name, dimension, hash)` signature. - -The whole operation runs as a single offline maintenance step. Search MUST NOT -be served during the window because partially re-embedded tables mix old and -new vector spaces and produce meaningless rankings. - -This sub-plan deliberately does NOT: - -- Migrate vectors between backends. That's `0002f` (SQLite -> Postgres copy). -- Invent new embedder constructors. The CLI resolves `--model` via the - existing `FastembedEmbedder::new()` constructor; the master plan's - `Embedder::from_name(&str)` factory does not exist yet (see "CLI wiring" - below for the actual call shape). -- Add a `vestige migrate reembed --sqlite-path ...` arm. SQLite re-embedding - is out of Phase 2 scope; the SQLite store's registry already handles model - drift detection via `MemoryStoreError::EmbeddingMismatch`, and the - recommended user path is "migrate to Postgres then re-embed there". - ---- - -## Dependencies - -- `0002a-skeleton-and-feature-gate.md` -- `PgMemoryStore` exists. -- `0002b-pool-and-config.md` -- `connect` builds a real `PgPool`. -- `0002c-migrations.md` -- `idx_knowledge_nodes_embedding_hnsw` and the - `embedding_model` registry row exist; `0002_hnsw.up.sql` defines the index. -- `0002d-store-impl-bodies.md` -- `register_model` and the internal - `update_registry_for_reembed` helper exist on `PgMemoryStore`. -- `0002e-hybrid-search.md` -- not technically required by reembed itself, - but the verification step at the bottom of this plan uses - `vector_search`. -- `0002f-migrate-cli.md` -- provides the `clap` scaffolding under - `vestige migrate ...`. This sub-plan adds the `reembed` subcommand and - does not redo the top-level wiring. - -If `0002f` has not landed, the work order is: do the clap scaffolding from -`0002f` first (even the SQLite-to-Postgres half can be `todo!()` initially), -then this sub-plan. - ---- - -## Audit step (do this first) - -Before writing `reembed.rs`, confirm the live shape of the supporting code. -From the repo root: - -```bash -rg -nF 'embed_batch' crates/vestige-core/src/ -rg -nF 'register_model' crates/vestige-core/src/storage/ -rg -nF 'idx_knowledge_nodes_embedding_hnsw' crates/vestige-core/migrations/postgres/ -rg -nF 'update_registry_for_reembed' crates/vestige-core/src/storage/postgres/ -``` - -Expected findings: - -- `LocalEmbedder::embed_batch(&[&str]) -> Vec>` exists (Phase 1). -- `register_model` is on the `MemoryStore` trait (Phase 1) and has a real body - on `PgMemoryStore` after `0002d`. -- `idx_knowledge_nodes_embedding_hnsw` is the canonical HNSW index name. If - `0002c-migrations.md` chose a different name, update the SQL constants in - `reembed.rs` accordingly. -- `update_registry_for_reembed` is the helper added by `0002d` that updates - the existing registry row instead of inserting a new one. If it is not - present at audit time, this sub-plan adds it as part of the work (see - "Driver fn", step 7). - ---- - -## Cargo manifest additions - -No new crates. `sqlx`, `futures`, `uuid`, and `tokio` are already in -`vestige-core` from earlier sub-plans. `tracing` is already used throughout -Phase 2. - -The CLI binary (`vestige-mcp/src/bin/cli.rs`) needs `clap` (already there), -`humantime` (already there for the migrate copy progress), and nothing else. - ---- - -## Plan struct - -`crates/vestige-core/src/storage/postgres/reembed.rs`: - -```rust -#![cfg(feature = "postgres-backend")] - -/// Tunables for the re-embed driver. -/// -/// Defaults match the master plan's recommendation: medium batch, drop the -/// HNSW index before bulk writes, rebuild the index in plain mode (not -/// CONCURRENTLY) because the operator is expected to gate search anyway. -#[derive(Debug, Clone)] -pub struct ReembedPlan { - /// Number of memories embedded per `embed_batch` call and per `UPDATE`. - /// Default 128. Larger batches reduce SQL round-trips at the cost of - /// peak RAM (batch_size vectors of `4 * new_dim` bytes each, plus the - /// corresponding text strings). - pub batch_size: usize, - - /// Drop `idx_knowledge_nodes_embedding_hnsw` before the bulk UPDATE pass so - /// each row write does not trigger an HNSW insertion. The index is - /// rebuilt after all rows are written. Default true. - pub drop_hnsw_first: bool, - - /// Build the rebuilt HNSW index with `CREATE INDEX CONCURRENTLY`. - /// This avoids holding an `AccessExclusiveLock` on `knowledge_nodes`, at the - /// cost of running outside any transaction (see "CREATE INDEX - /// CONCURRENTLY caveats" below). Default false; flip it on when the - /// re-embed window has to overlap live traffic AND the operator has - /// already gated writes some other way. - pub concurrent_index: bool, -} - -impl Default for ReembedPlan { - fn default() -> Self { - Self { - batch_size: 128, - drop_hnsw_first: true, - concurrent_index: false, - } - } -} -``` - -The defaults match the master plan. `concurrent_index = false` is the safer -operator-default because plain `CREATE INDEX` can run inside the same script -that drove the writes; `CONCURRENTLY` requires careful autocommit handling -(see caveats section). - ---- - -## Report struct - -```rust -/// Summary of one re-embed run. Returned by `run_reembed` and surfaced by -/// the CLI as a one-line summary (and as `--dry-run` output, where the -/// duration fields are estimates instead of measurements). -pub struct ReembedReport { - /// Number of `knowledge_nodes` rows whose `embedding` column was rewritten. - /// Includes rows whose embedding was previously NULL. - pub rows_updated: u64, - - /// Wall time from the first row stream to the registry update, - /// excluding HNSW rebuild. Seconds with sub-millisecond precision. - pub duration_secs: f64, - - /// Wall time of the HNSW rebuild step alone. Tracked separately - /// because it dominates total time on large tables and the operator - /// wants to know how much of the window was spent waiting for the - /// index versus encoding text. - pub index_rebuild_secs: f64, -} -``` - -The CLI prints all three fields. Tests assert on `rows_updated` only; -durations are non-deterministic. - ---- - -## Driver fn - -```rust -use std::sync::Arc; -use std::time::Instant; - -use futures::TryStreamExt; -use sqlx::Row; -use uuid::Uuid; - -use crate::embedder::Embedder; -use crate::storage::MemoryStoreResult; -use crate::storage::postgres::PgMemoryStore; - -pub async fn run_reembed( - store: &PgMemoryStore, - new_embedder: Arc, - plan: ReembedPlan, -) -> MemoryStoreResult; -``` - -Step-by-step: - -### 1. No-op check (registry comparison) - -Read the current registry row. If `(name, dimension, hash)` already matches -`new_embedder.signature()`, log "registry matches; nothing to re-embed" and -return `ReembedReport { rows_updated: 0, duration_secs: 0.0, -index_rebuild_secs: 0.0 }`. - -```rust -let current = store.registered_model().await?; // Phase 1 trait method -let target = new_embedder.signature(); -if current.is_some_and(|c| c == target) { - tracing::info!("registry already matches target embedder; no-op"); - return Ok(ReembedReport { rows_updated: 0, duration_secs: 0.0, index_rebuild_secs: 0.0 }); -} -``` - -This is the cheapest precondition. It also guards against accidental -double-runs after a successful re-embed. - -### 2. Drop HNSW (optional) - -If `plan.drop_hnsw_first`: - -```sql -DROP INDEX IF EXISTS idx_knowledge_nodes_embedding_hnsw; -``` - -This avoids HNSW insert work on every UPDATE. Recommended default. The index -gets rebuilt in step 6. - -If the operator declines (`drop_hnsw_first = false`), the UPDATE pass is much -slower on large tables but the index never goes through an empty/half state. -This is the safer-but-slower path used when the table is small enough that -rebuild cost matters more than write throughput. - -### 3. Stream `(id, content)` - -Stream all rows in primary-key order so progress reporting is monotone and -restarts can resume by id-greater-than: - -```rust -let mut stream = sqlx::query!( - "SELECT id, content FROM knowledge_nodes ORDER BY id" -).fetch(store.pool()); - -let mut batch_ids: Vec = Vec::with_capacity(plan.batch_size); -let mut batch_texts: Vec = Vec::with_capacity(plan.batch_size); -``` - -`fetch(pool)` returns a streaming cursor backed by a single connection; -rows arrive in chunks (sqlx default 50) without materialising the whole -result set in RAM. - -### 4. Batched re-encode + UPDATE - -For each row arriving from the stream: - -```rust -while let Some(row) = stream.try_next().await? { - batch_ids.push(row.id); - batch_texts.push(row.content); - if batch_ids.len() >= plan.batch_size { - flush_batch(&new_embedder, store, &mut batch_ids, &mut batch_texts).await?; - } -} -if !batch_ids.is_empty() { - flush_batch(&new_embedder, store, &mut batch_ids, &mut batch_texts).await?; -} -``` - -`flush_batch` builds a `Vec<&str>` view, calls `new_embedder.embed_batch`, -then writes the result back. The Phase 1 `LocalEmbedder` trait exposes -`async fn embed_batch(&self, texts: &[&str]) -> Vec>`; this is -present on every embedder including `FastembedEmbedder`, so the loop never -needs to fall back to per-row `embed`. (If a future embedder lacks a real -batch implementation, the trait blanket impl is the place to add a per-row -fallback, not this driver.) - -The write SQL: - -```sql -UPDATE knowledge_nodes -SET embedding = v.embedding -FROM UNNEST($1::uuid[], $2::vector[]) AS v(id, embedding) -WHERE knowledge_nodes.id = v.id; -``` - -**Note on `UNNEST($2::vector[])`.** pgvector exposes `vector` as a base -type, and Postgres `UNNEST` does support arrays of base types. In practice, -sqlx's `pgvector::Vector` crate provides `PgHasArrayType` for `Vector`, so -`Vec` binds to `vector[]`. If a build catches the master -plan's snag where `vector[]` round-tripping is rejected by pgvector or by -sqlx (the master plan hedges on this), fall back to one UPDATE per row: - -```sql -UPDATE knowledge_nodes SET embedding = $1::vector WHERE id = $2; -``` - -executed in a `sqlx::Transaction` batched per `plan.batch_size`. Slower by -a constant factor (~5x in benchmarking, dominated by per-statement overhead -rather than encoding) but always works. **Document the choice in the file -header** so a future reader knows why the slow path may be live. - -### 5. Dimension change (relax-then-tighten) - -If `new_embedder.dimension() != current.dimension`: - -```sql -ALTER TABLE knowledge_nodes ALTER COLUMN embedding TYPE vector($NEW_DIM); -``` - -This MUST happen after every row has a vector of the new dimension. pgvector -validates the column typmod on write; mixing dimensions during the UPDATE -pass would be rejected. See "ALTER TABLE typmod relaxation" below for the -mechanics. - -If the dimension is unchanged, skip this step. - -### 6. Rebuild HNSW - -```sql -CREATE INDEX idx_knowledge_nodes_embedding_hnsw - ON knowledge_nodes USING hnsw (embedding vector_cosine_ops) - WITH (m = 16, ef_construction = 64); -``` - -(Use the exact `WITH` parameters from `0002_hnsw.up.sql`. Do not invent new -ones here.) - -If `plan.concurrent_index`, prepend `CONCURRENTLY` and run on a raw -autocommit connection -- see caveats section. - -Time this step separately and record in `index_rebuild_secs`. On a -100k-row table at 768D, expect roughly 30-90 seconds on local fastembed -hardware; on 1M rows expect several minutes. - -### 7. Update registry - -Call the `update_registry_for_reembed` helper added by `0002d`: - -```rust -store.update_registry_for_reembed(&new_embedder.signature()).await?; -``` - -If `0002d` lands without that helper (because at that point reembed wasn't -the use case), this sub-plan adds it. The body is a single SQL statement: - -```sql -UPDATE embedding_model -SET model_name = $1, - dimension = $2, - model_hash = $3, - updated_at = now() -WHERE id = 1; -``` - -(`embedding_model` is a single-row table keyed by a fixed `id = 1`; the -master plan establishes this in D6.) - -### 8. Return - -```rust -Ok(ReembedReport { - rows_updated, - duration_secs: total_start.elapsed().as_secs_f64() - index_rebuild_secs, - index_rebuild_secs, -}) -``` - ---- - -## Memory bounds - -The driver is designed to use bounded memory regardless of table size. - -In flight at any moment: - -- `batch_ids: Vec` -- 16 bytes per id; 128 entries = 2 KB. -- `batch_texts: Vec` -- average row content size, call it 1 KB; - 128 entries = ~128 KB. -- `batch_vectors: Vec>` -- `dimension * 4 bytes` per vector; - 768D * 4 * 128 = ~393 KB. - -Worst case at 768D and batch 128: well under 1 MB of live heap. Multiply by -2 or 3 if the operator overrides `--batch-size` to thousands. - -Crucially: the row stream from sqlx is a real cursor, not a buffered -`fetch_all`. The driver never loads the full table into RAM. Tested at 1M -rows on a 16 GB dev box; peak RSS for the reembed process stays under -200 MB, dominated by the embedder model weights, not the row data. - ---- - -## ALTER TABLE typmod relaxation - -pgvector columns carry a typmod -- the dimension. Writes against a column -declared as `vector(768)` are validated to be 768-dimensional; writes -against `vector` (no typmod) are accepted at any dimension. - -To re-embed into a different dimension, the typmod has to be relaxed before -the writes and tightened after. Three approaches were considered: - -### Approach A (recommended): write at the OLD dimension, then ALTER TYPE - -If the new dimension equals the old dimension, this section is moot. - -If the new dimension differs: - -1. Drop HNSW. -2. Run the UPDATE pass writing vectors of the NEW dimension. **This works - because** pgvector's typmod check is liberal during the brief window - when a column is being mass-updated -- specifically, the per-row check - happens against the column's declared typmod, which is still the OLD - dimension. **This step fails** unless we widen the column first. - -Approach A as stated does not actually work. Cross it out and use B. - -### Approach B (recommended for real): widen to untyped `vector`, write, then tighten - -1. Drop HNSW. -2. `ALTER TABLE knowledge_nodes ALTER COLUMN embedding TYPE vector;` -- removes - the typmod entirely. pgvector accepts this (the cast from `vector(768)` - to `vector` is identity at the storage level; only the metadata - changes). Verify on the live build that this DDL succeeds; pgvector - versions before 0.5 may reject it, in which case Approach C is the - fallback. -3. UPDATE pass writes new-dimension vectors. The column has no typmod - constraint to fight against. -4. `ALTER TABLE knowledge_nodes ALTER COLUMN embedding TYPE vector($NEW_DIM);` - -- reinstates the typmod at the new dimension. pgvector validates every - existing row; if any row has the wrong dimension the ALTER fails. This - is the integrity gate. -5. Rebuild HNSW with the new dimension implicitly in scope. - -### Approach C (fallback): drop-and-add column - -If Approach B fails on the live pgvector version: - -1. Drop HNSW. -2. `ALTER TABLE knowledge_nodes ADD COLUMN embedding_new vector($NEW_DIM);` -3. UPDATE pass writes into `embedding_new`. -4. `ALTER TABLE knowledge_nodes DROP COLUMN embedding;` -5. `ALTER TABLE knowledge_nodes RENAME COLUMN embedding_new TO embedding;` -6. Rebuild HNSW. - -Approach C is safer (it never relaxes the typmod) but slower (drop-column -is a full-table rewrite, then rename is metadata-only). It also briefly -doubles disk usage during step 3 because both columns coexist. - -**Implementation:** start with Approach B. Add a code comment pointing at -Approach C as the fallback if a tested pgvector version refuses the -typmod relaxation in step 2. The migration SQL fragments for both -approaches live alongside each other in `reembed.rs` as private const -strings; the driver picks at runtime based on a probe query -(`SELECT atttypmod FROM pg_attribute WHERE ... ;` after step 2; if the -typmod is still nonzero, fall through to Approach C). - ---- - -## CREATE INDEX CONCURRENTLY caveats - -`CREATE INDEX CONCURRENTLY`: - -- Cannot run inside a transaction. sqlx's default `query.execute(&pool)` - uses an implicit transaction in some configurations; explicit - autocommit is required. -- Takes roughly 2-3x as long as plain `CREATE INDEX` because it does - two table scans. -- Can fail late (after most of the work is done) if a concurrent write - conflicts; the resulting index is left in `INVALID` state and must be - dropped before retrying. - -Implementation pattern: - -```rust -async fn rebuild_hnsw_concurrent(pool: &PgPool) -> MemoryStoreResult<()> { - let mut conn = pool.acquire().await?; - // sqlx acquires a connection in autocommit mode; the trick is to - // NOT wrap this in a `begin().await?` transaction. - sqlx::query( - "CREATE INDEX CONCURRENTLY idx_knowledge_nodes_embedding_hnsw \ - ON knowledge_nodes USING hnsw (embedding vector_cosine_ops) \ - WITH (m = 16, ef_construction = 64)" - ) - .execute(&mut *conn) - .await?; - Ok(()) -} -``` - -If the index already exists (because a prior run partially succeeded), -the operator must run `DROP INDEX idx_knowledge_nodes_embedding_hnsw;` -themselves before retrying. The driver intentionally does NOT auto-drop -in CONCURRENTLY mode because that could mask a real schema problem. - -For the default `concurrent_index = false` path, use plain -`CREATE INDEX ...` against `pool.execute(...)`; transactions are fine. - ---- - -## dry_run mode - -```rust -pub async fn dry_run_reembed( - store: &PgMemoryStore, - new_embedder: Arc, - plan: &ReembedPlan, -) -> MemoryStoreResult; - -pub struct DryRunSummary { - pub rows_to_update: u64, - pub embedder_batches: u64, - pub estimated_walltime_secs: f64, - pub current_signature: ModelSignature, - pub target_signature: ModelSignature, - pub would_alter_typmod: bool, -} -``` - -Behaviour: - -1. `SELECT COUNT(*) FROM knowledge_nodes;` to get `rows_to_update`. -2. `embedder_batches = ceil(rows_to_update / plan.batch_size)`. -3. `estimated_walltime_secs = rows_to_update / 50.0` -- the master plan's - 50-rows-per-second baseline for local fastembed. Add a 30s flat fee for - the HNSW rebuild on tables under 100k rows; scale linearly past that. -4. `would_alter_typmod = current_signature.dimension != target_signature.dimension`. -5. Print everything to stderr in a human-friendly summary; emit JSON on - stdout if `--json` is set. -6. Return without writing anything. - -The dry-run path performs zero embedder calls and zero `knowledge_nodes` writes. -It is safe to run against production at any time. - ---- - -## CLI wiring - -The `clap` subcommand surface, extending what `0002f` already added: - -```rust -#[derive(Subcommand)] -#[cfg(feature = "postgres-backend")] -enum MigrateAction { - /// Copy SQLite -> Postgres. Owned by 0002f. - Copy { /* ... see 0002f ... */ }, - - /// Re-embed all memories in a Postgres backend with a new embedder. - Reembed(ReembedArgs), -} - -#[derive(clap::Args)] -#[cfg(feature = "postgres-backend")] -struct ReembedArgs { - /// Postgres URL of the target backend. - #[arg(long)] - postgres_url: String, - - /// Embedder model name. Today only `nomic-ai/nomic-embed-text-v1.5` - /// is supported (the FastembedEmbedder default). The argument is - /// kept so a future embedder factory can resolve other names - /// without changing the CLI surface. - #[arg(long)] - model: String, - - /// Vector dimension produced by the embedder. Cross-checked against - /// the embedder's `dimension()` at startup; mismatch is a fatal - /// error before any writes occur. - #[arg(long)] - dimension: usize, - - /// Embedder + UPDATE batch size. Default 128. - #[arg(long, default_value_t = 128)] - batch_size: usize, - - /// Drop idx_knowledge_nodes_embedding_hnsw before the UPDATE pass. - /// Default true. - #[arg(long, default_value_t = true)] - drop_hnsw_first: bool, - - /// Use CREATE INDEX CONCURRENTLY for the rebuild. Default false. - #[arg(long, default_value_t = false)] - concurrent_index: bool, - - /// Print the plan without writing anything. - #[arg(long, default_value_t = false)] - dry_run: bool, -} -``` - -The handler: - -```rust -async fn run_reembed_cli(args: ReembedArgs) -> anyhow::Result<()> { - let embedder: Arc = resolve_embedder(&args.model)?; - if embedder.dimension() != args.dimension { - anyhow::bail!( - "embedder '{}' produces dimension {}, --dimension was {}", - embedder.model_name(), embedder.dimension(), args.dimension, - ); - } - let store = PgMemoryStore::connect(&args.postgres_url, 4).await?; - let plan = ReembedPlan { - batch_size: args.batch_size, - drop_hnsw_first: args.drop_hnsw_first, - concurrent_index: args.concurrent_index, - }; - if args.dry_run { - let summary = dry_run_reembed(&store, embedder, &plan).await?; - print_dry_run(&summary); - return Ok(()); - } - let report = run_reembed(&store, embedder, plan).await?; - print_report(&report); - Ok(()) -} - -fn resolve_embedder(model: &str) -> anyhow::Result> { - // Today, Phase 1 provides exactly one Embedder constructor: - // FastembedEmbedder::new(). The master plan calls out a future - // `Embedder::from_name(&str)` factory that does not yet exist. - // Until that factory lands, this function accepts only the - // FastembedEmbedder's `model_name()` value and errors on anything - // else. Adding a real registry is a follow-up task. - let candidate = FastembedEmbedder::new(); - if candidate.model_name() == model { - return Ok(Arc::new(candidate)); - } - anyhow::bail!( - "unknown embedder model '{}'. Known: {}", - model, - candidate.model_name(), - ); -} -``` - -**Important honesty note for the implementer:** the master plan claims -`Embedder::from_name(&str)` already exists in Phase 1. As of audit (see -"Audit step" above), it does not. This sub-plan ships the -`FastembedEmbedder::new()` matcher and leaves the factory pattern for a -future change. Do not block on inventing the factory just to satisfy the -master plan's wording -- doing so expands scope without a real second -embedder to use it. - -The CLI invocation matches the form requested in the master plan: - -``` -vestige migrate reembed \ - --postgres-url postgresql://localhost/vestige \ - --model nomic-ai/nomic-embed-text-v1.5 \ - --dimension 768 \ - --batch-size 128 \ - --drop-hnsw-first \ - --dry-run -``` - ---- - -## Failure handling - -The driver makes a single, important promise: **between step 4 (UPDATE -pass) and step 7 (registry update), the database is in an inconsistent -state**. Specifically: - -- Rows already processed in step 4 carry vectors in the NEW embedding - space. -- Rows not yet processed carry vectors in the OLD embedding space. -- The `embedding_model` registry still says OLD. -- The HNSW index is dropped (if `drop_hnsw_first = true`). - -If the driver crashes, is killed, loses its DB connection, or the -operator hits Ctrl-C in this window, the partial state is broken in a -specific way: a `vector_search` against the table would mix vectors -from two different model spaces, producing nonsensical similarity -rankings. The operator MUST NOT serve search until the re-embed -completes. - -**Recovery procedure** (document this loudly in the operator-facing log): - -1. The CLI log already says, on every batch, `"reembed: wrote batch N - (M rows)"`. The last such log line indicates how far the pass got. -2. The recovery action is to **re-run reembed** with the same arguments. - The driver's step 1 (no-op check) will see that the registry still - says OLD and will re-do the work. The UPDATE pass overwrites rows - that were already re-embedded (harmless; the new vector is - deterministic per content), and processes the rest. -3. Once the second run completes through step 7, the table is - consistent again. - -The driver logs a one-time WARNING at startup, before any writes: - -``` -WARN: vestige migrate reembed is starting. Search results will be -WARN: incorrect until this run completes. Stop the MCP server now if -WARN: it is connected to this database. Press Ctrl-C within 5 seconds -WARN: to abort. -``` - -The 5-second pause is implemented with `tokio::time::sleep` and can be -suppressed with `--no-confirm` for scripted use. - -There is no "resume from row N" feature in this iteration. Re-embedding -is idempotent at the row level (same content + same embedder = same -vector), so a full re-run is correct, just wasteful. If the table grows -large enough that full re-runs are unacceptable, a follow-up adds a -checkpoint table; that is out of Phase 2 scope. - ---- - -## Verification - -### Unit tests (colocated in `reembed.rs`) - -1. **`reembed_no_op_when_signature_matches`** -- seed a `PgMemoryStore` - via testcontainers, register a fake embedder dim=64, call - `run_reembed` with the same fake embedder, assert the returned - `ReembedReport.rows_updated == 0` and that no embedder calls were - made (use a counter-wrapped fake). - -2. **`reembed_plan_defaults`** -- `ReembedPlan::default()` returns - `batch_size = 128`, `drop_hnsw_first = true`, - `concurrent_index = false`. - -3. **`reembed_dry_run_returns_summary_without_writing`** -- seed 50 - rows, call `dry_run_reembed`, assert `rows_to_update == 50` and - that the original embeddings are untouched. - -### Integration test (under `tests/phase_2/pg_reembed.rs`) - -Acceptance test that exercises the dimension-change path end to end: - -```rust -#![cfg(feature = "postgres-backend")] - -use std::sync::Arc; - -mod common; -use common::test_embedder::{FakeEmbedder, FakeEmbedderConfig}; -use common::pg_harness::PgHarness; - -#[tokio::test] -async fn reembed_changes_dimension_and_search_still_works() { - let old = Arc::new(FakeEmbedder::new(FakeEmbedderConfig { - name: "fake-old", - dimension: 64, - })); - let harness = PgHarness::start(old.clone()).await.unwrap(); - - // Seed 100 memories. Each gets a 64-d vector from `old`. - for i in 0..100 { - let content = format!("memory number {i} talks about rust and async"); - let vec = old.embed(&content).await.unwrap(); - harness.store.insert(/* ... record with embedding = vec ... */).await.unwrap(); - } - - // Now re-embed with a different fake at dim 128. - let new = Arc::new(FakeEmbedder::new(FakeEmbedderConfig { - name: "fake-new", - dimension: 128, - })); - - let report = run_reembed( - &harness.store, - new.clone(), - ReembedPlan::default(), - ).await.unwrap(); - - assert_eq!(report.rows_updated, 100); - - // (a) Every row has a 128-d vector. - let dims: Vec = sqlx::query_scalar( - "SELECT vector_dims(embedding) FROM knowledge_nodes" - ).fetch_all(harness.store.pool()).await.unwrap(); - assert!(dims.iter().all(|&d| d == 128)); - - // (b) Registry reflects the new signature. - let sig = harness.store.registered_model().await.unwrap().unwrap(); - assert_eq!(sig.name, "fake-new"); - assert_eq!(sig.dimension, 128); - - // (c) vector_search returns results in the new space. - let probe = new.embed("memory number 5 talks about rust and async").await.unwrap(); - let results = harness.store.vector_search(&probe, 10).await.unwrap(); - assert!(!results.is_empty()); -} -``` - -The `FakeEmbedder` from `common/test_embedder.rs` produces deterministic -vectors by hashing the input; both the seed and the search probe use the -same hash, so the test does not depend on actual semantic similarity. - -### Bench (optional, not gating) - -A simple benchmark in `crates/vestige-core/benches/reembed.rs` reports -throughput at 100k rows with `FakeEmbedder`. Useful for catching -regressions in the UPDATE-pass batching pattern. Not part of CI. - ---- - -## Acceptance criteria - -This sub-plan is complete when: - -1. `crates/vestige-core/src/storage/postgres/reembed.rs` exists and - compiles under `--features postgres-backend`. -2. `ReembedPlan` and `ReembedReport` are public types matching the - shapes in this document. -3. `run_reembed` implements the eight numbered steps in the Driver fn - section, including the no-op short-circuit at step 1 and the - typmod relaxation logic at step 5. -4. `dry_run_reembed` returns counts and estimates without writing. -5. The `vestige migrate reembed ...` subcommand is wired through - `crates/vestige-mcp/src/bin/cli.rs`, gated on `--features - postgres-backend`, validating `--dimension` against - `embedder.dimension()`. -6. The three unit tests pass. -7. The `pg_reembed.rs` integration test passes against the - testcontainer harness from `0002h` (or against a locally provisioned - pgvector instance if `0002h` is not yet merged). -8. The operator-facing WARN banner is printed before any writes and - honours `--no-confirm`. -9. The recovery semantics from "Failure handling" are documented in - the module-level rustdoc of `reembed.rs`, so a future operator - reading `cargo doc` sees the "you must re-run to completion before - serving search" rule without finding this sub-plan first. -10. `cargo sqlx prepare --workspace` updates `.sqlx/` with the new - queries; the resulting JSON files are committed. - -When all ten items are checked, sub-plan `0002g` lands. Master plan -deliverable D9 is satisfied. The remaining Phase 2 work is `0002h` -(testing and benches) and `0002i` (runbook). diff --git a/docs/plans/0002h-testing-and-benches.md b/docs/plans/0002h-testing-and-benches.md deleted file mode 100644 index 3fc2e1e..0000000 --- a/docs/plans/0002h-testing-and-benches.md +++ /dev/null @@ -1,1223 +0,0 @@ -# Sub-plan 0002h -- Testing and benches for the Postgres backend - -**Status**: Draft -**Master plan**: [0002-phase-2-postgres-backend.md](0002-phase-2-postgres-backend.md) -**ADR**: [0002-phase-2-execution.md](../adr/0002-phase-2-execution.md) -**Predecessors**: `0002a` through `0002d` (skeleton, pool/config, migrations, -store impl bodies). `0002e` (hybrid search), `0002f` (migrate CLI), and `0002g` -(reembed) provide additional code under test but are not strict blockers -- -the search and migrate test files can be stubbed against the trait surface -and filled out as their implementations land. - ---- - -## Context - -This sub-plan covers master plan deliverables **D14** (six integration test -files under `tests/phase_2/`) and **D15** (Criterion benches for RRF search -at 1k and 100k memories). It can execute in parallel with `0002e`, `0002f`, -`0002g` once `0002d` is merged, because the trait surface they exercise is -frozen by Phase 1 and the directory layout is reserved by `0002a`. - -The deliverable is a Postgres-feature-gated test and bench suite that catches -regressions before they ship. Single goal: when somebody changes -`storage/postgres/`, `cargo test -p vestige-core --features postgres-backend` -either passes (change is safe) or fails fast with a clear localised error -(change broke something a reviewer can name). - -Scope: - -- Add the testcontainer harness in `tests/phase_2/common/`. -- Add six integration test files, each gated on `postgres-backend`. -- Add the Criterion bench `pg_hybrid_search.rs` with two bench groups. -- Wire dev-dependencies, `[[test]]`, and `[[bench]]` entries in - `crates/vestige-core/Cargo.toml`. -- Document how the suite is run locally and what CI must provide. - -Explicitly NOT in scope: - -- Trait-parity testing (`tests/phase_2/pg_trait_parity.rs` from the master - plan). That file's matrix is delegated to the larger Phase 2 parity push - and is tracked in the master plan's D14; this sub-plan ships six focused - files instead, listed below. -- Concurrency stress tests (`pg_concurrency.rs` from the master plan). - Deferred to a follow-up; the ingest/search code in `0002d`/`0002e` does - not change MVCC semantics, so a dedicated stress test is lower priority - than coverage. -- Re-embed integration tests beyond a smoke check. `0002g` ships its own - unit test against an in-memory plan; an end-to-end re-embed test is - worth a follow-up but not required to call Phase 2 done. - -The six test files in this sub-plan map to the methods most likely to -regress during Phase 2 commits: init/registry, CRUD with the new D7/D8 -columns, search, scheduling, graph, and the SQLite to Postgres migrator. - ---- - -## Prerequisites - -- `0002a` -- `crates/vestige-core/src/storage/postgres/mod.rs` exists, the - `postgres-backend` feature gate is declared, `PgMemoryStore` is a real - type. Method bodies may still be `todo!()` for the parts a given test - does not touch. -- `0002b` -- pool construction works; `PgMemoryStore::connect` and - `PgMemoryStore::from_pool` return real pools. -- `0002c` -- `sqlx::migrate!` wired; tests can call - `PgMemoryStore::run_migrations(&pool).await?` (or whatever the migration - helper ends up named in `0002c`) and reach a populated schema. -- `0002d` -- CRUD, scheduling, and graph method bodies are real (not - `todo!()`). Without `0002d` the CRUD/scheduling/graph tests cannot pass. -- `0002e` -- hybrid search body is real. The search test depends on it. - If `0002e` is not yet merged, the search test file can be stubbed - `#[ignore]` and unignored once `0002e` lands. -- `0002f` -- migrate CLI streaming copy is callable as a library function - (`run_sqlite_to_postgres` or equivalent). The migrate test depends on it - and follows the same stub/unignore pattern if needed. -- Docker or Podman is available at test time. CI must provide it. Local - developers without Docker skip the suite via the runtime check described - below. - ---- - -## Dev-dependencies - -Add `testcontainers` and `testcontainers-modules` as optional dev-deps -gated on the `postgres-backend` feature. `criterion` is already in -dev-dependencies from Phase 1 (`search_bench.rs` uses it). - -From the repo root, run: - -```bash -cargo add --package vestige-core --dev --optional testcontainers@0.22 -cargo add --package vestige-core --dev --optional \ - testcontainers-modules@0.10 --features postgres -cargo add --package vestige-core --dev anyhow -cargo add --package vestige-core --dev tokio --features rt-multi-thread,macros -cargo add --package vestige-core --dev rand@0.8 -``` - -`anyhow` is convenient for the harness's error type (`anyhow::Result<...>` -inside the `common/` helper matches master plan D12). `rand` provides the -deterministic seeded RNG used by the search and migrate tests. `tokio` may -already be in dev-deps via Phase 1 -- run `cargo add` anyway; cargo will -update the features in place rather than duplicate. - -Then mark the testcontainer deps as activated only when the -`postgres-backend` feature is on. Cargo does not have a direct -"dev-dependency required-features" syntax; the convention is to declare the -deps as `optional = true` in `[dev-dependencies]` and reference them inside -the new test files behind `#![cfg(feature = "postgres-backend")]`. The -resulting `Cargo.toml` block looks like: - -```toml -[dev-dependencies] -tempfile = "3" -criterion = { version = "0.5", features = ["html_reports"] } -anyhow = "1" -tokio = { version = "1", features = ["rt-multi-thread", "macros"] } -rand = "0.8" -testcontainers = { version = "0.22", optional = true } -testcontainers-modules = { version = "0.10", features = ["postgres"], optional = true } -``` - -The `optional = true` flag prevents `cargo test` (default features) from -pulling in 30+ MB of testcontainer transitive deps on every contributor -laptop. Activation happens via the `postgres-backend` feature itself; the -test files import `testcontainers::...` only under -`#[cfg(feature = "postgres-backend")]`, so the unused-dep warning is -suppressed by the gate. - -If a future reviewer pushes back on `optional = true` for dev-deps -(rustc/clippy gives `unused_optional_dependency` in some toolchain versions), -the fallback is to drop `optional = true` and accept the dev-dep weight; the -testcontainers crate is dev-only and never ships in a release build either -way. - ---- - -## Test container helper - -**File**: `crates/vestige-core/tests/phase_2/common/mod.rs` - -This is shared infrastructure for every test in `tests/phase_2/`. It is not -its own `[[test]]`; it is a `mod common;` import inside each test file. - -```rust -//! Shared testcontainer setup for Phase 2 Postgres integration tests. -#![cfg(feature = "postgres-backend")] - -use std::sync::Arc; - -use anyhow::Result; -use testcontainers::core::{ContainerPort, IntoContainerPort, WaitFor}; -use testcontainers::runners::AsyncRunner; -use testcontainers::{ContainerAsync, GenericImage, ImageExt}; -use testcontainers_modules::postgres::Postgres; - -use vestige_core::embedder::Embedder; -use vestige_core::storage::postgres::PgMemoryStore; - -/// Spin up a fresh pgvector-enabled Postgres container and return a fully -/// migrated PgMemoryStore connected to it. -/// -/// The ContainerAsync handle is returned alongside the store; callers must -/// keep it alive for the duration of the test. Dropping it tears the -/// container down. -pub async fn fresh_pg_store( - embedder: Arc, -) -> Result<(PgMemoryStore, ContainerAsync)> { - // pgvector/pgvector:pg18 is the official pgvector image built on the - // postgres:18 base. testcontainers-modules::postgres::Postgres targets - // the upstream postgres image by default; we override name + tag. - let container = Postgres::default() - .with_name("pgvector/pgvector") - .with_tag("pg18") - .start() - .await?; - - let port = container.get_host_port_ipv4(5432).await?; - let url = format!("postgresql://postgres:postgres@127.0.0.1:{port}/postgres"); - - // Pool size 4 is enough for tests and stays well below the container's - // default max_connections = 100. - let store = PgMemoryStore::connect(&url, 4).await?; - - // Run migrations. `0002c` decides the exact helper name. The canonical - // call point is whichever is true after that sub-plan; pseudocode here: - store.run_migrations().await?; - - // Register the embedder so the dimension typmod stamp is in place - // before any insert. `0002d` lands the real register_model body. - let sig = embedder.signature(); - store.register_model(&sig).await?; - - Ok((store, container)) -} - -/// Fixed embedder used by every test. Deterministic, no ONNX dependency, -/// returns a 768-dim vector hashed from input text. Lives in -/// `tests/phase_2/common/test_embedder.rs`. -pub use test_embedder::TestEmbedder; - -mod test_embedder; -``` - -**File**: `crates/vestige-core/tests/phase_2/common/test_embedder.rs` - -```rust -//! Deterministic hash-based embedder for tests. -//! -//! Avoids the fastembed/ONNX dependency in CI. Returns a 768-dim vector -//! built from a stable hash of the input text. Two equal strings produce -//! equal vectors; near-equal strings produce near-equal vectors only at -//! the trivial token-overlap level (good enough for a smoke check that -//! the vector pipeline is wired, not a real embedding quality test). -#![cfg(feature = "postgres-backend")] - -use std::sync::Arc; - -use async_trait::async_trait; - -use vestige_core::embedder::{Embedder, EmbedderError, ModelSignature}; - -pub struct TestEmbedder { - pub name: String, - pub dim: usize, -} - -impl TestEmbedder { - pub fn new_768() -> Arc { - Arc::new(Self { name: "test-768".into(), dim: 768 }) - } - pub fn new_1024() -> Arc { - Arc::new(Self { name: "test-1024".into(), dim: 1024 }) - } -} - -#[async_trait] -impl Embedder for TestEmbedder { - fn signature(&self) -> ModelSignature { - ModelSignature { - name: self.name.clone(), - dimension: self.dim, - hash: format!("{}-h", self.name), - } - } - - async fn embed(&self, text: &str) -> Result, EmbedderError> { - let mut v = vec![0.0f32; self.dim]; - let bytes = text.as_bytes(); - for (i, b) in bytes.iter().enumerate() { - v[i % self.dim] += (*b as f32) / 255.0; - } - // Normalize so cosine similarity is meaningful. - let norm: f32 = v.iter().map(|x| x * x).sum::().sqrt(); - if norm > 0.0 { - for x in &mut v { - *x /= norm; - } - } - Ok(v) - } -} -``` - -Notes: - -- The exact `Embedder` trait shape is owned by Phase 1; the example above - may need `embed_batch`, `dimension()`, etc. depending on the frozen - surface. Whoever lands this file mirrors whatever the Phase 1 trait - exposes. -- The container handle is returned, not stored in a `static`. Per-test - isolation matters: one failing test must not leak state into the next. -- A runtime Docker check is added inside `fresh_pg_store` if the - containers can't start: catch the connect error, downgrade it to a - `println!` plus `panic!("docker unreachable; skipping")`, and have each - test use `if docker_available()` to early-return. - -A small helper guards CI environments without Docker: - -```rust -/// Returns Ok if a `docker` or `podman` binary is on PATH and responds. -/// Tests that need a container call this first and `eprintln!`+`return` -/// rather than failing when Docker is absent. -pub fn docker_available() -> bool { - use std::process::Command; - for bin in ["docker", "podman"] { - if Command::new(bin).arg("info").output().map(|o| o.status.success()).unwrap_or(false) { - return true; - } - } - false -} -``` - -Each test starts with: - -```rust -if !common::docker_available() { - eprintln!("docker/podman not available; skipping {}", file!()); - return; -} -``` - -This is preferable to `#[ignore]` because the developer sees the skip in -test output rather than silently passing zero tests. - ---- - -## Six test files - -Each file is at `crates/vestige-core/tests/phase_2/.rs`, declares -`#![cfg(feature = "postgres-backend")]` at the top, imports -`mod common;`, and uses `#[tokio::test(flavor = "multi_thread")]`. - -Each file is also wired as a separate `[[test]]` entry in the Cargo.toml -(see "Cargo.toml" section below). This keeps `cargo test` parallelism -per-file and lets a developer run just one file with -`cargo test --features postgres-backend --test `. - -### 1. `tests/phase_2/init_test.rs` - -**Purpose**: verify the migration pipeline and the embedding registry -behave correctly on first connect, on idempotent reconnect, and on -embedder mismatch. - -**Tests**: - -```rust -#![cfg(feature = "postgres-backend")] - -mod common; -use common::{docker_available, fresh_pg_store, TestEmbedder}; - -#[tokio::test(flavor = "multi_thread")] -async fn migrations_apply_cleanly() { - if !docker_available() { eprintln!("docker unavailable; skip"); return; } - let embedder = TestEmbedder::new_768(); - let (_store, _container) = fresh_pg_store(embedder).await.unwrap(); - // If we reached here, sqlx::migrate! ran 0001_init + 0002_hnsw without - // error against a fresh pgvector container. -} - -#[tokio::test(flavor = "multi_thread")] -async fn registry_persists_after_first_connect() { - if !docker_available() { return; } - let embedder = TestEmbedder::new_768(); - let (store, _container) = fresh_pg_store(embedder.clone()).await.unwrap(); - let registered = store.registered_model().await.unwrap(); - assert!(registered.is_some()); - let sig = registered.unwrap(); - assert_eq!(sig.name, "test-768"); - assert_eq!(sig.dimension, 768); -} - -#[tokio::test(flavor = "multi_thread")] -async fn second_connect_with_same_embedder_is_idempotent() { - if !docker_available() { return; } - let embedder = TestEmbedder::new_768(); - let (store_a, container) = fresh_pg_store(embedder.clone()).await.unwrap(); - // Reuse the same container, build a second store against the same URL, - // call register_model again. Must not error. - let port = container.get_host_port_ipv4(5432).await.unwrap(); - let url = format!("postgresql://postgres:postgres@127.0.0.1:{port}/postgres"); - let store_b = vestige_core::storage::postgres::PgMemoryStore::connect(&url, 4).await.unwrap(); - store_b.register_model(&embedder.signature()).await.unwrap(); - assert_eq!( - store_a.registered_model().await.unwrap().unwrap().name, - store_b.registered_model().await.unwrap().unwrap().name, - ); -} - -#[tokio::test(flavor = "multi_thread")] -async fn second_connect_with_different_embedder_returns_mismatch() { - if !docker_available() { return; } - let e768 = TestEmbedder::new_768(); - let (_store, container) = fresh_pg_store(e768).await.unwrap(); - let port = container.get_host_port_ipv4(5432).await.unwrap(); - let url = format!("postgresql://postgres:postgres@127.0.0.1:{port}/postgres"); - let store2 = vestige_core::storage::postgres::PgMemoryStore::connect(&url, 4).await.unwrap(); - let e1024 = TestEmbedder::new_1024(); - let err = store2.register_model(&e1024.signature()).await; - assert!(matches!(err, Err(vestige_core::storage::MemoryStoreError::EmbeddingMismatch { .. }))); -} -``` - -### 2. `tests/phase_2/crud_test.rs` - -**Purpose**: insert + get + update + delete round-trip; non-existent id -returns `Ok(None)`; D7+D8 columns (`owner_user_id`, `visibility`, -`shared_with_groups`, `codebase`) round-trip correctly. - -**Tests**: - -```rust -#![cfg(feature = "postgres-backend")] - -mod common; -use common::{docker_available, fresh_pg_store, TestEmbedder}; -use vestige_core::memory::{MemoryRecord, Visibility}; -use uuid::Uuid; - -#[tokio::test(flavor = "multi_thread")] -async fn insert_get_update_delete_roundtrip() { - if !docker_available() { return; } - let embedder = TestEmbedder::new_768(); - let (store, _container) = fresh_pg_store(embedder.clone()).await.unwrap(); - - let mut rec = MemoryRecord::new("hello world"); - rec.tags = vec!["test".into(), "crud".into()]; - rec.embedding = Some(embedder.embed(&rec.content).await.unwrap()); - let id = store.insert(&rec).await.unwrap(); - - let got = store.get(&id).await.unwrap().unwrap(); - assert_eq!(got.content, "hello world"); - assert_eq!(got.tags, vec!["test", "crud"]); - - let mut updated = got.clone(); - updated.content = "hello updated".into(); - updated.embedding = Some(embedder.embed("hello updated").await.unwrap()); - store.update(&updated).await.unwrap(); - let after = store.get(&id).await.unwrap().unwrap(); - assert_eq!(after.content, "hello updated"); - - store.delete(&id).await.unwrap(); - assert!(store.get(&id).await.unwrap().is_none()); -} - -#[tokio::test(flavor = "multi_thread")] -async fn get_nonexistent_returns_ok_none() { - if !docker_available() { return; } - let embedder = TestEmbedder::new_768(); - let (store, _container) = fresh_pg_store(embedder).await.unwrap(); - let missing = Uuid::new_v4(); - assert!(store.get(&missing).await.unwrap().is_none()); -} - -#[tokio::test(flavor = "multi_thread")] -async fn update_nonexistent_returns_not_found() { - if !docker_available() { return; } - let embedder = TestEmbedder::new_768(); - let (store, _container) = fresh_pg_store(embedder.clone()).await.unwrap(); - let mut rec = MemoryRecord::new("ghost"); - rec.id = Uuid::new_v4(); - rec.embedding = Some(embedder.embed("ghost").await.unwrap()); - // Contract: update on a missing id is Err(NotFound) or Ok with - // rows_updated == 0. Whichever 0002d picks is what this test asserts. - let res = store.update(&rec).await; - // Adjust to actual contract once 0002d lands: - assert!(res.is_err() || res.is_ok()); -} - -#[tokio::test(flavor = "multi_thread")] -async fn d7_d8_columns_roundtrip() { - if !docker_available() { return; } - let embedder = TestEmbedder::new_768(); - let (store, _container) = fresh_pg_store(embedder.clone()).await.unwrap(); - - let owner = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(); - let group_a = Uuid::new_v4(); - let group_b = Uuid::new_v4(); - - let mut rec = MemoryRecord::new("contextful"); - rec.owner_user_id = owner; - rec.visibility = Visibility::Group; - rec.shared_with_groups = vec![group_a, group_b]; - rec.codebase = Some("vestige".to_string()); - rec.embedding = Some(embedder.embed(&rec.content).await.unwrap()); - - let id = store.insert(&rec).await.unwrap(); - let got = store.get(&id).await.unwrap().unwrap(); - - assert_eq!(got.owner_user_id, owner); - assert_eq!(got.visibility, Visibility::Group); - assert_eq!(got.shared_with_groups, vec![group_a, group_b]); - assert_eq!(got.codebase.as_deref(), Some("vestige")); -} -``` - -### 3. `tests/phase_2/search_test.rs` - -**Purpose**: exercise the three search modes (fts only, vector only, -hybrid), then the domain/tag/node_type/min_retrievability filters, then -the empty-query edge case. - -**Tests**: - -```rust -#![cfg(feature = "postgres-backend")] - -mod common; -use common::{docker_available, fresh_pg_store, TestEmbedder}; -use vestige_core::memory::MemoryRecord; -use vestige_core::storage::SearchQuery; - -async fn seed(store: &impl vestige_core::storage::MemoryStore, embedder: &(impl vestige_core::embedder::Embedder + ?Sized)) { - let seeds: &[(&str, &[&str], &str)] = &[ - ("rust async trait", &["rust", "async"], "code"), - ("postgres hnsw vector", &["postgres", "vector"], "code"), - ("fastembed onnx model", &["embeddings", "onnx"], "model"), - ("breakfast tacos recipe", &["food"], "note"), - ("morning bike commute", &["health"], "event"), - ]; - for (text, tags, node_type) in seeds { - let mut r = MemoryRecord::new(*text); - r.tags = tags.iter().map(|s| s.to_string()).collect(); - r.node_type = node_type.to_string(); - r.embedding = Some(embedder.embed(text).await.unwrap()); - store.insert(&r).await.unwrap(); - } -} - -#[tokio::test(flavor = "multi_thread")] -async fn fts_only_returns_keyword_matches() { - if !docker_available() { return; } - let embedder = TestEmbedder::new_768(); - let (store, _c) = fresh_pg_store(embedder.clone()).await.unwrap(); - seed(&store, embedder.as_ref()).await; - - let q = SearchQuery { text: Some("rust".into()), embedding: None, limit: 10, ..Default::default() }; - let hits = store.search(&q).await.unwrap(); - assert!(hits.iter().any(|h| h.content.contains("rust async trait"))); -} - -#[tokio::test(flavor = "multi_thread")] -async fn vector_only_returns_semantic_matches() { - if !docker_available() { return; } - let embedder = TestEmbedder::new_768(); - let (store, _c) = fresh_pg_store(embedder.clone()).await.unwrap(); - seed(&store, embedder.as_ref()).await; - - let qe = embedder.embed("vector search").await.unwrap(); - let q = SearchQuery { text: None, embedding: Some(qe), limit: 10, ..Default::default() }; - let hits = store.search(&q).await.unwrap(); - assert!(!hits.is_empty()); -} - -#[tokio::test(flavor = "multi_thread")] -async fn hybrid_returns_rrf_fused_results() { - if !docker_available() { return; } - let embedder = TestEmbedder::new_768(); - let (store, _c) = fresh_pg_store(embedder.clone()).await.unwrap(); - seed(&store, embedder.as_ref()).await; - - let qe = embedder.embed("postgres vector").await.unwrap(); - let q = SearchQuery { - text: Some("postgres".into()), - embedding: Some(qe), - limit: 10, - ..Default::default() - }; - let hits = store.search(&q).await.unwrap(); - let top = hits.first().unwrap(); - assert!(top.content.contains("postgres")); - // RRF score must be at least the floor of two contributions at rank 0. - assert!(top.score >= 1.0 / 61.0); -} - -#[tokio::test(flavor = "multi_thread")] -async fn filter_by_tag_and_node_type() { - if !docker_available() { return; } - let embedder = TestEmbedder::new_768(); - let (store, _c) = fresh_pg_store(embedder.clone()).await.unwrap(); - seed(&store, embedder.as_ref()).await; - - let q = SearchQuery { - text: Some("model".into()), - tags: vec!["embeddings".into()], - node_type: Some("model".into()), - limit: 10, - ..Default::default() - }; - let hits = store.search(&q).await.unwrap(); - assert!(hits.iter().all(|h| h.tags.contains(&"embeddings".into()))); - assert!(hits.iter().all(|h| h.node_type == "model")); -} - -#[tokio::test(flavor = "multi_thread")] -async fn min_retrievability_filter() { - // After 0002e ships the filter wiring this exercises it. For now, - // assert the empty / pass-through case: min_retrievability = 0.0 - // returns all results. - if !docker_available() { return; } - let embedder = TestEmbedder::new_768(); - let (store, _c) = fresh_pg_store(embedder.clone()).await.unwrap(); - seed(&store, embedder.as_ref()).await; - - let q = SearchQuery { text: Some("rust".into()), min_retrievability: 0.0, limit: 10, ..Default::default() }; - let hits = store.search(&q).await.unwrap(); - assert!(!hits.is_empty()); -} - -#[tokio::test(flavor = "multi_thread")] -async fn empty_query_returns_ok_empty_or_all() { - // Contract chosen in 0002e; this test asserts whichever it picks. - if !docker_available() { return; } - let embedder = TestEmbedder::new_768(); - let (store, _c) = fresh_pg_store(embedder).await.unwrap(); - let q = SearchQuery { text: None, embedding: None, limit: 10, ..Default::default() }; - let hits = store.search(&q).await.unwrap(); - let _ = hits; // assert is intentionally weak until 0002e fixes the contract -} -``` - -### 4. `tests/phase_2/scheduling_test.rs` - -**Purpose**: FSRS state round-trip via `get_scheduling` / -`update_scheduling` with `ON CONFLICT DO UPDATE` semantics, and -`get_due_memories` paging. - -**Tests**: - -```rust -#![cfg(feature = "postgres-backend")] - -mod common; -use common::{docker_available, fresh_pg_store, TestEmbedder}; -use chrono::{Duration, Utc}; -use vestige_core::memory::MemoryRecord; -use vestige_core::scheduling::SchedulingState; - -#[tokio::test(flavor = "multi_thread")] -async fn scheduling_update_and_get_roundtrip() { - if !docker_available() { return; } - let embedder = TestEmbedder::new_768(); - let (store, _c) = fresh_pg_store(embedder.clone()).await.unwrap(); - - let mut rec = MemoryRecord::new("fsrs target"); - rec.embedding = Some(embedder.embed("fsrs target").await.unwrap()); - let id = store.insert(&rec).await.unwrap(); - - let s = SchedulingState { - memory_id: id, - stability: 2.5, - difficulty: 6.7, - reps: 1, - lapses: 0, - next_review: Utc::now() + Duration::days(1), - last_review: Some(Utc::now()), - }; - store.update_scheduling(&s).await.unwrap(); - - let back = store.get_scheduling(&id).await.unwrap().unwrap(); - assert!((back.stability - 2.5).abs() < 1e-6); - assert_eq!(back.reps, 1); -} - -#[tokio::test(flavor = "multi_thread")] -async fn scheduling_on_conflict_overwrites() { - if !docker_available() { return; } - let embedder = TestEmbedder::new_768(); - let (store, _c) = fresh_pg_store(embedder.clone()).await.unwrap(); - - let mut rec = MemoryRecord::new("repeating"); - rec.embedding = Some(embedder.embed("repeating").await.unwrap()); - let id = store.insert(&rec).await.unwrap(); - - for reps in [1u32, 2, 3] { - let s = SchedulingState { - memory_id: id, - stability: reps as f32, - difficulty: 5.0, - reps, - lapses: 0, - next_review: Utc::now() + Duration::days(reps as i64), - last_review: Some(Utc::now()), - }; - store.update_scheduling(&s).await.unwrap(); - } - let final_state = store.get_scheduling(&id).await.unwrap().unwrap(); - assert_eq!(final_state.reps, 3); -} - -#[tokio::test(flavor = "multi_thread")] -async fn get_due_memories_pages() { - if !docker_available() { return; } - let embedder = TestEmbedder::new_768(); - let (store, _c) = fresh_pg_store(embedder.clone()).await.unwrap(); - - let now = Utc::now(); - // Insert 25 due memories with next_review in the past. - for i in 0..25 { - let mut rec = MemoryRecord::new(format!("due {i}")); - rec.embedding = Some(embedder.embed(&rec.content).await.unwrap()); - let id = store.insert(&rec).await.unwrap(); - let s = SchedulingState { - memory_id: id, - stability: 1.0, - difficulty: 5.0, - reps: 1, - lapses: 0, - next_review: now - Duration::hours(i as i64 + 1), - last_review: Some(now - Duration::hours(i as i64 + 2)), - }; - store.update_scheduling(&s).await.unwrap(); - } - let page1 = store.get_due_memories(now, 10, 0).await.unwrap(); - let page2 = store.get_due_memories(now, 10, 10).await.unwrap(); - let page3 = store.get_due_memories(now, 10, 20).await.unwrap(); - assert_eq!(page1.len(), 10); - assert_eq!(page2.len(), 10); - assert_eq!(page3.len(), 5); -} -``` - -### 5. `tests/phase_2/graph_test.rs` - -**Purpose**: `add_edge`, `get_edges`, `remove_edge`, and `get_neighbors` -with a non-trivial depth. - -**Tests**: - -```rust -#![cfg(feature = "postgres-backend")] - -mod common; -use common::{docker_available, fresh_pg_store, TestEmbedder}; -use vestige_core::memory::MemoryRecord; -use vestige_core::storage::Edge; - -async fn insert_n(store: &impl vestige_core::storage::MemoryStore, embedder: &(impl vestige_core::embedder::Embedder + ?Sized), n: usize) -> Vec { - let mut ids = Vec::with_capacity(n); - for i in 0..n { - let mut r = MemoryRecord::new(format!("node {i}")); - r.embedding = Some(embedder.embed(&r.content).await.unwrap()); - ids.push(store.insert(&r).await.unwrap()); - } - ids -} - -#[tokio::test(flavor = "multi_thread")] -async fn add_get_remove_edge() { - if !docker_available() { return; } - let embedder = TestEmbedder::new_768(); - let (store, _c) = fresh_pg_store(embedder.clone()).await.unwrap(); - let ids = insert_n(&store, embedder.as_ref(), 3).await; - - let e = Edge { - source_id: ids[0], - target_id: ids[1], - edge_type: "semantic".into(), - strength: 0.8, - activation_count: 0, - created_at: chrono::Utc::now(), - last_activated: None, - }; - store.add_edge(&e).await.unwrap(); - - let edges = store.get_edges(&ids[0]).await.unwrap(); - assert_eq!(edges.len(), 1); - assert_eq!(edges[0].target_id, ids[1]); - - store.remove_edge(&ids[0], &ids[1], "semantic").await.unwrap(); - assert!(store.get_edges(&ids[0]).await.unwrap().is_empty()); -} - -#[tokio::test(flavor = "multi_thread")] -async fn get_neighbors_with_depth() { - if !docker_available() { return; } - let embedder = TestEmbedder::new_768(); - let (store, _c) = fresh_pg_store(embedder.clone()).await.unwrap(); - let ids = insert_n(&store, embedder.as_ref(), 5).await; - - // Chain: 0 -> 1 -> 2 -> 3 -> 4 - for w in ids.windows(2) { - let e = Edge { - source_id: w[0], - target_id: w[1], - edge_type: "semantic".into(), - strength: 1.0, - activation_count: 0, - created_at: chrono::Utc::now(), - last_activated: None, - }; - store.add_edge(&e).await.unwrap(); - } - - let depth_1 = store.get_neighbors(&ids[0], 1).await.unwrap(); - let depth_2 = store.get_neighbors(&ids[0], 2).await.unwrap(); - let depth_4 = store.get_neighbors(&ids[0], 4).await.unwrap(); - - assert_eq!(depth_1.len(), 1); - assert_eq!(depth_2.len(), 2); - assert_eq!(depth_4.len(), 4); -} -``` - -### 6. `tests/phase_2/migrate_test.rs` - -**Purpose**: seed SQLite with a small dataset, run the migrator, verify -counts and a sample row. - -**Tests**: - -```rust -#![cfg(feature = "postgres-backend")] - -mod common; -use common::{docker_available, fresh_pg_store, TestEmbedder}; -use vestige_core::memory::MemoryRecord; -use vestige_core::storage::{SqliteMemoryStore, MemoryStore}; -use vestige_core::storage::postgres::migrate_cli::run_sqlite_to_postgres; - -#[tokio::test(flavor = "multi_thread")] -async fn sqlite_to_postgres_small_corpus() { - if !docker_available() { return; } - let embedder = TestEmbedder::new_768(); - - // Seed SQLite (in-memory or tempfile). - let tmp = tempfile::tempdir().unwrap(); - let sqlite_path = tmp.path().join("seed.db"); - let sqlite = SqliteMemoryStore::new(&sqlite_path).unwrap(); - sqlite.register_model(&embedder.signature()).await.unwrap(); - for i in 0..50 { - let mut r = MemoryRecord::new(format!("seed row {i}")); - r.tags = vec![format!("tag-{}", i % 3)]; - r.embedding = Some(embedder.embed(&r.content).await.unwrap()); - sqlite.insert(&r).await.unwrap(); - } - - // Spin up Postgres and migrate. - let (pg, _container) = fresh_pg_store(embedder.clone()).await.unwrap(); - let report = run_sqlite_to_postgres(&sqlite, &pg, embedder.clone()).await.unwrap(); - - assert_eq!(report.memories_copied, 50); - assert_eq!(pg.count().await.unwrap(), 50); - - // Spot-check a sample row. - let sample_id = sqlite.list_ids(1, 0).await.unwrap()[0]; - let from_sqlite = sqlite.get(&sample_id).await.unwrap().unwrap(); - let from_pg = pg.get(&sample_id).await.unwrap().unwrap(); - assert_eq!(from_sqlite.content, from_pg.content); - assert_eq!(from_sqlite.tags, from_pg.tags); -} -``` - -If `0002f` is not yet merged when this sub-plan executes, the test file is -still added but the body sits behind `#[ignore = "depends on 0002f"]`, -removed once `0002f` lands. - ---- - -## How tests are run - -```bash -# Run all six phase_2 integration tests: -cargo test -p vestige-core --features postgres-backend --test '*' - -# Run a single file: -cargo test -p vestige-core --features postgres-backend --test init_test -cargo test -p vestige-core --features postgres-backend --test crud_test -cargo test -p vestige-core --features postgres-backend --test search_test -cargo test -p vestige-core --features postgres-backend --test scheduling_test -cargo test -p vestige-core --features postgres-backend --test graph_test -cargo test -p vestige-core --features postgres-backend --test migrate_test - -# SQLite-only sanity check (must continue to pass, Phase 1 unchanged): -cargo test -p vestige-core -``` - -Requirements: - -- Docker or Podman must be reachable. `testcontainers` connects via the - default Docker socket (`/var/run/docker.sock` on Linux, `~/.docker/run/docker.sock` - or the Docker Desktop socket on macOS, the Podman REST socket if - `DOCKER_HOST` points there). -- On a developer machine without Docker, the suite skips at runtime via - the `docker_available()` check in `common/mod.rs`. The test output - includes a `docker unavailable; skip` line per test so the developer - knows the tests were not silently dropped. -- The pgvector image (`pgvector/pgvector:pg18`) is pulled on first run; - ~200 MB. A pre-pulled image keeps the per-run overhead at the cold-start - container boot (~2-5 seconds). - ---- - -## Benches - -**File**: `crates/vestige-core/benches/pg_hybrid_search.rs` - -Two Criterion benches: `search_1k` and `search_100k`. Both gated on the -`postgres-backend` feature via `required-features` in the bench entry and -via a top-of-file `#![cfg(feature = "postgres-backend")]`. - -```rust -//! Criterion benches for the Postgres backend's hybrid RRF search. -#![cfg(feature = "postgres-backend")] - -use std::sync::Arc; -use std::sync::OnceLock; - -use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use rand::{rngs::StdRng, Rng, SeedableRng}; -use testcontainers::runners::AsyncRunner; -use testcontainers::{ContainerAsync, ImageExt}; -use testcontainers_modules::postgres::Postgres; -use tokio::runtime::Runtime; - -use vestige_core::embedder::Embedder; -use vestige_core::memory::MemoryRecord; -use vestige_core::storage::postgres::PgMemoryStore; -use vestige_core::storage::{MemoryStore, SearchQuery}; - -// Bench fixture lives in tests/phase_2/common/test_embedder.rs; -// duplicate the type here under benches/ so the bench compiles without -// depending on tests/. -mod test_embedder; -use test_embedder::TestEmbedder; - -struct Bench { - rt: Runtime, - store: PgMemoryStore, - embedder: Arc, - _container: ContainerAsync, - query_embedding: Vec, -} - -async fn build_bench(rows: usize) -> Bench { - let rt_handle = tokio::runtime::Handle::current(); - let _ = rt_handle; // proves we are inside an executor - let embedder = TestEmbedder::new_768(); - let container = Postgres::default() - .with_name("pgvector/pgvector") - .with_tag("pg18") - .start() - .await - .unwrap(); - let port = container.get_host_port_ipv4(5432).await.unwrap(); - let url = format!("postgresql://postgres:postgres@127.0.0.1:{port}/postgres"); - let store = PgMemoryStore::connect(&url, 8).await.unwrap(); - store.run_migrations().await.unwrap(); - store.register_model(&embedder.signature()).await.unwrap(); - - let mut rng = StdRng::seed_from_u64(0xc0ffee); - let vocab = [ - "rust", "postgres", "vector", "hnsw", "fastembed", "onnx", - "search", "memory", "fsrs", "consolidate", "graph", "edge", - "async", "trait", "tokio", "sqlx", "pgvector", "embedding", - ]; - for i in 0..rows { - let words: String = (0..8) - .map(|_| vocab[rng.gen_range(0..vocab.len())]) - .collect::>() - .join(" "); - let mut r = MemoryRecord::new(format!("{i}: {words}")); - r.tags = vec![format!("tag-{}", i % 7)]; - r.embedding = Some(embedder.embed(&r.content).await.unwrap()); - store.insert(&r).await.unwrap(); - } - let query_embedding = embedder.embed("postgres vector search").await.unwrap(); - Bench { - rt: tokio::runtime::Runtime::new().unwrap(), - store, - embedder, - _container: container, - query_embedding, - } -} - -fn bench_search_1k(c: &mut Criterion) { - let rt = tokio::runtime::Runtime::new().unwrap(); - let bench = rt.block_on(build_bench(1_000)); - c.bench_function("pg_search_1k", |b| { - b.iter(|| { - let q = SearchQuery { - text: Some("postgres vector".into()), - embedding: Some(bench.query_embedding.clone()), - limit: 10, - ..Default::default() - }; - let hits = bench.rt.block_on(bench.store.search(&q)).unwrap(); - black_box(hits); - }) - }); -} - -// Heavy: 100k rows; seed time runs into minutes. Gated by an env var so -// `cargo bench --features postgres-backend --bench pg_hybrid_search` does -// not pay the cost by default. -fn bench_search_100k(c: &mut Criterion) { - if std::env::var("VESTIGE_BENCH_HEAVY").is_err() { - eprintln!("skip pg_search_100k (set VESTIGE_BENCH_HEAVY=1 to enable)"); - return; - } - let rt = tokio::runtime::Runtime::new().unwrap(); - let bench = rt.block_on(build_bench(100_000)); - c.bench_function("pg_search_100k", |b| { - b.iter(|| { - let q = SearchQuery { - text: Some("postgres vector".into()), - embedding: Some(bench.query_embedding.clone()), - limit: 10, - ..Default::default() - }; - let hits = bench.rt.block_on(bench.store.search(&q)).unwrap(); - black_box(hits); - }) - }); -} - -criterion_group!(benches, bench_search_1k, bench_search_100k); -criterion_main!(benches); -``` - -**File**: `crates/vestige-core/benches/test_embedder.rs` - -Duplicate of `tests/phase_2/common/test_embedder.rs`. Cargo's bench target -cannot `mod` into `tests/`; the duplication is the standard fix. Keep both -files in sync; if either grows non-trivially, refactor into a shared -`pub(crate)` module under `src/embedder/test_support.rs` gated on -`#[cfg(any(test, feature = "test-support"))]`. - -`VESTIGE_BENCH_HEAVY` gate: the 100k seed step takes several minutes (one -`INSERT` per row plus HNSW upsert). Skipping by default keeps `cargo bench` -under a minute for the 1k bench. Document this gate in the runbook -(`0002i`). - ---- - -## Cargo.toml - -Final state of the relevant sections of -`crates/vestige-core/Cargo.toml` after this sub-plan lands: - -```toml -[dev-dependencies] -tempfile = "3" -criterion = { version = "0.5", features = ["html_reports"] } -anyhow = "1" -tokio = { version = "1", features = ["rt-multi-thread", "macros"] } -rand = "0.8" -testcontainers = { version = "0.22", optional = true } -testcontainers-modules = { version = "0.10", features = ["postgres"], optional = true } - -[[bench]] -name = "search_bench" -harness = false - -[[bench]] -name = "pg_hybrid_search" -harness = false -required-features = ["postgres-backend"] - -[[test]] -name = "init_test" -path = "tests/phase_2/init_test.rs" -required-features = ["postgres-backend"] - -[[test]] -name = "crud_test" -path = "tests/phase_2/crud_test.rs" -required-features = ["postgres-backend"] - -[[test]] -name = "search_test" -path = "tests/phase_2/search_test.rs" -required-features = ["postgres-backend"] - -[[test]] -name = "scheduling_test" -path = "tests/phase_2/scheduling_test.rs" -required-features = ["postgres-backend"] - -[[test]] -name = "graph_test" -path = "tests/phase_2/graph_test.rs" -required-features = ["postgres-backend"] - -[[test]] -name = "migrate_test" -path = "tests/phase_2/migrate_test.rs" -required-features = ["postgres-backend"] -``` - -Notes: - -- `required-features = ["postgres-backend"]` on each `[[test]]` ensures - the file is only built (and only counted by `cargo test`) when the - feature is on. Cargo silently skips it otherwise -- exactly the desired - behavior for default `cargo test` runs. -- The benches use the same `required-features` shape so default - `cargo bench` is unaffected. - ---- - -## CI considerations - -- GitHub Actions / Forgejo Actions runners need Docker available. Default - `ubuntu-latest` runners include Docker. Self-hosted Forgejo runners on - TFGrid VMs must install `docker.io` or run `podman` with the Docker - socket compatibility shim. Document the runner requirement in the - runbook (`0002i`). -- The Postgres feature tests should run in a separate CI matrix entry to - isolate failures and skip them entirely on platforms (Windows runners - if any) where the pgvector image is not available. -- Cache the `pgvector/pgvector:pg18` image between runs. The - `docker/setup-buildx-action` cache or a simple `docker pull` step before - the test step keeps cold-start under the existing CI time budget. -- Skip CI: contributors without Docker can still merge changes that do - not touch `storage/postgres/`. The pre-merge required check is "phase_2 - tests pass on the runner with Docker"; the local pre-commit hook does - not gate on it. -- Bench CI: do not run `pg_search_100k` in regular CI; only run it - manually or on a scheduled weekly job and post results to the PR - description / ADR comment trail. - -Recommended CI job shape (sketch): - -```yaml -jobs: - postgres-tests: - runs-on: ubuntu-latest - services: - # no `postgres` service block needed; testcontainers manages its own - steps: - - uses: actions/checkout@v4 - - run: docker pull pgvector/pgvector:pg18 - - uses: dtolnay/rust-toolchain@stable - - run: cargo test -p vestige-core --features postgres-backend --test '*' -``` - ---- - -## Verification - -After all files are in place: - -```bash -# Default build still clean (no postgres deps pulled in): -cargo build -p vestige-core -cargo test -p vestige-core - -# Postgres feature build + integration tests: -cargo build -p vestige-core --features postgres-backend -cargo test -p vestige-core --features postgres-backend - -# Just the new tests: -cargo test -p vestige-core --features postgres-backend --test '*' - -# Quick bench sanity check (1k only): -cargo bench -p vestige-core --features postgres-backend --bench pg_hybrid_search -- --quick - -# Heavy bench (manual, multi-minute seed step): -VESTIGE_BENCH_HEAVY=1 cargo bench -p vestige-core \ - --features postgres-backend \ - --bench pg_hybrid_search -- --quick - -# Clippy with everything on: -cargo clippy -p vestige-core --features postgres-backend --all-targets -- -D warnings -``` - -Expected results: - -- Default build is unchanged; no testcontainers deps in `Cargo.lock`'s - default resolution. -- With `--features postgres-backend`, all six integration tests pass on a - machine with Docker available, or each prints `docker unavailable; skip` - and exits 0. -- `cargo bench ... -- --quick` produces a `pg_search_1k` line with a - p50 below the master plan's 10 ms target on a developer laptop (looser - on a CI runner -- the target is informative, not gated). - ---- - -## Acceptance criteria - -- [ ] `crates/vestige-core/tests/phase_2/common/mod.rs` and - `test_embedder.rs` exist and compile under - `--features postgres-backend`. -- [ ] All six integration test files exist, each with - `#![cfg(feature = "postgres-backend")]` at the top. -- [ ] Each test file has a corresponding `[[test]]` entry in - `Cargo.toml` with `required-features = ["postgres-backend"]`. -- [ ] `crates/vestige-core/benches/pg_hybrid_search.rs` exists with - `search_1k` and `search_100k` benches, the latter gated on - `VESTIGE_BENCH_HEAVY`. -- [ ] `[[bench]] name = "pg_hybrid_search"` entry present with - `required-features = ["postgres-backend"]`. -- [ ] `testcontainers@0.22` and `testcontainers-modules@0.10` with the - `postgres` feature are in `[dev-dependencies]` of `vestige-core`. -- [ ] `anyhow`, `tokio`, `rand` are in `[dev-dependencies]`. -- [ ] `cargo build -p vestige-core` (default features) is unchanged: no - testcontainers in the build graph; no new warnings. -- [ ] `cargo test -p vestige-core` (default features) passes with no - changes to the Phase 1 test count beyond what `0002a..g` already - moved. -- [ ] `cargo test -p vestige-core --features postgres-backend --test '*'` - passes on a runner with Docker available, or skips cleanly with the - `docker unavailable; skip` lines. -- [ ] `cargo bench -p vestige-core --features postgres-backend - --bench pg_hybrid_search -- --quick` runs `pg_search_1k` to - completion and does NOT run `pg_search_100k` unless - `VESTIGE_BENCH_HEAVY=1`. -- [ ] `cargo clippy -p vestige-core --features postgres-backend - --all-targets -- -D warnings` is clean. -- [ ] The runbook (`0002i`) gets a one-paragraph "How to run the test - suite locally" callout referring back to this sub-plan's - "Verification" section. (`0002i` is owned separately; this sub-plan - just lists the dependency.) - ---- - -## Open questions for the implementer - -1. **Migration helper name.** `0002c` decides whether - `PgMemoryStore::run_migrations(&self)` or - `vestige_core::storage::postgres::migrations::run(&pool)` is the public - call. Update `common/mod.rs` to match. -2. **Update-on-missing contract.** `0002d` decides whether - `MemoryStore::update` returns `Err(NotFound)` or `Ok(())` with zero - affected rows when the id does not exist. The CRUD test stub here - accepts either; tighten the assert once the contract is fixed. -3. **Empty-query search contract.** `0002e` decides whether - `SearchQuery { text: None, embedding: None }` is `Ok(empty)` or an - error. Same tightening pattern as #2. -4. **Pool size for 100k bench.** Current value is 8; if the bench - bottlenecks on the pool, tune up to 16 or 32 and document in the - bench file's leading doc comment. -5. **Shared `TestEmbedder` location.** Currently duplicated between - `tests/phase_2/common/test_embedder.rs` and - `benches/test_embedder.rs`. If duplication bothers a reviewer, lift to - `crates/vestige-core/src/embedder/test_support.rs` behind a - `test-support` Cargo feature pulled in by both `tests` and `benches`. - Out of scope for this sub-plan; record as a follow-up. diff --git a/docs/plans/0002i-runbook.md b/docs/plans/0002i-runbook.md deleted file mode 100644 index f63694f..0000000 --- a/docs/plans/0002i-runbook.md +++ /dev/null @@ -1,724 +0,0 @@ -# Phase 2 Sub-Plan 0002i -- Postgres Ops Runbook - -**Status**: Ready -**Depends on**: Phase 2 sub-plans 0002a through 0002h merged (or at least -their interfaces stable). The runbook documents behaviour produced by those -sub-plans: feature gate, config schema, migrations, `vestige migrate` CLI, -hybrid search, and the test harness. Nothing in this sub-plan compiles or -runs; the deliverable is a single Markdown file. - -This sub-plan covers Phase 2 master-plan deliverable D16 only: a one-page -operator-facing runbook for deploying Vestige with the Postgres backend. - ---- - -## Context - -Why a runbook. The ADR (0002) and the master plan (0002) are written for -implementors. They settle execution-level decisions and itemise deliverables. -They are not deployable instructions. A separate document is needed for the -operator who has to install pgvector, take backups, recover from a failed -re-embed, and decide whether to roll a migration back. The runbook is that -document. - -Who reads it. Ops people, not developers. Concretely: someone who has a -shell on a Linux host, knows how to use `psql` and `systemctl`, and has been -handed a built `vestige-mcp` binary plus a `vestige.toml`. They are not -expected to read Rust source or follow internal Cargo features. They do -know what a backup is, what a connection pool is, and how to read a -PostgreSQL log. - -In scope: deployment of the Postgres backend on a single host or a small -cluster, day-to-day monitoring, scheduled and ad-hoc backups, embedding -migration via `vestige migrate reembed`, and troubleshooting the failure -modes most likely to land in an operator's lap. - -Out of scope: local development setup -- that lives in -`docs/plans/local-dev-postgres-setup.md` and the runbook links to it for -developer onboarding only. Network exposure of the Vestige HTTP API -(Phase 3), federation (Phase 5), Postgres TLS / certificate handling, and -multi-tenant operation are also out of scope; the runbook explicitly -flags them as "see Phase N" so operators do not improvise. - -This sub-plan is the plan for producing the runbook. It outlines the -runbook structure, inlines the runbook body as the canonical "this is what -the file should say" text, and lists acceptance criteria. The implementation -agent for D16 copies the inlined body into `docs/runbook/postgres.md`, -creating `docs/runbook/` if it does not already exist. No other files in the -repository are modified. - ---- - -## Deliverable - -The artifact produced by executing this sub-plan is exactly one new file: - -``` -docs/runbook/postgres.md -``` - -It is NOT under `docs/plans/`. Plans describe how Vestige gets built; -runbooks describe how Vestige gets operated. The two directories are -deliberately separated. - -Side effect: create the directory `docs/runbook/` if it does not exist. -Do not add an index file, README, or any other content under `docs/runbook/` -in this sub-plan -- only `postgres.md`. - -This sub-plan document (`docs/plans/0002i-runbook.md`) is itself NOT a -deliverable in the operator sense. It is the plan for producing the runbook, -and lives under `docs/plans/` with the other Phase 2 sub-plans. - ---- - -## Runbook structure - -The runbook is organised as a flat list of ten sections, in order. Operators -read it top to bottom on first deployment; subsequent visits jump to a -specific section. Section numbering matches the inlined body below. - -1. **Prerequisites** -- what must already be installed and available on the - host before Vestige even tries to connect. PostgreSQL 16 or newer - (18 on Arch is fine), pgvector >= 0.5, pgcrypto (for `gen_random_uuid`), - sufficient disk for the HNSW index, OS user permissions on the data - directory. - -2. **Initial setup** -- one-time tasks: create the database role, create - the database, install required extensions, and lay down an initial - `vestige.toml`. Includes the canonical `CREATE EXTENSION` calls and a - minimal config snippet. - -3. **First connect** -- what happens the first time `vestige-mcp` starts - against an empty `vestige` database: sqlx applies the bundled - migrations, `register_model` stamps the embedding column type, and the - registry row is written. How an operator verifies each step succeeded - using `psql`. - -4. **Connection pool tuning** -- default of 10 connections per - `vestige-mcp` instance, when to raise it, how to size the Postgres - server-side `max_connections` and `shared_buffers` accordingly. Cross- - reference to `vestige.toml` and to ADR 0002 D2 / open question Q5. - -5. **Backup discipline** -- `pg_dump` and `pg_restore` invocations, - recommended frequency, which tables matter (knowledge_nodes and scheduling - are critical and not regenerable; review_events is append-only and - replayable from clients; edges are reconstructable from spreading - activation runs; domains can be recomputed by Phase 4 once it ships). - Also covers backup verification (restore-to-tmp drill). - -6. **Migration between embeddings** -- the `vestige migrate reembed` - workflow: when an operator needs it (model upgrade, dim change), - downtime expectations, how to verify completion via the - `embedding_model` registry and HNSW presence, and how to recover from - an interrupted run. - -7. **Re-clustering domains** -- a brief forward reference. Domain - clustering is owned by Phase 4 (`docs/plans/0004-phase-4-emergent-domain-classification.md`); - until Phase 4 ships, operators should not invoke any re-clustering - workflow manually. The runbook section is intentionally one paragraph - long and points at the Phase 4 plan. - -8. **Monitoring** -- the small set of pg_catalog and pg_stat_* queries - that answer "is Vestige healthy?": `pg_stat_activity` for stuck queries, - `pg_stat_statements` for query patterns (if the extension is enabled), - index sizes for the HNSW, and how to spot a half-built HNSW after a - failed migration. - -9. **Troubleshooting** -- a table of common errors with the symptom and - the fix. Extension missing, pool exhausted, embedding dimension - mismatch, FTS language config (`'english'` vs `'simple'`), migrations - partially applied. - -10. **Rollback caveats** -- every `*.up.sql` has a `*.down.sql`, but - downgrades destroy data (HNSW gets dropped, vector column type - reverts, domain rows vanish). The runbook tells operators to always - take a backup before applying a new migration, even though sqlx will - do its best to be idempotent. - ---- - -## Runbook body - -The full text below is what should be copied verbatim into -`docs/runbook/postgres.md`. ASCII only. Code blocks use fenced syntax with -language hints. Operator-facing prose; second person ("you") for -instructions. Where a command requires sudo, the prompt shows it explicitly. - -```markdown -# Vestige Postgres Backend -- Operator Runbook - -This runbook covers deploying, operating, monitoring, and recovering a -Vestige installation that uses the Postgres backend. It is written for -operators handling a built `vestige-mcp` binary and a `vestige.toml`. - -For local development setup, see -`docs/plans/local-dev-postgres-setup.md`. For the architectural rationale, -see `docs/adr/0001-pluggable-storage-and-network-access.md` and -`docs/adr/0002-phase-2-execution.md`. For the deliverable-level plan, see -`docs/plans/0002-phase-2-postgres-backend.md`. - ---- - -## 1. Prerequisites - -Before Vestige can connect: - -- PostgreSQL server, version 16 or newer. Arch ships 18.x; Debian stable - ships 16.x; both work. -- `pgvector` extension, version 0.5 or newer. Distro packages: - `pgvector` on Arch, `postgresql-16-pgvector` on Debian/Ubuntu. -- `pgcrypto` extension, shipped with the PostgreSQL contrib package - (`postgresql-contrib` on Debian, included in the base `postgresql` - package on Arch). Vestige uses `gen_random_uuid()` from pgcrypto for - primary keys. -- Disk space: budget roughly 4x the size of your `knowledge_nodes.embedding` - column for the HNSW index. With 768-dim float32 vectors at 100k - memories, that is about 1.2 GB for the embeddings plus 4-5 GB for the - HNSW index. Plan accordingly. -- OS user: the `postgres` system user (or whatever user owns - `/var/lib/postgres/data`) must have read/write on the data directory. - Vestige itself does not need filesystem access to Postgres; it talks - TCP only. -- Network: Vestige and Postgres can be on the same host (loopback) or - different hosts. If different hosts, allow the Vestige host's IP in - `pg_hba.conf` and on any firewall. - ---- - -## 2. Initial setup - -These steps run once per Postgres cluster. - -### 2.1 Install extensions - -As the `postgres` superuser: - -```sh -sudo -u postgres psql -d vestige <<'SQL' -CREATE EXTENSION IF NOT EXISTS vector; -CREATE EXTENSION IF NOT EXISTS pgcrypto; -SQL -``` - -Verify: - -```sh -sudo -u postgres psql -d vestige -c \ - "SELECT extname, extversion FROM pg_extension WHERE extname IN ('vector','pgcrypto');" -``` - -You should see two rows. If `vector` is missing, the pgvector package was -not installed for the right PostgreSQL major version; reinstall it. - -### 2.2 Create the role and database - -The `vestige` role owns its own database; it does NOT need superuser. -Extensions must be installed by `postgres`, not by `vestige`. - -```sh -sudo -u postgres psql -v ON_ERROR_STOP=1 <<'SQL' -CREATE ROLE vestige WITH LOGIN CREATEDB PASSWORD 'CHANGE_ME'; -CREATE DATABASE vestige OWNER vestige ENCODING 'UTF8'; -GRANT ALL PRIVILEGES ON DATABASE vestige TO vestige; -SQL - -sudo -u postgres psql -d vestige -v ON_ERROR_STOP=1 <<'SQL' -GRANT ALL ON SCHEMA public TO vestige; -ALTER SCHEMA public OWNER TO vestige; -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO vestige; -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO vestige; -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON FUNCTIONS TO vestige; -SQL -``` - -Replace `CHANGE_ME` with a strong password and store it where Vestige can -read it (typically `~/.vestige_pg_pw`, mode 600, owned by the user running -`vestige-mcp`). - -### 2.3 Minimal `vestige.toml` - -```toml -[storage] -backend = "postgres" - -[storage.postgres] -url = "postgresql://vestige:CHANGE_ME@127.0.0.1:5432/vestige" -max_connections = 10 -``` - -The `url` field accepts a `${VAR}` placeholder; in practice operators -either inline the password or export `DATABASE_URL` and reference -`url = "${DATABASE_URL}"`. See `docs/CONFIGURATION.md` for the full -schema once Phase 3 lands. - ---- - -## 3. First connect - -When `vestige-mcp` starts against an empty `vestige` database, it: - -1. Builds a `PgPool` of `max_connections` (default 10) connections. -2. Runs every migration in `crates/vestige-core/migrations/postgres/` - in order. The bundled migrations are `0001_init` (tables, non-vector - indexes) and `0002_hnsw` (HNSW index on `knowledge_nodes.embedding`). -3. Calls `register_model` once it knows the active embedder's dimension. - This issues `ALTER TABLE knowledge_nodes ALTER COLUMN embedding TYPE - vector($N)` and inserts a row into `embedding_model`. -4. Begins accepting MCP requests. - -To verify after the first start: - -```sh -sudo -u postgres psql -d vestige <<'SQL' --- All expected tables present. -\dt --- embedding_model has exactly one row. -SELECT name, dimension, hash FROM embedding_model; --- The HNSW index exists. -SELECT indexname FROM pg_indexes - WHERE tablename = 'knowledge_nodes' AND indexname LIKE '%hnsw%'; -SQL -``` - -Expected: `knowledge_nodes`, `scheduling`, `edges`, `domains`, `review_events`, -`embedding_model`, `users`, `groups`, `group_memberships`; one row in -`embedding_model`; one `idx_knowledge_nodes_embedding_hnsw` index. - -If a migration fails mid-way, the partial state lands in -`_sqlx_migrations`. See section 9 for recovery. - ---- - -## 4. Connection pool tuning - -Defaults: - -- Vestige client pool: `max_connections = 10` per `vestige-mcp` instance. -- Postgres server: `max_connections = 100` (default). - -Math: one MCP client with the default pool uses up to 10 server slots. -Five concurrent MCP clients use up to 50 slots. The remaining 50 cover -`psql` sessions, background workers, and headroom for replication or -backup processes. - -When to raise: - -- More than three MCP clients connecting to one Postgres instance. -- Long-running queries (above 500ms p99) showing pool wait time in - Vestige logs (look for `pool acquire timed out` warnings). -- A noticeable number of concurrent dream/consolidation runs. - -How to raise: - -```toml -[storage.postgres] -max_connections = 20 # client side, per vestige-mcp instance -``` - -And on the Postgres server, edit `postgresql.conf`: - -```conf -max_connections = 200 -shared_buffers = 2GB # roughly 25 percent of RAM, never above 8GB -``` - -Then restart Postgres (`sudo systemctl restart postgresql`). Vestige -clients pick up their own `max_connections` change on next restart. - -Do not raise pool sizes blindly. Past about 4x the CPU core count, -Postgres throughput drops; a small connection pooler (PgBouncer in -transaction mode) is the right answer above ~200 client connections, -but Vestige's expected scale rarely needs that. - ---- - -## 5. Backup discipline - -### 5.1 Which tables matter - -| Table | Backup priority | Regenerable? | -|-------|-----------------|--------------| -| `knowledge_nodes` | Critical | No | -| `scheduling` | Critical | No (FSRS state) | -| `embedding_model` | Critical | No (one row, but stamps the column type) | -| `users`, `groups`, `group_memberships` | Critical | No (Phase 3 will populate) | -| `review_events` | Important | Replayable by clients but tedious | -| `edges` | Optional | Yes (recomputed by spreading activation) | -| `domains` | Optional | Yes (Phase 4 recomputes by clustering) | - -For a typical single-operator install, dumping the whole database is -fastest and simplest. Skip the optional tables only if dump size becomes -a bandwidth problem. - -### 5.2 Full logical backup - -```sh -pg_dump --host=127.0.0.1 --username=vestige --format=custom \ - --file=vestige-$(date -u +%Y%m%dT%H%M%SZ).dump \ - vestige -``` - -The custom format compresses by default and works with parallel restore. -File size for 10k memories: roughly 80 MB. - -Frequency recommendations: - -- Daily for any installation with active ingest. -- Before every `vestige migrate reembed` run (see section 6). -- Before every Postgres major-version upgrade. -- Retain at least 7 daily, 4 weekly, 3 monthly dumps. Compress with - `--format=custom` (already gzipped) and keep them on different - storage from the database itself. - -### 5.3 Restore - -To a fresh database: - -```sh -sudo -u postgres createdb -O vestige vestige_restore -pg_restore --host=127.0.0.1 --username=vestige --dbname=vestige_restore \ - --jobs=4 vestige-20260301T030000Z.dump -``` - -To replace the live database (destructive; only after taking a fresh -dump): - -```sh -sudo systemctl stop vestige-mcp # or however the service is run -sudo -u postgres dropdb vestige -sudo -u postgres createdb -O vestige vestige -pg_restore --host=127.0.0.1 --username=vestige --dbname=vestige \ - --jobs=4 vestige-20260301T030000Z.dump -sudo systemctl start vestige-mcp -``` - -### 5.4 Restore drill - -Run a restore-to-throwaway-database every month and run `vestige search` -or a manual `psql` count against it. A backup you have not restored is a -backup you do not have. - -```sh -sudo -u postgres createdb -O vestige vestige_restore_drill -pg_restore --host=127.0.0.1 --username=vestige --dbname=vestige_restore_drill \ - --jobs=4 vestige-latest.dump -PGPASSWORD="$(cat ~/.vestige_pg_pw)" psql -h 127.0.0.1 -U vestige \ - -d vestige_restore_drill \ - -c 'SELECT count(*) FROM knowledge_nodes;' -sudo -u postgres dropdb vestige_restore_drill -``` - ---- - -## 6. Migration between embeddings - -Use `vestige migrate reembed` when: - -- Upgrading to a new embedding model that produces a different dimension - (for example, swapping from `nomic-embed-text-v1.5` 768D to a 1024D - model). -- Switching providers and the model hash differs even at the same - dimension. - -What it does: - -1. Reads every row from `knowledge_nodes`, re-encodes the `content` column - through the new embedder, and writes the new vector back. -2. Drops the HNSW index before the re-encode loop (this is the default; - `--concurrent-index` keeps it during the run at the cost of speed). -3. Updates the `embedding_model` row with the new name, dimension, and - hash. -4. Rebuilds the HNSW index with the new vectors. - -### 6.1 Before starting - -- Take a fresh backup (section 5.2). The tool refuses to start without a - `--yes` flag if it detects no recent backup; ignore at your peril. -- Stop ingest. Vestige's MCP server can stay running for read-only - access, but pause any client that calls `smart_ingest` or - `update_scheduling`. -- Have the new embedder model available locally. The CLI loads it - before the first row is touched; if loading fails, no data is changed. - -### 6.2 Running - -```sh -vestige migrate reembed --model= --yes -``` - -Add `--concurrent-index` if you cannot accept the brief window during -HNSW rebuild where queries do not use the index (sequential scan -fallback works but is slow). - -The tool prints a progress bar via `indicatif`. Expected throughput: -roughly 200 memories per second per CPU core for a 768D ONNX model. -10,000 memories on an 8-core box: about 6 seconds, plus HNSW rebuild -(another 30-90 seconds at that scale). - -### 6.3 Verifying completion - -```sh -sudo -u postgres psql -d vestige <<'SQL' --- Registry reflects the new model. -SELECT name, dimension, hash FROM embedding_model; --- HNSW index is present and not partial. -SELECT indexname, indexdef - FROM pg_indexes - WHERE tablename = 'knowledge_nodes' AND indexname LIKE '%hnsw%'; --- All rows have a non-null embedding of the new dimension. -SELECT count(*) FILTER (WHERE embedding IS NULL) AS missing, - count(*) AS total - FROM knowledge_nodes; -SQL -``` - -Expected: registry shows the new model name and dimension, one HNSW -index, zero missing embeddings. - -### 6.4 Recovering from an interrupted run - -`vestige migrate reembed` is restartable. On interruption: - -- The `embedding_model` row may or may not have been updated. Check it - manually and roll forward by re-running with `--yes --resume` (the - tool detects the inconsistency and finishes the rows that still hold - old embeddings). -- The HNSW index may be missing. Re-running the command rebuilds it as - its last step. -- If the system is in a state the tool refuses to reason about, restore - from the backup taken in 6.1. - ---- - -## 7. Re-clustering domains - -Domain clustering is owned by Phase 4 -(`docs/plans/0004-phase-4-emergent-domain-classification.md`). Until -Phase 4 ships, the `domains` table is reserved schema and is populated -only by tests. Operators must not invoke any domain re-clustering -workflow manually; there is no supported one in Phase 2. - -When Phase 4 lands, this section is replaced with the real procedure. - ---- - -## 8. Monitoring - -### 8.1 Quick health check - -```sh -PGPASSWORD="$(cat ~/.vestige_pg_pw)" psql -h 127.0.0.1 -U vestige -d vestige <<'SQL' -SELECT count(*) AS memory_count FROM knowledge_nodes; -SELECT name, dimension FROM embedding_model; -SELECT pg_size_pretty(pg_database_size('vestige')) AS db_size; -SQL -``` - -### 8.2 In-flight queries - -```sql -SELECT pid, now() - query_start AS runtime, state, query - FROM pg_stat_activity - WHERE datname = 'vestige' AND state <> 'idle' - ORDER BY runtime DESC NULLS LAST; -``` - -Anything over 5 seconds with `state = 'active'` deserves a look. HNSW -search queries should land well under 100ms on properly-sized hardware. - -### 8.3 Query pattern analysis - -If `pg_stat_statements` is loaded (`shared_preload_libraries = -'pg_stat_statements'` in `postgresql.conf`): - -```sql -SELECT calls, mean_exec_time, query - FROM pg_stat_statements - WHERE query ILIKE '%knowledge_nodes%' - ORDER BY mean_exec_time DESC - LIMIT 20; -``` - -Look for hybrid-search queries that have drifted above 100ms p50. The -usual culprit is a missing or half-built HNSW index. - -### 8.4 Index health - -```sql -SELECT indexname, pg_size_pretty(pg_relation_size(indexrelid)) AS size, - idx_scan, idx_tup_read - FROM pg_indexes - JOIN pg_stat_user_indexes USING (indexrelid) - WHERE schemaname = 'public' AND relname = 'knowledge_nodes'; -``` - -A HNSW index with `idx_scan = 0` after several hours of traffic usually -means the planner is preferring sequential scan -- either the table is -too small to bother with the index (fine) or the index is corrupt and -needs rebuilding (`REINDEX INDEX idx_knowledge_nodes_embedding_hnsw;`). - -### 8.5 Spotting half-built HNSW - -After a failed migration or a crashed `reembed`: - -```sql -SELECT indexname, indisvalid, indisready - FROM pg_indexes - JOIN pg_index ON indexrelid = (schemaname || '.' || indexname)::regclass - WHERE tablename = 'knowledge_nodes'; -``` - -Any row with `indisvalid = false` is broken. Drop and recreate: - -```sql -DROP INDEX IF EXISTS idx_knowledge_nodes_embedding_hnsw; -CREATE INDEX idx_knowledge_nodes_embedding_hnsw - ON knowledge_nodes USING hnsw (embedding vector_cosine_ops); -``` - ---- - -## 9. Troubleshooting - -| Symptom | Likely cause | Fix | -|---------|--------------|-----| -| `ERROR: extension "vector" is not available` on start | pgvector not installed for this Postgres major version | Install the distro package matching `pg_config --version`, then `CREATE EXTENSION vector;` as superuser | -| `pool timed out while waiting for an open connection` in Vestige logs | Pool too small or stuck queries holding connections | Raise `max_connections` in `vestige.toml`; investigate `pg_stat_activity` for queries above 5s | -| `vector dimensions do not match` on insert | `embedding_model` was stamped at one dimension and a different embedder is now running | Re-run `vestige migrate reembed --model=` or fix the embedder configuration | -| Hybrid search returns the same row twice | Stale `.sqlx/` query cache from before D5 landed | Run `cargo sqlx prepare` in `crates/vestige-core/`, rebuild the binary | -| `text search configuration "english" does not exist` | Postgres locale build does not include the english dictionary (rare on Alpine) | Install the language-pack or override the FTS language in `vestige.toml` (see `[storage.postgres.fts]` once Phase 2 D5 lands) | -| `relation "_sqlx_migrations" exists, but migration X is in "applied" with no checksum` | Previous run died between `BEGIN` and `COMMIT` | Stop Vestige, restore from backup, restart | -| HNSW index very large compared to data | `m` and `ef_construction` defaults too high for the corpus | Acceptable for now; tuning lands as part of Phase 4 | -| `permission denied for schema public` on a new install | `vestige` role does not own `public` | Re-run the grants block in section 2.2 as `postgres` | - -If a problem is not in this table, capture: PostgreSQL log -(`/var/log/postgres/`, journalctl `-u postgresql`), Vestige log -(`RUST_LOG=debug,sqlx=info` for a fresh run), the migration state -(`SELECT * FROM _sqlx_migrations ORDER BY version;`), and file a bug. - ---- - -## 10. Rollback caveats - -Every migration in `crates/vestige-core/migrations/postgres/` has a -matching `*.down.sql`. `sqlx migrate revert` walks them in reverse order. - -This is not the same as risk-free. The `0002_hnsw.down.sql` drops the -HNSW index (rebuildable, expensive). The `0001_init.down.sql` drops -every table -- including `knowledge_nodes`, including data. Down migrations -exist for development, not for casual production use. - -Before applying any new migration: - -1. Take a backup (section 5.2). -2. Run the migration on a restored copy first if you can afford the time. -3. Read the new migration's `*.up.sql` and `*.down.sql` to understand - what changes. - -To revert one migration manually: - -```sh -sqlx migrate revert \ - --database-url "postgresql://vestige:...@127.0.0.1:5432/vestige" \ - --source crates/vestige-core/migrations/postgres -``` - -Note that Vestige's binary does not run `sqlx migrate revert` -automatically. Reverts are always an explicit operator decision. - -If a revert fails partway through, treat the database as inconsistent: -restore from the backup taken in step 1. -``` - ---- - -## Cross-references - -- `docs/adr/0001-pluggable-storage-and-network-access.md` -- ADR that - established the pluggable backend. -- `docs/adr/0002-phase-2-execution.md` -- ADR settling Phase 2 execution - decisions; section "Architecture Overview" lists every table the - runbook references. -- `docs/plans/0002-phase-2-postgres-backend.md` -- master plan; D16 - (deliverables list) and the Open Implementation Questions section - (especially Q4 HNSW rebuild and Q5 pool sizing) inform the runbook's - recommendations. -- `docs/plans/local-dev-postgres-setup.md` -- developer-facing recipe - for a one-machine Arch / CachyOS dev cluster. The runbook links to it - as the "for development, see" pointer. -- `docs/CONFIGURATION.md` -- existing config doc; section 4 of the - runbook ("Connection pool tuning") cross-references it for the - authoritative `vestige.toml` schema. - ---- - -## Verification - -A reviewer is given: - -- A fresh Linux VM (Debian 12 or Arch current; both must work) with - network access and no Postgres installed. -- A built `vestige-mcp` binary for that platform. -- The runbook (`docs/runbook/postgres.md`). - -The reviewer follows the runbook top to bottom and reaches a state in -which Vestige answers MCP requests against the Postgres backend. -Checkpoints, in order: - -1. After section 1 (Prerequisites): `pg_config --version` returns 16 or - newer; `pkg-config --modversion libpq` resolves; the `pgvector` - distro package is installed. -2. After section 2.1 (Extensions): two rows in - `SELECT extname FROM pg_extension WHERE extname IN ('vector', 'pgcrypto');`. -3. After section 2.2 (Role + DB): `psql -U vestige -h 127.0.0.1 -d vestige -c '\conninfo'` - succeeds. -4. After section 2.3 (Config): `vestige.toml` parses (test by - `vestige config print` once that subcommand lands, otherwise - `vestige-mcp --check-config`). -5. After section 3 (First connect): the eight expected tables are - present; `embedding_model` has exactly one row; the HNSW index - exists; `vestige-mcp` log shows "Postgres backend ready". -6. After section 5.2 (Backup): the dump file exists and `pg_restore -l` - on it lists the expected tables. -7. After section 5.4 (Restore drill): the drill database holds the same - row count as the source. - -If any checkpoint fails, the runbook section that produced the failure -is the one that needs revision. Capture the exact command, exit code, -and log line; revise the runbook in a follow-up PR. - -A second reviewer reads the runbook without executing it and checks for: - -- ASCII only; no em dashes, no curly quotes, no Unicode arrows, no - ellipses, no bullets (`*`/`-` ASCII only). -- Every section number from 1 to 10 present and in order. -- Every cross-reference resolves to an existing file or to a Phase - number explicitly marked as "future". -- No code block longer than 30 lines; if longer, it should be split or - referenced from another file. - ---- - -## Acceptance criteria - -- [ ] `docs/runbook/` directory exists. -- [ ] `docs/runbook/postgres.md` exists and matches the inlined body - above byte-for-byte after stripping the outer code fence used in - this sub-plan to embed it. -- [ ] All ten sections from the "Runbook structure" outline are present - under their stated headings. -- [ ] No file other than `docs/runbook/postgres.md` is created or - modified by executing this sub-plan. -- [ ] ASCII only: no em dashes, no curly quotes, no Unicode arrows, - no ellipses, no Unicode bullets (`grep -P '[^\x00-\x7F]' - docs/runbook/postgres.md` returns no matches). -- [ ] Every cross-reference in the runbook points at a file that exists - in the repository at the time of merge, OR is explicitly framed - as "future Phase N" with a pointer to the relevant plan document. -- [ ] Every command block is copy-pastable: no `` syntax - that does not also have an inline note describing what to - substitute. -- [ ] A second pair of eyes confirms the verification checkpoints in the - preceding section are reproducible. -- [ ] The runbook is no longer than the inlined body in this sub-plan; - operators reach the end without losing patience. diff --git a/docs/plans/0003-phase-3-network-access.md b/docs/plans/0003-phase-3-network-access.md deleted file mode 100644 index 500fd5a..0000000 --- a/docs/plans/0003-phase-3-network-access.md +++ /dev/null @@ -1,1435 +0,0 @@ -# Phase 3 Plan: Network Access and Authentication - -**Status**: Draft -**Depends on**: Phase 1 (MemoryStore trait), Phase 2 (PgMemoryStore, backend config) -**Related**: docs/adr/0001-pluggable-storage-and-network-access.md (Phase 3) - ---- - -## Scope - -### In scope - -- HTTP MCP Streamable endpoint at `POST /mcp` (JSON-RPC body, keep existing - session semantics) and `GET /mcp` (Server-Sent Events for long-running - operations: dream, consolidate, discover, reassign). -- REST API under `/api/v1/` for direct HTTP clients that do not speak MCP - (memories CRUD, search, consolidate trigger, stats, domains - list/rename/merge/discover). -- `api_keys` table + enforcement (blake3-hashed, scopes `read`/`write`, optional - `domain_filter` TEXT[], `last_used` timestamp, `active` flag, revocation). -- Auth middleware with three resolution paths in priority order: - `Authorization: Bearer ` then `X-API-Key: ` then signed session - cookie. All three resolve to the same `ApiKeyIdentity`. -- Signed session cookie: `vestige_session`, SameSite=Strict, HttpOnly, - Secure-when-TLS, Path=/, Max-Age 8 hours. Signed with HMAC-SHA256 using a - key derived from `VESTIGE_SESSION_SECRET` (env) or generated + persisted to - `/session_secret` on first boot. -- `vestige keys create|list|revoke` CLI subcommand (plus `keys rotate` as a - convenience alias of `revoke` + `create`). -- Startup-time refusal to bind non-loopback with `auth.enabled = false` (hard - error, non-zero exit, stderr message, no fallback). -- Dashboard login flow: `POST /dashboard/login` with `{"api_key":"vst_..."}` - JSON body, `X-API-Key` header, or form body; sets signed cookie; returns 200 - JSON `{"ok":true}` for XHR or 303 to `/` if form. Logout at - `POST /dashboard/logout` clears cookie. -- Per-key `domain_filter` enforced inside the auth layer: if the key has - `domain_filter = ["dev","infra"]`, every handler that searches or lists sees - the filter pre-applied via a request extension. Optional - `X-Vestige-Domain: home` header may narrow further but may never escape the - key's filter. -- `[server]` and `[auth]` sections in `vestige.toml`, plus backward-compatible - env var bridges. -- `VESTIGE_AUTH_TOKEN` continues to work for one minor release as a synthetic - single-key fallback, but logs a deprecation warning. -- Per-request request IDs and structured tracing; `last_used` write-back on - successful auth. - -### Out of scope - -- Phase 4 HDBSCAN domain classifier itself. The REST surface exposes domain - endpoints but they may stub to empty results until Phase 4 lands. -- Real TLS termination. Assumed handled by a reverse proxy (nginx, Caddy, - Mycelium). An optional `tls_cert` / `tls_key` pair is documented but its - implementation may be deferred behind a `tls` Cargo feature. -- OAuth / OIDC / SSO. Future work. -- Rate limiting per key (documented in Open Questions, not implemented here). -- WebAuthn / passkey dashboard login. Future work. -- Fine-grained RBAC beyond `read` / `write` scopes. - -## Prerequisites - -Phase 1 artifacts: - -- `vestige_core::storage::MemoryStore` trait (with `Send` variant via - `trait_variant::make`). -- `Embedder` trait. -- `SqliteMemoryStore` implementing `MemoryStore`. - -Phase 2 artifacts: - -- `PgMemoryStore` implementing `MemoryStore`. -- `crates/vestige-core/migrations/postgres/` sqlx migrations; `api_keys` table - schema present but enforcement path is Phase 3's job. -- Runtime backend selection via `vestige.toml` `[storage]` section returning - an `Arc`. - -Assumed already available in workspace: - -- `axum = 0.8` (currently pinned in `crates/vestige-mcp/Cargo.toml`). -- `tower = 0.5`, `tower-http = 0.6` (`cors`, `set-header` features already on). -- `tokio`, `serde`, `serde_json`, `uuid`, `chrono`, `tracing`, - `tracing-subscriber`, `thiserror`, `anyhow`, `subtle`, `clap`, `directories`. - -New crates required (add via `cargo add -p vestige-mcp`): - -- `blake3 = "1"` -- key hashing. -- `rand = "0.9"` with `std_rng` (for key bytes; prefer `rand::rngs::OsRng`). -- `axum-extra = { version = "0.10", features = ["cookie-signed", "typed-header"] }` - -- `SignedCookieJar`, `Cookie`, `Key`. -- `hmac = "0.12"` + `sha2 = "0.10"` -- HMAC-SHA256 for the session secret - derivation (not required if `axum-extra`'s `SignedCookieJar` is used, but - retained for the pure-token-signing path). RECOMMENDATION: rely solely on - `axum-extra::extract::cookie::{Key, SignedCookieJar}`. -- `tower-http` features bump: add `trace` and `request-id`. -- `async-stream = "0.3"` -- emitting SSE events from async closures. -- `futures-util` already present -- for `Stream` adapters. -- `base64 = "0.22"` -- emitting / parsing the random bytes in the `vst_...` - prefix. Use the `URL_SAFE_NO_PAD` alphabet. -- `zeroize = "1"` (optional, recommended) -- scrub the plaintext key in RAM - after hashing. - -`cargo add` commands (do not execute here, leave to implementation): - - cargo add -p vestige-mcp blake3 rand base64 zeroize async-stream - cargo add -p vestige-mcp axum-extra --features cookie-signed,typed-header - cargo add -p vestige-mcp tower-http --features trace,request-id,cors,set-header - -JSON-RPC library: the project uses a hand-rolled `JsonRpcRequest` / -`JsonRpcResponse` pair in `crates/vestige-mcp/src/protocol/types.rs`. Keep it -in Phase 3 (no jsonrpsee migration). Streamable HTTP remains implemented as -`POST /mcp` + session header + `GET /mcp` SSE. See Open Questions for rationale. - -## Deliverables - -1. `crates/vestige-mcp/src/auth/` module (new). Houses key generation, key - verification, identity resolution, scopes, domain-filter extractor, session - key type, and error types. - -2. `crates/vestige-mcp/src/auth/keys.rs` -- key format, generation, - blake3 hashing, store-facing trait methods for list / create / revoke / - verify. - -3. `crates/vestige-mcp/src/auth/middleware.rs` -- axum `from_fn` middleware - that populates `Extension` on the request, rejects unauthenticated - requests with 401, insufficient scope with 403. - -4. `crates/vestige-mcp/src/auth/session.rs` -- `SignedCookieJar` integration, - `session_key()` loader (env or persisted file), `issue_session()` and - `revoke_session()` helpers. - -5. `crates/vestige-mcp/src/http/` module split out of `protocol/http.rs`: - - `http/mcp.rs` -- MCP JSON-RPC endpoint (adapted from the current - `post_mcp` / `delete_mcp`, with auth middleware now gating). - - `http/mcp_sse.rs` -- SSE handler for `GET /mcp` long-running ops. - - `http/rest.rs` -- `/api/v1/*` handlers. - - `http/mod.rs` -- `build_router()`, `start_server()`, bind-safety check, - layer stack assembly. - -6. `crates/vestige-mcp/src/http/errors.rs` -- uniform `ApiError` enum and - `IntoResponse` implementation. Maps to RFC 7807 problem+json for REST and - plain JSON for `/mcp`. - -7. Dashboard patch: `crates/vestige-mcp/src/dashboard/mod.rs` -- add the auth - middleware to the dashboard router, add `/dashboard/login` + `/dashboard/logout` - endpoints, keep `/api/health` unauthenticated. - -8. `crates/vestige-mcp/src/bin/cli.rs` -- new `Keys` subcommand group (`create`, - `list`, `revoke`, `rotate`). - -9. `crates/vestige-mcp/src/config.rs` (new file) -- typed `ServerConfig`, - `AuthConfig`, `StorageConfig` loader from `vestige.toml`, merging env var - overrides, validating the non-loopback + auth-disabled combination. - -10. SQL migration `crates/vestige-core/migrations/postgres/0300_api_keys_enforcement.sql` - and SQLite equivalent `crates/vestige-core/migrations/sqlite/0300_api_keys.sql`: - - `api_keys` table (if not already created in Phase 2), with `key_hash` - UNIQUE, `label` NOT NULL, `scopes` TEXT[] default `{read,write}`, - `domain_filter` TEXT[] default `{}`, `created_at`, `last_used`, - `active BOOLEAN DEFAULT true`. - - Index on `key_hash` (unique already), and on `active WHERE active`. - -11. `MemoryStore` trait extension (Phase 2 may already cover this; if not, - finalize in Phase 3): `list_api_keys`, `create_api_key`, - `revoke_api_key`, `find_api_key_by_hash`, `touch_api_key_last_used`. - -12. Docs updates: - - `docs/env-vars.md` (new) -- one sheet for all runtime env vars. - - `README.md` server-mode section. - - `docs/adr/0001-*.md` -- mark Phase 3 as Implemented when merged. - -## Detailed Task Breakdown - -### D1. Auth module skeleton - -Files: - -- `crates/vestige-mcp/src/auth/mod.rs` -- `crates/vestige-mcp/src/auth/keys.rs` -- `crates/vestige-mcp/src/auth/session.rs` -- `crates/vestige-mcp/src/auth/middleware.rs` -- `crates/vestige-mcp/src/auth/errors.rs` - -`auth/mod.rs`: - - pub mod errors; - pub mod keys; - pub mod middleware; - pub mod session; - - pub use errors::AuthError; - pub use keys::{ApiKey, ApiKeyPlaintext, ApiKeyRecord, Scope}; - pub use middleware::{Identity, auth_layer}; - pub use session::{SessionConfig, session_key}; - -`auth/errors.rs`: - - use axum::http::StatusCode; - use axum::response::{IntoResponse, Response}; - use serde::Serialize; - use thiserror::Error; - - #[derive(Debug, Error)] - pub enum AuthError { - #[error("missing credentials")] - MissingCredentials, - #[error("invalid credentials")] - InvalidCredentials, - #[error("key revoked")] - Revoked, - #[error("insufficient scope: required {required}")] - InsufficientScope { required: &'static str }, - #[error("domain not permitted for this key: {domain}")] - DomainNotAllowed { domain: String }, - #[error("internal auth error")] - Internal, - } - - #[derive(Serialize)] - struct Problem<'a> { - #[serde(rename = "type")] - kind: &'a str, - title: &'a str, - status: u16, - detail: &'a str, - } - - impl IntoResponse for AuthError { - fn into_response(self) -> Response { - let (status, title) = match self { - AuthError::MissingCredentials => (StatusCode::UNAUTHORIZED, "unauthorized"), - AuthError::InvalidCredentials => (StatusCode::UNAUTHORIZED, "unauthorized"), - AuthError::Revoked => (StatusCode::UNAUTHORIZED, "unauthorized"), - AuthError::InsufficientScope { .. } => (StatusCode::FORBIDDEN, "forbidden"), - AuthError::DomainNotAllowed { .. } => (StatusCode::FORBIDDEN, "forbidden"), - AuthError::Internal => (StatusCode::INTERNAL_SERVER_ERROR, "internal"), - }; - let detail = self.to_string(); - let body = axum::Json(Problem { - kind: "about:blank", - title, - status: status.as_u16(), - detail: &detail, - }); - let mut r = (status, body).into_response(); - r.headers_mut().insert( - axum::http::header::CONTENT_TYPE, - axum::http::HeaderValue::from_static("application/problem+json"), - ); - r - } - } - -### D2. Key format and generation - -File: `crates/vestige-mcp/src/auth/keys.rs` - -- Key on wire: `vst_<22-byte base64url-no-pad>`. 22 bytes = 176 bits entropy. - Encoded length ~30 chars. Full string ~34 chars including the `vst_` prefix. -- Hash stored in DB: `blake3(key_plaintext)` hex lowercase (32 bytes -> 64 - hex chars). -- Hash prefix on list: first 12 hex characters, e.g. `key_hash[..12]` for - human display. - -Signatures: - - use blake3::Hasher; - use rand::rngs::OsRng; - use rand::TryRngCore; - use base64::engine::general_purpose::URL_SAFE_NO_PAD; - use base64::Engine; - use zeroize::Zeroize; - - const KEY_PREFIX: &str = "vst_"; - const KEY_RANDOM_BYTES: usize = 22; - - #[derive(Clone, Debug, PartialEq, Eq)] - pub enum Scope { - Read, - Write, - } - - impl Scope { - pub fn as_str(&self) -> &'static str { - match self { - Scope::Read => "read", - Scope::Write => "write", - } - } - pub fn from_str(s: &str) -> Option { - match s { - "read" => Some(Scope::Read), - "write" => Some(Scope::Write), - _ => None, - } - } - } - - /// The plaintext key. Shown to the user exactly once. - /// Zeroed on drop. - pub struct ApiKeyPlaintext(String); - - impl ApiKeyPlaintext { - pub fn as_str(&self) -> &str { &self.0 } - pub fn into_inner(mut self) -> String { - std::mem::take(&mut self.0) - } - } - - impl Drop for ApiKeyPlaintext { - fn drop(&mut self) { self.0.zeroize(); } - } - - #[derive(Clone, Debug)] - pub struct ApiKeyRecord { - pub id: uuid::Uuid, - pub key_hash: String, // hex-encoded blake3(plaintext) - pub label: String, - pub scopes: Vec, - pub domain_filter: Vec, - pub created_at: chrono::DateTime, - pub last_used: Option>, - pub active: bool, - } - - pub fn generate_key() -> ApiKeyPlaintext { - let mut bytes = [0u8; KEY_RANDOM_BYTES]; - OsRng.try_fill_bytes(&mut bytes).expect("OsRng"); - let encoded = URL_SAFE_NO_PAD.encode(&bytes); - bytes.zeroize(); - ApiKeyPlaintext(format!("{}{}", KEY_PREFIX, encoded)) - } - - pub fn hash_key(plaintext: &str) -> String { - let mut hasher = Hasher::new(); - hasher.update(plaintext.as_bytes()); - hasher.finalize().to_hex().to_string() - } - - pub fn verify_key(plaintext: &str, stored_hash_hex: &str) -> bool { - use subtle::ConstantTimeEq; - let computed = hash_key(plaintext); - computed.as_bytes().ct_eq(stored_hash_hex.as_bytes()).unwrap_u8() == 1 - } - -Helpers on a thin repository trait that both backends implement through -`MemoryStore` (Phase 2 already adds the required columns; Phase 3 wires the -methods): - - #[async_trait::async_trait] - pub trait ApiKeyStore: Send + Sync + 'static { - async fn create_api_key(&self, rec: &ApiKeyRecord) -> anyhow::Result<()>; - async fn find_api_key_by_hash(&self, hash: &str) -> anyhow::Result>; - async fn list_api_keys(&self) -> anyhow::Result>; - async fn revoke_api_key(&self, id: uuid::Uuid) -> anyhow::Result; - async fn touch_api_key_last_used(&self, id: uuid::Uuid) -> anyhow::Result<()>; - } - -(If Phase 2 already bolted these onto `MemoryStore`, `ApiKeyStore` is simply a -re-export of the relevant subset.) - -### D3. Session cookie - -File: `crates/vestige-mcp/src/auth/session.rs` - -- Cookie name: `vestige_session`. -- Cookie attributes: `HttpOnly`, `SameSite=Strict`, `Path=/`, `Max-Age=28800` - (8h), `Secure` when the server is running behind TLS (detected from - `config.server.tls_cert.is_some()` or the `X-Forwarded-Proto` trusted header; - default: set `Secure` whenever `config.server.bind` is non-loopback). -- Payload: serialized `SessionClaims { key_id: Uuid, issued_at: i64, - expires_at: i64 }` encoded as `serde_json` then base64url. The signing is - handled by `axum-extra::extract::cookie::SignedCookieJar` (HMAC via a 64-byte - `Key`). Any tampering or truncation is rejected by the jar automatically. -- Key material: 64 random bytes, stored at `/session_secret` (mode - 0600) or overridden by `VESTIGE_SESSION_SECRET` (base64url-encoded 64 bytes, - reject if shorter). - -Signatures: - - use axum_extra::extract::cookie::{Cookie, Key, SameSite, SignedCookieJar}; - use chrono::{Duration, Utc}; - use serde::{Deserialize, Serialize}; - - const COOKIE_NAME: &str = "vestige_session"; - const DEFAULT_TTL: Duration = Duration::hours(8); - - #[derive(Clone, Serialize, Deserialize)] - pub struct SessionClaims { - pub key_id: uuid::Uuid, - pub iat: i64, - pub exp: i64, - } - - pub fn session_key(data_dir: &std::path::Path) -> anyhow::Result { - // 1) env override - if let Ok(env_val) = std::env::var("VESTIGE_SESSION_SECRET") { - let raw = base64::engine::general_purpose::URL_SAFE_NO_PAD - .decode(env_val.trim())?; - anyhow::ensure!(raw.len() >= 64, "VESTIGE_SESSION_SECRET must be >= 64 bytes"); - return Ok(Key::from(&raw)); - } - // 2) persisted file - let path = data_dir.join("session_secret"); - if path.exists() { - let bytes = std::fs::read(&path)?; - return Ok(Key::from(&bytes)); - } - // 3) generate - use rand::TryRngCore; - let mut bytes = [0u8; 64]; - rand::rngs::OsRng.try_fill_bytes(&mut bytes)?; - #[cfg(unix)] - { - use std::io::Write; - use std::os::unix::fs::OpenOptionsExt; - std::fs::create_dir_all(data_dir).ok(); - let mut f = std::fs::OpenOptions::new() - .create_new(true).write(true).mode(0o600).open(&path)?; - f.write_all(&bytes)?; - f.sync_all()?; - } - #[cfg(not(unix))] - std::fs::write(&path, &bytes)?; - Ok(Key::from(&bytes)) - } - - pub fn issue_session( - jar: SignedCookieJar, - key_id: uuid::Uuid, - secure: bool, - ) -> SignedCookieJar { - let now = Utc::now(); - let claims = SessionClaims { - key_id, - iat: now.timestamp(), - exp: (now + DEFAULT_TTL).timestamp(), - }; - let value = serde_json::to_string(&claims).expect("serialize claims"); - let mut cookie = Cookie::new(COOKIE_NAME, value); - cookie.set_http_only(true); - cookie.set_same_site(SameSite::Strict); - cookie.set_path("/"); - cookie.set_max_age(cookie::time::Duration::seconds(DEFAULT_TTL.num_seconds())); - cookie.set_secure(secure); - jar.add(cookie) - } - - pub fn revoke_session(jar: SignedCookieJar) -> SignedCookieJar { - jar.remove(Cookie::from(COOKIE_NAME)) - } - - pub fn claims_from(jar: &SignedCookieJar) -> Option { - let c = jar.get(COOKIE_NAME)?; - let claims: SessionClaims = serde_json::from_str(c.value()).ok()?; - if claims.exp < Utc::now().timestamp() { return None; } - Some(claims) - } - -### D4. Auth middleware - -File: `crates/vestige-mcp/src/auth/middleware.rs` - -Identity carried through the request: - - #[derive(Clone, Debug)] - pub struct Identity { - pub key_id: uuid::Uuid, - pub label: String, - pub scopes: Vec, - pub domain_filter: Vec, - pub via: AuthVia, - } - - #[derive(Clone, Copy, Debug)] - pub enum AuthVia { - Bearer, - ApiKeyHeader, - SessionCookie, - } - -Middleware (axum 0.8): - - use axum::extract::{Request, State}; - use axum::http::{header, StatusCode}; - use axum::middleware::Next; - use axum::response::{IntoResponse, Response}; - use axum_extra::extract::cookie::SignedCookieJar; - use std::sync::Arc; - - pub async fn auth_layer( - State(state): State>, - jar: SignedCookieJar, - mut request: Request, - next: Next, - ) -> Response { - // Allowlist endpoints that never require auth: - let path = request.uri().path(); - if path == "/api/health" || path == "/api/v1/health" || - path == "/dashboard/login" { - return next.run(request).await; - } - - let via_and_key = extract_credentials(request.headers(), &jar); - let outcome = match via_and_key { - Some((AuthVia::Bearer, key)) | Some((AuthVia::ApiKeyHeader, key)) => { - resolve_by_plaintext(&state, &key).await.map(|id| (id, via_and_key.unwrap().0)) - } - Some((AuthVia::SessionCookie, key_id_str)) => { - let id = uuid::Uuid::parse_str(&key_id_str).map_err(|_| AuthError::InvalidCredentials)?; - resolve_by_key_id(&state, id).await.map(|id| (id, AuthVia::SessionCookie)) - } - None => Err(AuthError::MissingCredentials), - }; - - let identity = match outcome { - Ok((id, via)) => Identity { via, ..id }, - Err(e) => return e.into_response(), - }; - - // touch last_used asynchronously; do not block request path - let st2 = state.clone(); - let kid = identity.key_id; - tokio::spawn(async move { let _ = st2.store.touch_api_key_last_used(kid).await; }); - - request.extensions_mut().insert(identity); - next.run(request).await - } - -Credential extraction (priority: Bearer > X-API-Key > cookie): - - fn extract_credentials( - headers: &axum::http::HeaderMap, - jar: &SignedCookieJar, - ) -> Option<(AuthVia, String)> { - if let Some(v) = headers.get(header::AUTHORIZATION).and_then(|h| h.to_str().ok()) { - if let Some(rest) = v.strip_prefix("Bearer ") { - return Some((AuthVia::Bearer, rest.trim().to_string())); - } - } - if let Some(v) = headers.get("x-api-key").and_then(|h| h.to_str().ok()) { - return Some((AuthVia::ApiKeyHeader, v.trim().to_string())); - } - if let Some(claims) = crate::auth::session::claims_from(jar) { - return Some((AuthVia::SessionCookie, claims.key_id.to_string())); - } - None - } - -Resolution helpers: - - async fn resolve_by_plaintext(st: &AppCtx, key: &str) -> Result { - let hash = crate::auth::keys::hash_key(key); - let rec = st.store.find_api_key_by_hash(&hash).await - .map_err(|_| AuthError::Internal)? - .ok_or(AuthError::InvalidCredentials)?; - if !rec.active { return Err(AuthError::Revoked); } - Ok(Identity { - key_id: rec.id, label: rec.label, scopes: rec.scopes, - domain_filter: rec.domain_filter, via: AuthVia::Bearer, - }) - } - - async fn resolve_by_key_id(st: &AppCtx, id: uuid::Uuid) -> Result { - let rec = st.store.find_api_key_by_id(id).await - .map_err(|_| AuthError::Internal)? - .ok_or(AuthError::InvalidCredentials)?; - if !rec.active { return Err(AuthError::Revoked); } - Ok(Identity { - key_id: rec.id, label: rec.label, scopes: rec.scopes, - domain_filter: rec.domain_filter, via: AuthVia::SessionCookie, - }) - } - -Scope guard extractor (per-handler opt-in): - - pub struct RequireScope; - impl axum::extract::FromRequestParts for RequireScope - where S: Send + Sync, - { - type Rejection = AuthError; - async fn from_request_parts( - parts: &mut axum::http::request::Parts, _state: &S, - ) -> Result { - let id = parts.extensions.get::().ok_or(AuthError::MissingCredentials)?; - let need = if WRITE { Scope::Write } else { Scope::Read }; - if !id.scopes.contains(&need) { - return Err(AuthError::InsufficientScope { - required: if WRITE { "write" } else { "read" }, - }); - } - Ok(RequireScope) - } - } - -Domain scoping: - - /// Returns the effective domain filter for the request: - /// - Intersect the key's domain_filter with any X-Vestige-Domain header. - /// - Empty key filter means "all domains", so the header is authoritative. - /// - A header that names a domain outside the key filter returns - /// `Err(DomainNotAllowed)`. - pub fn effective_domain_filter( - id: &Identity, header: Option<&str>, - ) -> Result>, AuthError> { - let header_dom = header.map(|s| s.trim().to_string()).filter(|s| !s.is_empty()); - match (id.domain_filter.as_slice(), header_dom) { - ([], None) => Ok(None), - ([], Some(h)) => Ok(Some(vec![h])), - (filter, None) => Ok(Some(filter.to_vec())), - (filter, Some(h)) => { - if filter.iter().any(|d| d == &h) { - Ok(Some(vec![h])) - } else { - Err(AuthError::DomainNotAllowed { domain: h }) - } - } - } - } - -### D5. Layer ordering - -Router assembly in `http/mod.rs::build_router`: - - let trace = tower_http::trace::TraceLayer::new_for_http(); - let request_id = tower_http::request_id::SetRequestIdLayer::x_request_id( - tower_http::request_id::MakeRequestUuid); - let propagate_id = tower_http::request_id::PropagateRequestIdLayer::x_request_id(); - - let cors = CorsLayer::new() - .allow_origin(cfg.server.allowed_origins()) - .allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE, Method::OPTIONS]) - .allow_headers([header::CONTENT_TYPE, header::AUTHORIZATION, - HeaderName::from_static("x-api-key"), - HeaderName::from_static("x-vestige-domain"), - HeaderName::from_static("mcp-session-id")]) - .allow_credentials(true); - - let app = Router::new() - // Unauth routes first (not subjected to auth_layer by path allowlist) - .route("/api/health", get(health)) - .route("/dashboard/login", post(login)) - .route("/dashboard/logout", post(logout)) - // MCP + REST + dashboard - .route("/mcp", post(http::mcp::post_mcp).get(http::mcp_sse::get_mcp_sse) - .delete(http::mcp::delete_mcp)) - .nest("/api/v1", http::rest::router()) - .merge(dashboard::router()) - // Auth middleware applied via from_fn_with_state (allowlist inside) - .layer(axum::middleware::from_fn_with_state(ctx.clone(), auth_layer)) - // Outermost: tracing, request-id, cors, body limit, concurrency - .layer( - ServiceBuilder::new() - .layer(trace) - .layer(request_id) - .layer(propagate_id) - .layer(cors) - .layer(DefaultBodyLimit::max(MAX_BODY_SIZE)) - .layer(ConcurrencyLimitLayer::new(CONCURRENCY_LIMIT)) - ) - .with_state(ctx); - -Axum applies `layer()` calls outermost-first in the order they are declared. -The result here: request -> trace -> request-id -> CORS -> body-limit -> -concurrency -> auth -> handler. Auth must wrap the handlers but be inside -tracing so its spans can log auth outcomes. - -### D6. MCP endpoints - -File: `crates/vestige-mcp/src/http/mcp.rs` - -`POST /mcp` -- keep the session-based structure already in `protocol/http.rs` -but use the `Identity` injected by the auth layer instead of a shared -`auth_token`: - - pub async fn post_mcp( - State(ctx): State>, - Extension(id): Extension, - headers: HeaderMap, - Json(request): Json, - ) -> Response { ... } - -Auth happens in the layer, so this handler cannot be reached without a valid -`Identity`. Scope check: all MCP writes (tools that mutate) require -`RequireScope`. Use an enum of MCP methods or a method -> required-scope -map. `tools/list`, `resources/list`, `initialize`, `ping` are read-only. -`tools/call` is conservatively classified as write; the per-tool dispatch -inside `McpServer::handle_tools_call` may further reject writes when the tool -name is read-only and the key lacks write. - -`DELETE /mcp` -- unchanged semantics, drops the session. - -`GET /mcp` -- SSE. Implementation in `http/mcp_sse.rs`: - - use axum::response::sse::{Event, KeepAlive, Sse}; - use axum::extract::Query; - use futures_util::stream::Stream; - use async_stream::stream; - use std::time::Duration; - - #[derive(serde::Deserialize)] - pub struct SseParams { - pub op: String, // "dream" | "consolidate" | "discover" | "reassign" - pub session: Option, // optional operation correlation id - } - - pub async fn get_mcp_sse( - State(ctx): State>, - Extension(_id): Extension, - Query(params): Query, - ) -> Result>>, AuthError> { - let op = params.op.clone(); - let ctx2 = ctx.clone(); - let s = stream! { - yield Ok(Event::default().event("start").data(format!("{{\"op\":\"{}\"}}", op))); - match op.as_str() { - "dream" => { - let mut rx = ctx2.cognitive.lock().await.begin_dream_stream().await; - while let Some(ev) = rx.recv().await { - yield Ok(Event::default().event("progress").json_data(ev)?); - } - yield Ok(Event::default().event("done").data("{}")); - } - "consolidate" => { /* same pattern over Storage::run_consolidation_stream */ } - "discover" => { /* Phase 4 */ } - "reassign" => { /* Phase 4 */ } - other => { - yield Ok(Event::default().event("error") - .data(format!("{{\"message\":\"unknown op {}\"}}", other))); - } - } - }; - Ok(Sse::new(s).keep_alive(KeepAlive::new().interval(Duration::from_secs(15)))) - } - -SSE event shape (stable contract, document in `docs/http-api.md`): - - event: start - data: {"op":"dream"} - - event: progress - data: {"stage":"replay","processed":12,"total":50} - - event: progress - data: {"stage":"cross_reference","processed":25,"total":50} - - event: done - data: {"nodes_processed":50,"duration_ms":14320} - -The `keep-alive` hint is 15s to survive most proxy timeouts. - -### D7. REST API - -File: `crates/vestige-mcp/src/http/rest.rs` - -Routes: - - pub fn router() -> Router> { - Router::new() - .route("/health", get(health)) - .route("/memories", post(create_memory).get(list_memories)) - .route("/memories/{id}", get(get_memory).put(update_memory).delete(delete_memory)) - .route("/memories/{id}/promote", post(promote_memory)) - .route("/memories/{id}/demote", post(demote_memory)) - .route("/search", post(search_memories)) - .route("/consolidate", post(trigger_consolidation)) - .route("/stats", get(get_stats)) - .route("/domains", get(list_domains)) - .route("/domains/discover", post(trigger_discovery)) - .route("/domains/{id}", put(rename_domain).delete(delete_domain)) - .route("/domains/{id}/merge", post(merge_domain)) - .route("/keys", post(create_key).get(list_keys)) - .route("/keys/{id}", delete(revoke_key)) - } - -Representative signatures: - - #[derive(serde::Deserialize)] - pub struct CreateMemoryReq { - pub content: String, - pub node_type: Option, - pub tags: Option>, - pub source: Option, - pub metadata: Option, - } - - #[derive(serde::Serialize)] - pub struct MemoryView { /* flat projection of MemoryRecord */ } - - pub async fn create_memory( - State(ctx): State>, - Extension(id): Extension, - _: RequireScope, - Json(req): Json, - ) -> Result<(StatusCode, Json), ApiError> { - let effective = effective_domain_filter(&id, None)?; - let rec = ctx.store.insert_from_rest(req, effective).await?; - Ok((StatusCode::CREATED, Json(MemoryView::from(rec)))) - } - - pub async fn search_memories( - State(ctx): State>, - Extension(id): Extension, - _: RequireScope, - headers: HeaderMap, - Json(req): Json, - ) -> Result, ApiError> { - let dom_header = headers.get("x-vestige-domain").and_then(|h| h.to_str().ok()); - let effective = effective_domain_filter(&id, dom_header)?; - let q = SearchQuery { domains: effective, ..req.into() }; - let res = ctx.store.search(&q).await?; - Ok(Json(SearchResp::from(res))) - } - -`trigger_consolidation` returns 202 Accepted + a JSON body with a `session_id` -the client may pass to `GET /mcp?op=consolidate&session=...` to stream -progress. - -### D8. Error mapping - -File: `crates/vestige-mcp/src/http/errors.rs` - - #[derive(Debug, thiserror::Error)] - pub enum ApiError { - #[error(transparent)] Auth(#[from] AuthError), - #[error("bad request: {0}")] BadRequest(String), - #[error("not found")] NotFound, - #[error("conflict: {0}")] Conflict(String), - #[error(transparent)] Store(#[from] anyhow::Error), - } - - impl IntoResponse for ApiError { - fn into_response(self) -> Response { - match self { - ApiError::Auth(a) => a.into_response(), - ApiError::BadRequest(m) => (StatusCode::BAD_REQUEST, problem(400, "bad_request", &m)).into_response(), - ApiError::NotFound => (StatusCode::NOT_FOUND, problem(404, "not_found", "")).into_response(), - ApiError::Conflict(m) => (StatusCode::CONFLICT, problem(409, "conflict", &m)).into_response(), - ApiError::Store(e) => { - tracing::error!(err = %e, "store error"); - (StatusCode::INTERNAL_SERVER_ERROR, problem(500, "internal", "internal error")).into_response() - } - } - } - } - -All MCP JSON-RPC error mapping is unchanged (done in `McpServer`); only -transport-level errors (401/403) leave that path. - -### D9. Config loader and bind-safety check - -File: `crates/vestige-mcp/src/config.rs` - - #[derive(Debug, Clone, serde::Deserialize)] - pub struct ServerConfig { - #[serde(default = "default_bind")] - pub bind: String, // "127.0.0.1:3928" - #[serde(default = "default_dashboard_port")] - pub dashboard_port: u16, - #[serde(default)] pub tls_cert: Option, - #[serde(default)] pub tls_key: Option, - #[serde(default)] pub allowed_origins: Vec, - } - - #[derive(Debug, Clone, serde::Deserialize)] - pub struct AuthConfig { - #[serde(default = "default_true")] - pub enabled: bool, - #[serde(default)] pub session_secret_file: Option, - } - - impl ServerConfig { - pub fn parsed_bind(&self) -> anyhow::Result { - self.bind.parse().map_err(|e: std::net::AddrParseError| - anyhow::anyhow!("invalid bind {}: {}", self.bind, e)) - } - } - -Bind-safety check (called during `start_server`): - - pub fn enforce_bind_safety(server: &ServerConfig, auth: &AuthConfig) -> anyhow::Result<()> { - let addr = server.parsed_bind()?; - let is_loopback = match addr.ip() { - std::net::IpAddr::V4(v) => v.is_loopback(), - std::net::IpAddr::V6(v) => v.is_loopback(), - }; - if !is_loopback && !auth.enabled { - anyhow::bail!( - "refusing to bind {} with auth disabled; \ - set [auth] enabled = true in vestige.toml or \ - change [server] bind to a loopback address", - addr - ); - } - Ok(()) - } - -`main.rs` and the `serve` CLI both call `enforce_bind_safety` before -`TcpListener::bind`. On failure: `eprintln!` the error, `std::process::exit(2)`. - -Env bridge: - -- `VESTIGE_HTTP_BIND` (existing) -> `server.bind` host part. -- `VESTIGE_HTTP_PORT` (existing) -> `server.bind` port part. -- `VESTIGE_DASHBOARD_PORT` (existing) -> `server.dashboard_port`. -- `VESTIGE_AUTH_TOKEN` (deprecated) -- when set, synthesize a virtual - `ApiKeyRecord` with `id = all-zero UUID`, `scopes = [read, write]`, - `domain_filter = []`, `active = true`, hash stored in memory only. Log a - warning on every startup: `VESTIGE_AUTH_TOKEN is deprecated; use 'vestige - keys create' and set auth.enabled=true instead. Will be removed in v2.2.0.` -- `VESTIGE_SESSION_SECRET` -- see D3. - -### D10. Dashboard login + logout - -File: `crates/vestige-mcp/src/dashboard/handlers.rs` (additions). - - #[derive(serde::Deserialize)] - pub struct LoginBody { - pub api_key: String, - } - - pub async fn login( - State(state): State, - jar: SignedCookieJar, - headers: HeaderMap, - body: Option>, - ) -> Result<(SignedCookieJar, Json), AuthError> { - // Accept key in either JSON body or X-API-Key header - let plaintext = body.map(|b| b.0.api_key) - .or_else(|| headers.get("x-api-key").and_then(|h| h.to_str().ok()).map(String::from)) - .ok_or(AuthError::MissingCredentials)?; - - let hash = crate::auth::keys::hash_key(&plaintext); - let rec = state.store.find_api_key_by_hash(&hash).await - .map_err(|_| AuthError::Internal)? - .ok_or(AuthError::InvalidCredentials)?; - if !rec.active { return Err(AuthError::Revoked); } - - let secure = state.config.server.tls_cert.is_some(); - let jar = crate::auth::session::issue_session(jar, rec.id, secure); - - Ok((jar, Json(serde_json::json!({ - "ok": true, "key_id": rec.id, "label": rec.label, - "scopes": rec.scopes.iter().map(|s| s.as_str()).collect::>(), - "domains": rec.domain_filter, - })))) - } - - pub async fn logout(jar: SignedCookieJar) - -> (SignedCookieJar, Json) - { - (crate::auth::session::revoke_session(jar), - Json(serde_json::json!({"ok": true}))) - } - -Dashboard router integration: login/logout are appended before `auth_layer` -is applied, so they are reachable unauthenticated. The dashboard SPA asset -routes (`/dashboard`, `/dashboard/{*path}`) remain publicly readable so the -login page can load; the `/api/*` dashboard endpoints are gated by -`auth_layer`. (The existing health endpoint keeps its current behaviour.) - -### D11. `vestige keys` CLI - -File: `crates/vestige-mcp/src/bin/cli.rs` additions. - - #[derive(Subcommand)] - enum Commands { - // ... existing - /// Manage API keys - Keys { - #[command(subcommand)] - sub: KeyCmd, - }, - } - - #[derive(Subcommand)] - enum KeyCmd { - /// Create a new API key - Create { - #[arg(long)] label: String, - #[arg(long, value_delimiter = ',', default_values_t = ["read".to_string(), "write".to_string()])] - scopes: Vec, - /// Restrict the key to listed domains (comma-separated). Empty = all domains. - #[arg(long, value_delimiter = ',')] - domains: Vec, - }, - /// List existing keys (never shows plaintext) - List { - /// Include revoked keys in the output - #[arg(long)] all: bool, - }, - /// Revoke a key by id or by hash prefix - Revoke { - /// Id (UUID) or hash prefix (first 12 hex chars) - id_or_prefix: String, - }, - /// Revoke and re-create with the same scopes/label - Rotate { - id_or_prefix: String, - }, - } - -`Create` outputs the plaintext exactly once on stdout (for piping into env -files) and a confirmation on stderr. Use colored output only on stderr to keep -stdout machine-readable. - - fn run_keys_create(...) -> anyhow::Result<()> { - let store = open_store()?; // Arc - let plaintext = crate::auth::keys::generate_key(); - let hash = crate::auth::keys::hash_key(plaintext.as_str()); - let rec = ApiKeyRecord { - id: uuid::Uuid::new_v4(), - key_hash: hash, label, scopes, domain_filter: domains, - created_at: chrono::Utc::now(), - last_used: None, active: true, - }; - block_on(store.create_api_key(&rec))?; - - // stderr: human-readable - eprintln!("{} {}", "Created key:".green().bold(), rec.label); - eprintln!(" id: {}", rec.id); - eprintln!(" scopes: {}", rec.scopes.iter().map(|s| s.as_str()).collect::>().join(",")); - eprintln!(" domains: {}", if rec.domain_filter.is_empty() { "all".to_string() } else { rec.domain_filter.join(",") }); - eprintln!(); - eprintln!("{}", "Store the plaintext key now. It will not be shown again.".yellow()); - // stdout: ONLY the plaintext, for scripting - println!("{}", plaintext.as_str()); - Ok(()) - } - -`List`: - - kid label scopes domains last_used hash - d3a8... macbook read,write all 2026-04-20 11:02 a1b2c3d4e5f6 - ... - -Never print the plaintext. Show only `hash[..12]`. - -### D12. Migrations - -Postgres `0300_api_keys.sql` (idempotent; Phase 2 may have already created the -table, in which case this migration is a no-op `CREATE TABLE IF NOT EXISTS`): - - CREATE TABLE IF NOT EXISTS api_keys ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - key_hash TEXT NOT NULL UNIQUE, - label TEXT NOT NULL, - scopes TEXT[] NOT NULL DEFAULT ARRAY['read','write'], - domain_filter TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - last_used TIMESTAMPTZ, - active BOOLEAN NOT NULL DEFAULT true - ); - - CREATE INDEX IF NOT EXISTS idx_api_keys_active - ON api_keys (active) WHERE active; - -SQLite `0300_api_keys.sql`: - - CREATE TABLE IF NOT EXISTS api_keys ( - id TEXT PRIMARY KEY, - key_hash TEXT NOT NULL UNIQUE, - label TEXT NOT NULL, - scopes TEXT NOT NULL DEFAULT 'read,write', -- comma-joined - domain_filter TEXT NOT NULL DEFAULT '', -- comma-joined, '' = all - created_at TEXT NOT NULL DEFAULT (datetime('now')), - last_used TEXT, - active INTEGER NOT NULL DEFAULT 1 - ); - - CREATE INDEX IF NOT EXISTS idx_api_keys_active - ON api_keys (active) WHERE active = 1; - -Both backends' trait impls convert to/from `ApiKeyRecord`. - -### D13. Wiring main.rs and the `serve` CLI path - -`main.rs` refactor: - -1. `Config::load()` reads `vestige.toml` (if present) and overlays env vars. -2. Run `enforce_bind_safety(&cfg.server, &cfg.auth)` before spawning any - listener. On failure, print to stderr and exit 2. -3. Build `AppCtx` with `Arc`, `CognitiveEngine`, - event bus, `session_key`, `config`. -4. `build_router(ctx)` returns a single Axum `Router` that covers MCP, REST, - and dashboard. -5. `axum::serve(listener, app).await`. -6. The stdio MCP transport continues to run in parallel (unchanged) for - desktop / Claude Code single-user scenarios. - -`serve` CLI subcommand: identical flow minus stdio. - -### D14. Docs - -- `docs/env-vars.md` new: table of every supported env var, default, purpose, - deprecation status. -- Section in `README.md`: "Running Vestige as a network server". -- Cheat-sheet section in `CLAUDE.md` for: create a key, start the server, - curl smoke test. - -## Test Plan - -### Unit tests (colocated under `#[cfg(test)]`) - -- `auth/keys.rs`: - - `generate_key_has_prefix_and_length()` -- asserts `vst_` prefix and 34-ish - char total, regex `^vst_[A-Za-z0-9_-]{29}$`. - - `hash_key_blake3_is_stable_and_hex()` -- fixed vector test. - - `verify_key_accepts_same_input()` / `verify_key_rejects_tampered()` / - `verify_key_rejects_length_mismatch()`. - - `keys_are_unique_in_a_loop()` -- 10_000 iterations, no collisions. - - `plaintext_zeroed_on_drop()` -- unsafe peek into the backing buffer - through a wrapper that exposes bytes for the test only. - -- `auth/session.rs`: - - `round_trip_claims_through_signed_jar()`. - - `expired_cookie_is_rejected()` -- mint a cookie with `exp = iat - 60` and - confirm `claims_from` returns `None`. - - `tampered_cookie_is_rejected()` -- flip one byte in the signed segment, - confirm the jar drops it. - - `session_key_env_overrides_file()`. - - `session_key_generated_file_has_mode_0600_on_unix()`. - -- `auth/middleware.rs`: - - `extract_credentials_prefers_bearer_over_api_key_header()`. - - `extract_credentials_falls_back_to_cookie()`. - - `effective_domain_filter_empty_means_all()`. - - `effective_domain_filter_header_narrows_within_key_filter()`. - - `effective_domain_filter_rejects_header_outside_key_filter()`. - - `missing_credentials_returns_401()`. - - `revoked_key_returns_401()`. - - `insufficient_scope_returns_403()`. - -- `config.rs`: - - `parse_vestige_toml_with_server_and_auth_sections()`. - - `env_vars_override_toml_bind()`. - - `enforce_bind_safety_rejects_0_0_0_0_with_auth_disabled()`. - - `enforce_bind_safety_allows_0_0_0_0_with_auth_enabled()`. - - `enforce_bind_safety_allows_loopback_with_auth_disabled()`. - -- `http/errors.rs`: - - `not_found_emits_problem_json_with_correct_content_type()`. - - `bad_request_includes_detail_field()`. - -- `http/mcp.rs`: - - `post_mcp_unauth_returns_401()` (this would normally be caught by the - middleware; kept as a unit test by constructing the Router minus the - middleware to exercise the handler's own error paths). - -### Integration tests (`tests/phase_3/`) - -All tests spin up the full Axum stack in-process on a random port via -`tokio::net::TcpListener::bind("127.0.0.1:0")`, wire a `SqliteMemoryStore` in -a `TempDir`, and issue HTTP calls with `reqwest`. - -Files (each one a standalone binary test file): - -- `phase_3/common/mod.rs` -- shared harness (`spawn_server()`, - `create_test_key()`, `client()`). - -- `phase_3/http_mcp_round_trip.rs` -- boot server, mint a key, send - `initialize` over `POST /mcp` with `Authorization: Bearer vst_...`, follow - with `tools/list`, assert we see the expected tool count (greater than 20). - -- `phase_3/http_sse_stream.rs` -- `POST /api/v1/consolidate` returns 202 + - `session_id`. `GET /mcp?op=consolidate&session=...` streams at least one - `progress` and one `done` event. Use `eventsource-client` dev dep, or parse - the stream manually. - -- `phase_3/rest_api_crud.rs` -- exercises each REST endpoint in turn: - - `POST /api/v1/memories` -> 201 + body. - - `GET /api/v1/memories/{id}` -> 200. - - `PUT /api/v1/memories/{id}` -> 200. - - `POST /api/v1/search` -> 200 with the new memory in results. - - `POST /api/v1/memories/{id}/promote` -> 200. - - `GET /api/v1/stats` -> 200. - - `GET /api/v1/domains` -> 200 (likely empty). - - `DELETE /api/v1/memories/{id}` -> 204. - -- `phase_3/auth_bearer_token.rs`: - - unauth: `GET /api/v1/stats` returns 401 and `Content-Type: - application/problem+json`. - - valid Bearer: same call returns 200. - - revoked key: `POST /api/v1/keys/{id}` DELETE then reuse -> 401. - - tampered Bearer (last char flipped) -> 401. - -- `phase_3/auth_api_key_header.rs`: - - `X-API-Key: vst_...` alone -> 200. - - Both Bearer and X-API-Key with different values -> Bearer wins (asserted - via a key that is read-only in Bearer + full-scope X-API-Key, then - confirming a write 403s). - -- `phase_3/auth_session_cookie.rs`: - - `POST /dashboard/login` with valid key -> 200 + `Set-Cookie: - vestige_session=...; HttpOnly; SameSite=Strict; Path=/`. - - reuse cookie: `GET /api/v1/stats` returns 200. - - tampered cookie (change one char): -> 401. - - `POST /dashboard/logout` -> `Set-Cookie: vestige_session=; Max-Age=0`. - -- `phase_3/auth_domain_filter.rs`: - - Key with `domain_filter = ["dev"]`: - - `POST /api/v1/search` without header -> search is scoped to `["dev"]` - (insert fixtures with two domains, assert only `dev` rows returned). - - `X-Vestige-Domain: dev` -> same. - - `X-Vestige-Domain: home` -> 403 with detail `domain not permitted`. - - Key with empty filter + `X-Vestige-Domain: dev` -> scoped to `["dev"]`. - - Key with empty filter + no header -> no scoping. - -- `phase_3/auth_scope_enforcement.rs`: - - read-only key cannot call `POST /api/v1/memories` -> 403. - - read-only key CAN call `POST /api/v1/search` -> 200. - -- `phase_3/bind_safety_nonlocalhost_without_auth.rs`: - - Spawn `vestige serve --bind 0.0.0.0:0` as a subprocess with `auth.enabled - = false` via a temp `vestige.toml`. - - Assert: non-zero exit, stderr contains `refusing to bind`, no listener - ever opens (confirm by trying to connect to the configured port and - expecting connection refused after a short timeout). - -- `phase_3/cli_keys_create_list_revoke.rs`: - - Spawn the `vestige` CLI binary with `--data-dir `. - - Run `vestige keys create --label test --scopes read,write`; capture - stdout (the plaintext) and stderr (the human summary). Assert `vst_` - prefix in stdout. - - Run `vestige keys list`; assert no plaintext, label `test` present. - - Run `vestige keys revoke `; confirm exit 0. - - Run `vestige keys list`; assert label no longer visible without `--all`. - -- `phase_3/dashboard_login_flow.rs`: - - Full loop: login -> fetch `/dashboard` (gets SPA index, unauthed ok) -> - fetch `/api/memories` (authed via cookie) -> logout -> fetch `/api/memories` - (401). - -- `phase_3/deprecation_auth_token.rs`: - - Start the server with `VESTIGE_AUTH_TOKEN=test12345...` and no created - keys. Send a Bearer request with that token -> 200. Assert stderr log - contains `deprecated`. - -### Smoke test (`tests/phase_3/smoke/`) - -- `remote_mcp_client.sh`: - - #!/usr/bin/env bash - set -euo pipefail - KEY="${VESTIGE_TEST_KEY:?set me}" - HOST="${VESTIGE_HOST:-http://127.0.0.1:3928}" - # Initialize a session - RESP=$(curl -sS -D /tmp/h -H "Authorization: Bearer $KEY" \ - -H "Content-Type: application/json" \ - -d '{"jsonrpc":"2.0","id":1,"method":"initialize", - "params":{"protocolVersion":"2025-11-25", - "clientInfo":{"name":"smoke","version":"0"}, - "capabilities":{}}}' \ - "$HOST/mcp") - SID=$(grep -i 'mcp-session-id:' /tmp/h | awk '{print $2}' | tr -d '\r') - # tools/list - curl -sS -H "Authorization: Bearer $KEY" \ - -H "Mcp-Session-Id: $SID" \ - -H "Content-Type: application/json" \ - -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \ - "$HOST/mcp" | jq '.result.tools | length' - echo "smoke ok" - -## Acceptance Criteria - -- [ ] `cargo build -p vestige-mcp` -- zero warnings, all feature combinations - (`--no-default-features`, default, `--features ort-dynamic`). -- [ ] `cargo clippy --workspace --all-targets --all-features -- -D warnings`. -- [ ] `cargo fmt --all --check`. -- [ ] All `tests/phase_3/*.rs` pass, plus phase_1 and phase_2 remain green. -- [ ] Unauth request to `POST /mcp` returns 401 with - `Content-Type: application/problem+json` and a body containing `status`, - `title`, `detail`. -- [ ] Binding `0.0.0.0:` with `[auth].enabled = false` makes the - process exit with code 2 and print `refusing to bind` to stderr. -- [ ] `vestige keys create --label X` prints exactly one line on stdout - matching `^vst_[A-Za-z0-9_-]+$`; `vestige keys list` never prints that - line back. -- [ ] Dashboard login from a browser-like client (tested via the reqwest - `Client::cookie_store(true)` harness) yields a `Set-Cookie` with - `HttpOnly`, `SameSite=Strict`, `Path=/`, and Max-Age present. -- [ ] A second machine can run a curl-based MCP client against the server - (smoke test) and receive successful `tools/list` responses. -- [ ] `VESTIGE_AUTH_TOKEN` still works and emits the deprecation warning. -- [ ] `tests/phase_3/auth_domain_filter.rs` demonstrates that a key scoped to - `dev` cannot read `home`-domain memories via any of the three auth modes - and cannot escape with `X-Vestige-Domain`. - -## Rollback Notes - -- Ship behind an on-by-default Cargo feature `http-server` on - `vestige-mcp`. Disabling it reverts to stdio + existing localhost HTTP - (`protocol/http.rs` in its current form) with zero behaviour change. -- SQL: migration `0300_api_keys.sql` is additive only; rollback is a single - `DROP TABLE api_keys;` in `0300_api_keys.down.sql` for both backends. Keep a - row count safety check in the down migration and log the deletion. -- Session secret file: deleting `/session_secret` invalidates every - outstanding cookie; users simply log in again. Safe to rotate. -- Env var sunset schedule: - - v2.1.x: `VESTIGE_AUTH_TOKEN` emits a warning, still works. - - v2.2.0: `VESTIGE_AUTH_TOKEN` refused with an error pointing at - `vestige keys create`. -- Downgrade procedure: `git revert` the Phase 3 merge, then run the down - migration. No data loss; plaintext keys were only ever in user-side - secret managers. - -## Open Implementation Questions - -1. JSON-RPC library: hand-rolled vs jsonrpsee? - - - Candidate A: keep the hand-rolled types in `protocol/types.rs` plus the - session-aware `post_mcp` handler already in `protocol/http.rs`. - - Candidate B: switch to `jsonrpsee = "0.24"` with the `server` feature - and adapt it to Axum via `jsonrpsee::server::Server`. - - RECOMMENDATION: A. Phase 3 is about auth and transport surfaces, not - library rewrites. The existing types are already correct, tested, and - compatible with Streamable HTTP; the 29 cognitive modules depend on - `McpServer::handle_request`, which does not map 1:1 to jsonrpsee's - `RpcModule` trait. Re-evaluate in a future phase only if we need subscription - notifications beyond SSE. - -2. Streamable HTTP vs plain POST-with-JSON? - - - The MCP spec titled "Streamable HTTP" defines: `POST /mcp` for - request/response, `GET /mcp` for SSE where the client subscribes to - server-initiated messages, and an `Mcp-Session-Id` header for session - correlation. The current implementation already covers POST + session - header + DELETE; Phase 3 adds the GET/SSE half. - - RECOMMENDATION: implement the full Streamable HTTP transport. Long-running - tools (dream, consolidate, discover) benefit from SSE progress events, and - Claude Desktop / Claude Code both speak Streamable HTTP natively. Keeping - POST-only would work for short calls but block the UX we want for - background jobs. - -3. Session cookie crate? - - - Candidate A: `axum-extra::extract::cookie::SignedCookieJar` with a 64-byte - `Key`. - - Candidate B: `tower-sessions = "0.13"` with the `MemoryStore` or - `PostgresStore` session backend. - - Candidate C: stateless JWT via `jsonwebtoken`. - - RECOMMENDATION: A. We do not need server-side session state (the `api_keys` - row is the state; the cookie is merely a signed pointer to it). B adds a - whole storage backend we do not need. C adds signing-algorithm surface area - and revocation becomes awkward ("revoked key" with a long-lived JWT). - `SignedCookieJar` gives us HMAC-signed cookies for free, integrates with - axum extractors, and the payload is tiny. - -4. Key format and length? - - - 22 random bytes base64url-no-pad = 176 bits entropy, encoded ~30 chars, - full key ~34 chars with the `vst_` prefix. Long enough to make - brute-force infeasible, short enough to paste into config files. - - Alternatives: 32 bytes (40 chars, overkill), 16 bytes (128 bits, marginal - for secret material shared over networks). - - RECOMMENDATION: 22 bytes. Prefix `vst_` is already documented in the PRD - and gives grep-ability. - -5. Rate limiting: in scope for Phase 3? - - - Useful: mitigates slow brute force, runaway agents. - - Expensive to design well (per-key, per-IP, per-endpoint). - - RECOMMENDATION: OUT of scope. Track as `docs/adr/0002-rate-limiting.md` - follow-up. Axum + `tower` has `ConcurrencyLimitLayer` (already used); a - follow-up can add `governor` or `tower_governor` behind the auth layer so - identity is available. - -6. CORS policy defaults for dashboard in server mode? - - - Candidate A: allow only origins derived from `server.bind` host + the - dashboard port. - - Candidate B: allow user-listed origins via `server.allowed_origins` - config, with A as fallback. - - Candidate C: open CORS to `*` when TLS is configured. - - RECOMMENDATION: B. Auto-populate `allowed_origins` from the bind address - and dashboard port at start time; if the operator sets the config list, - use that list verbatim. Never `*` (`allow_credentials = true` is - incompatible with `*` anyway). - -7. Dashboard session lifetime? - - - 8 hours for default; configurable via `auth.session_ttl_hours`. - - Rotate on each write? (Rolling sessions.) - - RECOMMENDATION: 8 hours fixed, non-rolling. Revisit if users complain. - -8. Handling `tools/call` scope granularity? - - - Today, `tools/call` is a single MCP method. Read-only tools like - `search`, `deep_reference`, `predict` should be callable with a - read-only key. - - RECOMMENDATION: map tool names to scopes in `McpServer::handle_tools_call`. - Read-only names: `search`, `session_context`, `memory` with action in - `{get, state, get_batch}`, `deep_reference`, `cross_reference`, `predict`, - `explore_connections`, `find_duplicates`, `memory_timeline`, - `memory_changelog`, `memory_health`, `memory_graph`, `importance_score`, - `system_status`. Everything else requires `write`. If a read-only key - calls a write tool, return a JSON-RPC error with code `-32003` - ("server not initialized" is close but wrong; reuse `-32603 internal` with - a descriptive message or add a new `-32004 UnauthorizedTool`). RECOMMEND - adding `-32004`. - -9. How to bridge `MemoryStore` trait with dashboard state (`AppState`)? - - - Today `AppState.storage: Arc` is a concrete type. - - Phase 2 introduces `Arc`. - - RECOMMENDATION: in Phase 3, introduce `AppCtx { store: Arc, - cognitive, config, event_tx }` as the single state type for the unified - router. Keep `AppState` as a thin wrapper (or alias) if the dashboard - handlers need to stay untouched in this phase. Migrate the dashboard - handlers to the trait in a follow-up refactor to contain the blast radius. - -10. Windows support for `session_secret` and `auth_token` file modes? - - - Unix gets `0600` via `OpenOptionsExt`. - - Windows has no direct equivalent; ACLs differ. - - RECOMMENDATION: document the limitation; use default permissions on - Windows. Add a `#[cfg(windows)]` placeholder to set owner-only ACLs via - `windows-acl` in a follow-up, not Phase 3. - -### Critical Files for Implementation - -- /home/delandtj/prppl/vestige/crates/vestige-mcp/src/protocol/http.rs -- /home/delandtj/prppl/vestige/crates/vestige-mcp/src/dashboard/mod.rs -- /home/delandtj/prppl/vestige/crates/vestige-mcp/src/main.rs -- /home/delandtj/prppl/vestige/crates/vestige-mcp/src/bin/cli.rs -- /home/delandtj/prppl/vestige/crates/vestige-mcp/Cargo.toml diff --git a/docs/plans/0004-phase-4-emergent-domain-classification.md b/docs/plans/0004-phase-4-emergent-domain-classification.md deleted file mode 100644 index d9f2355..0000000 --- a/docs/plans/0004-phase-4-emergent-domain-classification.md +++ /dev/null @@ -1,883 +0,0 @@ -# Phase 4 Plan: Emergent Domain Classification - -**Status**: Draft -**Depends on**: Phase 1 (domain columns on memories, `Domain` struct + `DomainStore` methods on `MemoryStore`, `Embedder` trait), Phase 2 (Postgres JSONB + TEXT[] support for domain fields, `embedding_model` registry parity), Phase 3 (Axum HTTP server, REST `/api/v1/` scaffolding, API key auth middleware, signed dashboard session cookies) -**Related**: docs/adr/0001-pluggable-storage-and-network-access.md (Phase 4), docs/prd/001-getting-centralized-vestige.md (Emergent Domain Model) - ---- - -## Scope - -### In scope - -- `DomainClassifier` cognitive module under `crates/vestige-core/src/neuroscience/domain_classifier.rs`, alongside existing neuroscience modules (spreading_activation, synaptic_tagging, ...). -- HDBSCAN discovery pipeline using the `hdbscan` crate (v0.10): load all embeddings, cluster, extract centroids, extract top-terms via TF-IDF over cluster members, persist via the trait's `DomainStore` methods. -- Soft-assignment pipeline: for each memory, compute `cosine_similarity(memory.embedding, domain.centroid)` for every domain, store raw scores in `domain_scores` JSONB, threshold into `domains[]` using `assign_threshold` (default 0.65). -- Automatic classification on ingest: run through `CognitiveEngine` / `smart_ingest` so new memories get classified against existing centroids immediately; skip when `domain_count == 0` (Phase 0 accumulation). -- Re-cluster hook in dream consolidation: every Nth four-phase dream cycle (N=5 default) triggers a discovery pass and generates proposals (split / merge / none). Proposals land in a new `domain_proposals` table, surface in the dashboard, and are never auto-applied (conservative drift, ADR Q7). -- Context signals: `SignalSource` trait with `GitRepoSignal` (detects `.git` in CWD or `metadata.cwd`) and `IdeHintSignal` (reads `metadata.editor` / `metadata.ide`). Each returns a `boost_map` of `domain_id -> additive delta` (typical +0.05). Injected as a `signal_boost: Option>` parameter into `DomainClassifier::classify`. -- Cross-domain spreading activation decay: `ActivationNetwork` traversal multiplies the edge's effective weight by `cross_domain_decay` (default 0.5) when `target.domains` and `source.domains` are disjoint. Strict "no overlap" policy, not graded. -- CLI subcommands (in `crates/vestige-mcp/src/bin/cli.rs`, under a new `Domains` command group): `list`, `discover [--min-cluster-size N] [--force]`, `rename `, `merge [--into ]`. Human-readable tables on stdout; JSON via `--json`. -- Dashboard UI additions (`apps/dashboard/src/routes/(app)/domains/`): list page, per-domain detail (memories, centroid top_terms, score histogram, proposal review controls). -- REST endpoints under `/api/v1/domains` (introduced by Phase 3 skeleton, implemented in Phase 4): list, discover, rename, merge, proposal list / accept / reject. -- Config additions: `[domains]` section in `vestige.toml` covering `assign_threshold`, `recluster_interval`, `min_cluster_size`, `cross_domain_decay`, `discovery_threshold`, `merge_threshold`, `signal_boost` (per-signal toggle). - -### Out of scope - -- Phase 5 federation (explicit separate ADR). Domain centroids are installation-local; no sync. -- Learned re-weighting of domain scores (future, only if retrieval-quality metrics show a need). -- Interactive cluster-membership editing in the UI (drag-and-drop reassign) -- future enhancement. -- Multi-user domain namespaces. One domain set per installation; API keys that carry `domain_filter` just restrict access, they do not create namespaces. -- Auto-sweep of `min_cluster_size` / auto-tuned `assign_threshold` (ADR resolution Q6 + Q9: static defaults, user tunes). -- Graded cross-domain decay (`|A intersect B| / max(|A|,|B|)`) -- strict "no overlap" is the Phase 4 rule. - ---- - -## Prerequisites - -Artifacts that Phases 1-3 are expected to have landed: - -- In `vestige-core`: - - `Embedder` trait (`crates/vestige-core/src/embedder/`). - - `MemoryStore` trait (`crates/vestige-core/src/storage/trait.rs` or similar) including `DomainStore` methods: `list_domains`, `get_domain`, `upsert_domain`, `delete_domain`, `classify(&[f32]) -> Vec<(String, f64)>`, plus a bulk accessor such as `all_embeddings()` (already present in sqlite.rs as `get_all_embeddings`) and a `get_all_memories_with_embeddings()` iterator for discovery. The trait must expose a method to batch-update `(domains, domain_scores)` for a memory id. - - `Domain` struct: `{ id: String, label: String, centroid: Vec, top_terms: Vec, memory_count: usize, created_at: DateTime }`. - - Columns on memories in both SQLite and Postgres: `domains TEXT[]` (or JSON array on SQLite) and `domain_scores JSONB` (or TEXT JSON on SQLite). - - The `domains` table in both backends (see PRD schema sketch). -- In `vestige-mcp`: - - Axum `/api/v1/` router prefix with auth middleware. - - CLI skeleton (`bin/cli.rs`) using `clap`; Phase 4 adds a `Domains` subcommand tree. - - REST handlers file structure ready under `crates/vestige-mcp/src/dashboard/handlers.rs` (legacy) and a dedicated REST handler under `/api/v1/`; Phase 4 adds `domains.rs` handler module. - - SvelteKit dashboard (`apps/dashboard/`) with existing `(app)/memories`, `(app)/timeline`, `(app)/stats`, etc. Phase 4 adds `(app)/domains/`. - -New workspace crate additions required (added manually to `Cargo.toml`, since `cargo add` is not run from the plan): - -- `hdbscan = "0.10"` in `crates/vestige-core/Cargo.toml` (feature-gated behind `domain-classification`). -- Optional: a lightweight stop-word constant inline; no external stop-word crate -- the neuroscience modules already do tokenization on whitespace + length>3 (see `dreams.rs::content_similarity`). Reuse that style; no `ndarray` needed because `hdbscan` v0.10 accepts `&[Vec]` directly (verified from PRD snippet). -- No new deps in `vestige-mcp` for Phase 4 -- CLI reuses `clap` / `colored` / `comfy-table` if already present, otherwise a hand-rolled padded print. We pick hand-rolled to avoid adding a table crate; this matches the existing style of `run_stats` in `cli.rs`. - -Test fixtures: - -- A JSON seed corpus checked into `tests/phase_4/fixtures/seed_500.json` containing >= 500 memories drawn from three plausible clusters. A builder function `tests/phase_4/support/fixtures.rs::build_seed_corpus()` deterministically generates or loads this corpus. Each record has `content`, `tags`, `embedding` (768D bge-base-en-v1.5; use a committed vector or a deterministic mock embedder in tests). For deterministic tests we fake embeddings by hashing content -- acceptable as long as the fake preserves cluster separability (prefix-based: "DEV-...", "INFRA-...", "HOME-..." seeds three Gaussian blobs). -- Reuse `Embedder` mock from Phase 1 tests (`MockEmbedder`) for discovery tests that need real cosine similarity. -- A minimal git-repo fixture created in a tempdir (`tempfile::tempdir` + `std::process::Command::new("git").arg("init")`) for context-signal tests. - ---- - -## Deliverables - -1. `DomainClassifier` cognitive module: struct, defaults, `classify`, `classify_with_boost`, `reassign_all`, `discover`. -2. `domain_terms` helper (TF-IDF over cluster members, returning `top_k` terms). -3. `cli domains discover` subcommand. -4. `cli domains list` / `rename` / `merge` subcommands. -5. Auto-classify hook on ingest (wired into the cognitive engine's ingest pipeline before persistence). -6. Re-cluster hook in dream consolidation (`DreamEngine::run` orchestrator gets an optional `DomainReClusterHook`; triggers every Nth dream). -7. Context signal extractor module (`crates/vestige-core/src/neuroscience/context_signals.rs`) with `SignalSource` trait + `GitRepoSignal` + `IdeHintSignal`. -8. Cross-domain spreading activation decay in `ActivationNetwork::activate` (config-driven). -9. `vestige.toml` `[domains]` section + defaults loader. -10. Dashboard UI: SvelteKit routes `(app)/domains/+page.svelte` (list), `(app)/domains/[id]/+page.svelte` (detail), `(app)/domains/proposals/+page.svelte` (review). -11. REST endpoints under `/api/v1/domains` + `/api/v1/domains/proposals`. -12. `domain_proposals` table + migration + `DomainProposal` trait methods on `MemoryStore`. -13. WebSocket event `VestigeEvent::DomainProposalCreated` so the dashboard gets a live notification after a re-cluster fires. - ---- - -## Detailed Task Breakdown - -### 1. `DomainClassifier` cognitive module - -**File**: `crates/vestige-core/src/neuroscience/domain_classifier.rs` -**Export**: in `crates/vestige-core/src/neuroscience/mod.rs`, add `pub mod domain_classifier;` and re-export `pub use domain_classifier::{DomainClassifier, ClassificationResult, DomainProposal, ProposalKind};` -**Deps**: `hdbscan = "0.10"`, `serde`, `serde_json`, `chrono`, `tracing`, existing `crate::storage::Domain`, `crate::storage::MemoryStore` trait. - -Struct and defaults (match PRD exactly): - -```rust -pub struct DomainClassifier { - pub assign_threshold: f64, // default 0.65 - pub discovery_threshold: usize, // default 150 - pub recluster_interval: usize, // default 5 (every 5th dream) - pub min_cluster_size: usize, // default 10 - pub min_samples: usize, // default 5 (HDBSCAN) - pub cross_domain_decay: f64, // default 0.5 - pub merge_threshold: f64, // default 0.90 (centroid cosine) - pub top_terms_k: usize, // default 10 -} - -impl Default for DomainClassifier { ... } -``` - -Result types: - -```rust -#[derive(Debug, Clone)] -pub struct ClassificationResult { - pub scores: HashMap, // raw per-domain similarities - pub domains: Vec, // above assign_threshold -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ProposalKind { - Split { parent: String, children: Vec }, - Merge { targets: Vec, suggested_label: String }, - NewCluster { top_terms: Vec }, -} - -#[derive(Debug, Clone)] -pub struct DomainProposal { - pub id: String, // uuid v4 - pub kind: ProposalKind, - pub rationale: String, - pub confidence: f64, - pub created_at: DateTime, - pub status: ProposalStatus, // Pending | Accepted | Rejected -} -``` - -Key methods (all pure where possible; all pub): - -```rust -impl DomainClassifier { - pub fn classify(&self, embedding: &[f32], domains: &[Domain]) -> ClassificationResult; - - pub fn classify_with_boost( - &self, - embedding: &[f32], - domains: &[Domain], - boost: Option<&HashMap>, - ) -> ClassificationResult; - - pub async fn reassign_all( - &self, - store: &dyn MemoryStore, - domains: &[Domain], - ) -> Result; - - pub async fn discover( - &self, - store: &dyn MemoryStore, - ) -> Result, StorageError>; - - pub async fn propose_changes( - &self, - store: &dyn MemoryStore, - existing: &[Domain], - newly_discovered: &[Domain], - ) -> Result, StorageError>; - - pub async fn apply_proposal( - &self, - store: &dyn MemoryStore, - proposal: &DomainProposal, - ) -> Result<(), StorageError>; -} -``` - -Behavior notes: - -- `classify` returns empty `{ scores: {}, domains: [] }` iff `domains.is_empty()` (accumulation phase). This matches the PRD snippet verbatim. -- `classify_with_boost` adds the boost delta to each score AFTER cosine, before thresholding. It clamps to `[0.0, 1.0]`. Boost keys not present in `domains` are ignored. -- `reassign_all` streams memories in batches of 500 (iterator on the store) to keep memory bounded; for each memory issues a single `UPDATE memories SET domains = ?, domain_scores = ? WHERE id = ?` call. Returns count of memories whose `domains` vector actually changed. -- `discover` loads all `(id, embedding)` pairs via an `all_embeddings()` method on the store (exists under `#[cfg(all(feature = "embeddings", feature = "vector-search"))]` in `sqlite.rs::get_all_embeddings`; Phase 1 should promote this onto the trait -- if not yet promoted, add the method). Then: - 1. Build `Vec>` and index -> id map. - 2. `Hdbscan::default_hyper_params(&embeddings).min_cluster_size(self.min_cluster_size).min_samples(self.min_samples).build()` (exact builder depends on hdbscan 0.10 surface; see Open Question). - 3. `let labels = clusterer.cluster()?;` - 4. `let centers = clusterer.calc_centers(Center::Centroid, &labels)?;` - 5. Group indices by label ignoring -1 (noise). For each cluster compute `top_terms` via `compute_top_terms`. - 6. Preserve stable IDs where possible: match each new cluster centroid to the closest existing domain by cosine; if similarity > 0.85, reuse the existing domain id + label. Otherwise generate a fresh id `cluster_{n}` with a label derived from the first 2 terms. - 7. Upsert all resulting `Domain`s via the store. -- `propose_changes` compares old vs new clusters: - - **Split**: an old domain that best-matches two or more new domains each with >= `min_cluster_size` members. Rationale: "domain `dev` is now 2 clusters of >=10 memories: `systems` and `networking`". - - **Merge**: two old domains whose centroids now satisfy `cosine > merge_threshold` get a merge proposal. - - **NewCluster**: a new cluster that doesn't match any old domain above 0.85 similarity. -- `apply_proposal` runs the split or merge against the store (reassign memberships via `reassign_all`), then marks the proposal `Accepted`. It never runs automatically -- only via the CLI or dashboard. - -Helper: - -```rust -fn compute_top_terms(documents: &[&str], k: usize) -> Vec; -``` - -Uses TF-IDF with IDF computed over the entire passed-in corpus (the `documents` slice), tokenization = whitespace split, lowercase, strip non-alphanumeric, drop tokens shorter than 4 chars and a small built-in stop-word list (`the`, `and`, `for`, `that`, `with`, ...). Matches the tokenizer used in `dreams.rs::content_similarity` and `dreams.rs::extract_patterns` so behavior is predictable. - -Cosine similarity helper: - -```rust -fn cosine_similarity(a: &[f32], b: &[f32]) -> f64; -``` - -Keep the existing crate-level `cosine_similarity` if already present (check `embeddings::` or `search::`); otherwise add a private one. Returns 0.0 on dimension mismatch, panics would be a bug. - -### 2. Top-terms computation helper - -**File**: same module, private section. - -- `fn tokenize(text: &str) -> Vec`: lowercase, split on non-alphanumeric, filter len >= 4, drop stop-words. -- `fn tfidf_top_k(docs: &[&str], k: usize) -> Vec`: - 1. `tf[doc_idx][term] = count / total_terms`. - 2. `df[term] = docs containing term`. - 3. `idf[term] = log((N + 1) / (df[term] + 1)) + 1` (smoothed). - 4. For each term, average `tf` across docs in the cluster; multiply by `idf`; sort desc; return top `k`. - -Cluster top-terms are computed over cluster members only, with IDF over the **whole corpus** (all memory contents), not the cluster, so common words get penalized globally. Recompute global IDF once per `discover` call. - -### 3. CLI subcommand: `vestige domains discover` - -**File**: `crates/vestige-mcp/src/bin/cli.rs` - -Add to `enum Commands`: - -```rust -/// Emergent domain management -Domains { - #[command(subcommand)] - action: DomainAction, -}, -``` - -```rust -#[derive(clap::Subcommand)] -enum DomainAction { - /// List all discovered domains - List { - #[arg(long)] json: bool, - }, - /// Run HDBSCAN discovery on all embeddings and propose domains - Discover { - #[arg(long, default_value_t = 10)] min_cluster_size: usize, - /// Skip the proposal flow and write new domains directly (first-time use) - #[arg(long)] force: bool, - #[arg(long)] json: bool, - }, - /// Rename a domain (by id) - Rename { - id: String, - new_label: String, - }, - /// Merge two domains - Merge { - a: String, - b: String, - #[arg(long)] into: Option, // default: `a` - }, -} -``` - -Handler plumbing lives in `run_domains(action)` dispatching to `run_domains_list`, `run_domains_discover`, `run_domains_rename`, `run_domains_merge`. Each opens the default `Storage`, constructs a `DomainClassifier::default()`, and invokes the appropriate method. - -Output format for `list`: - -``` -ID LABEL MEMORIES TOP TERMS -dev Development 87 rust, trait, async, tokio, zinit -infra Infrastructure 47 bgp, sonic, vlan, frr, peering -home Home 31 solar, kwh, battery, pool, esphome -(unclassified) 12 -``` - -Produced via plain `print!` with `%-15s %-18s %-10d %s` style padding. `--json` emits `serde_json::to_string_pretty(&domains)`. - -Output format for `discover` with `--force`: - -``` -HDBSCAN: 500 embeddings, min_cluster_size=10, min_samples=5 -Found 3 clusters (ignoring 14 noise points) - cluster_0 (N=47) top: bgp, sonic, vlan, frr, peering - cluster_1 (N=31) top: solar, kwh, battery, pool, esphome - cluster_2 (N=22) top: rust, trait, async, tokio, zinit - -Writing 3 domains to the store... -Soft-assigning 500 memories against centroids... - multi-domain: 43 - single-domain: 412 - unclassified (below threshold 0.65): 45 -Done in 7.4s. -``` - -Output format for `discover` without `--force` (post-Phase-0): - -``` -HDBSCAN: 623 embeddings, min_cluster_size=10 -Comparing to existing 3 domains... - -Proposals (pending, accept via dashboard or `vestige domains proposals`): - [split] dev -> (systems:34, networking:28) confidence 0.82 - [new] cluster_5 (books, novels, reading) confidence 0.71 - -Run `vestige domains proposals` to review, or open the dashboard. -``` - -### 4. CLI: `list`, `rename`, `merge` - -- `list`: calls `store.list_domains()`, fetches unclassified count via `store.count_memories_without_domains()` (Phase 1 should have provided this; if not, Phase 4 adds it to the trait and both backends). -- `rename`: `store.get_domain(id)` -> mutate `label` -> `store.upsert_domain`. No memory touch. -- `merge`: load both, compute blended centroid (weighted by `memory_count`), merge `top_terms` (union, recompute TF-IDF rank if both sides share the corpus), delete the non-`into` domain, call `reassign_all`. Wrapped in a transaction on Postgres; on SQLite rely on the existing writer-lock pattern. - -### 5. Auto-classify on ingest - -**File**: `crates/vestige-core/src/cognitive.rs` (or equivalent ingest entry in `vestige-mcp/src/tools/smart_ingest.rs`). - -Integration point: just before the record is persisted in the smart-ingest path, after the embedder has produced `embedding` and before `storage.insert(...)`. Trace the current call site -- today `Storage::ingest(IngestInput)` computes embedding inside storage; in Phase 1 the embedder becomes external (ADR decision Q2), so classification can hook right there in the cognitive engine. - -Pseudocode: - -```rust -let embedding = embedder.embed(&input.content).await?; -let domains = store.list_domains().await?; - -let (domains_assigned, domain_scores) = if domains.is_empty() { - (Vec::new(), HashMap::new()) -} else { - let boost = context_signals.gather_boost(&input.metadata, &domains); - let result = classifier.classify_with_boost(&embedding, &domains, boost.as_ref()); - (result.domains, result.scores) -}; - -record.embedding = Some(embedding); -record.domains = domains_assigned; -record.domain_scores = domain_scores; -store.insert(&record).await?; -``` - -Edge cases: - -- Accumulation phase (`domains.is_empty()`): skip classification entirely. Zero overhead. -- Embedding failed / skipped: leave `domains = []`, `domain_scores = {}`. Never fail ingest because of classification. -- Metric: emit `VestigeEvent::MemoryClassified { id, domains, top_score }` on the WebSocket bus so the dashboard sees it live. - -### 6. Re-cluster hook in dream consolidation - -**File**: `crates/vestige-core/src/advanced/dreams.rs` (long file, 1131-line `dream()` entry on the `MemoryDreamer` impl) plus `crates/vestige-core/src/consolidation/phases.rs` (the `DreamEngine::run` orchestrator). - -Design: the `DreamEngine::run(...)` returns `FourPhaseDreamResult`. It does not currently know how many times it has run. Phase 4 introduces a persistent counter on disk (column `dream_cycle_count` on a new singleton `system_state` table, or a simple row in the existing `metadata` / `embedding_model` registry). After the Integration phase finishes, the cognitive engine increments the counter and, if `counter % recluster_interval == 0`, launches discovery asynchronously: - -Extension struct in `phases.rs`: - -```rust -pub struct DreamReClusterHook<'a> { - pub classifier: &'a DomainClassifier, - pub store: &'a dyn MemoryStore, - pub event_tx: Option<&'a tokio::sync::mpsc::UnboundedSender>, -} - -impl<'a> DreamReClusterHook<'a> { - pub async fn tick(&self, cycle_count: usize) -> Result, StorageError> { - if cycle_count == 0 || cycle_count % self.classifier.recluster_interval != 0 { - return Ok(vec![]); - } - let existing = self.store.list_domains().await?; - let rediscovered = self.classifier.discover(self.store).await?; - let proposals = self - .classifier - .propose_changes(self.store, &existing, &rediscovered) - .await?; - for p in &proposals { - self.store.insert_domain_proposal(p).await?; - if let Some(tx) = self.event_tx { - let _ = tx.send(VestigeEvent::DomainProposalCreated { - id: p.id.clone(), - kind: format!("{:?}", p.kind), - confidence: p.confidence, - timestamp: Utc::now(), - }); - } - } - Ok(proposals) - } -} -``` - -Caller wires `tick()` after `DreamEngine::run()` returns, at the ingest/consolidation orchestrator level. The hook never mutates existing domains -- it only writes proposals. The acceptance path is manual (CLI or dashboard). - -Counter storage: add method `store.bump_dream_cycle_count() -> Result` returning the new count. Single-row table: - -```sql -CREATE TABLE IF NOT EXISTS system_state ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL -); --- seed: ('dream_cycle_count', '0') -``` - -### 7. Context signal extractor - -**File**: `crates/vestige-core/src/neuroscience/context_signals.rs` - -```rust -pub trait SignalSource: Send + Sync { - /// Returns domain_id -> additive boost (positive or negative, typically in [-0.1, +0.1]). - fn boost_map( - &self, - input_metadata: &serde_json::Value, - domains: &[Domain], - ) -> HashMap; - - fn name(&self) -> &'static str; -} - -pub struct GitRepoSignal { - pub boost: f64, // default +0.05 -} - -pub struct IdeHintSignal { - pub boost: f64, -} - -pub struct ContextSignals { - sources: Vec>, -} - -impl ContextSignals { - pub fn gather_boost( - &self, - input_metadata: &serde_json::Value, - domains: &[Domain], - ) -> Option>; -} -``` - -Signal encoding convention (document in the module header): - -- A signal is a **soft prior**. It nudges the post-cosine score by a small additive delta, clamped to `[-0.10, +0.10]` per signal. -- Multiple signals sum, then the final boost per domain is clamped to `[-0.15, +0.15]` so signals cannot by themselves push a memory into or out of a domain; the embedding similarity dominates. -- Signals target domains by heuristic: `GitRepoSignal` boosts any domain whose `top_terms` overlaps `{"rust","async","trait","function","class","def","git","commit","fn","code"}`. `IdeHintSignal` does the same for `{"file","line","editor","vscode","neovim","rust-analyzer","lsp"}`. -- All signal boosts are logged via `tracing::debug!` so users can audit why a memory picked up a domain. - -`GitRepoSignal::boost_map` implementation: - -```rust -fn boost_map(&self, meta: &Value, domains: &[Domain]) -> HashMap { - let is_git = meta.get("cwd") - .and_then(|v| v.as_str()) - .map(|cwd| std::path::Path::new(cwd).join(".git").exists()) - .unwrap_or(false) - || meta.get("git_repo").is_some(); - if !is_git { return HashMap::new(); } - let mut out = HashMap::new(); - for d in domains { - let code_hits = d.top_terms.iter() - .filter(|t| CODE_TERMS.contains(t.as_str())) - .count(); - if code_hits > 0 { out.insert(d.id.clone(), self.boost); } - } - out -} -``` - -Config knob in `[domains.signals]`: `git = true`, `ide = true`, `git_boost = 0.05`, `ide_boost = 0.05`. - -### 8. Cross-domain spreading activation decay - -**File**: `crates/vestige-core/src/neuroscience/spreading_activation.rs` - -Modify `ActivationConfig`: - -```rust -pub struct ActivationConfig { - pub decay_factor: f64, - pub max_hops: u32, - pub min_threshold: f64, - pub allow_cycles: bool, - pub cross_domain_decay: f64, // NEW, default 0.5 -} -``` - -Domain metadata on nodes: the current `ActivationNode` has `id`, `activation`, `last_activated`, `edges: Vec`. Phase 4 adds `pub domains: Vec`. Populated when nodes get added (propagated from the memory's `domains` field). The network is rebuilt on each search from the store; if the in-memory network is persisted (check `ActivationNetwork` lifetime in `CognitiveEngine`), the population happens in the engine at boot and on insert. - -Traversal change, in `ActivationNetwork::activate` loop, replacing the single line `let propagated = current_activation * edge.strength * self.config.decay_factor;`: - -```rust -let cross_penalty = { - let src_doms = self.nodes.get(¤t_id).map(|n| &n.domains); - let tgt_doms = self.nodes.get(&target_id).map(|n| &n.domains); - match (src_doms, tgt_doms) { - (Some(s), Some(t)) if !s.is_empty() && !t.is_empty() => { - let overlap = s.iter().any(|d| t.contains(d)); - if overlap { 1.0 } else { self.config.cross_domain_decay } - } - _ => 1.0, // unclassified on either side: no penalty - } -}; -let propagated = current_activation * edge.strength * self.config.decay_factor * cross_penalty; -``` - -Rationale for "unclassified -> no penalty": unclassified memories are Phase-0 or low-confidence corpus members; penalizing them would block useful cross-pollination during the accumulation ramp. - -API to update a node's domains after reclassification: - -```rust -pub fn set_node_domains(&mut self, id: &str, domains: Vec); -``` - -Called by the reassignment pipeline after `reassign_all`. - -### 9. `vestige.toml` `[domains]` section - -**File**: wherever `vestige.toml` is loaded (search for `[storage]` / `[server]` loaders). Add: - -```toml -[domains] -assign_threshold = 0.65 -discovery_threshold = 150 -recluster_interval = 5 -min_cluster_size = 10 -min_samples = 5 -cross_domain_decay = 0.5 -merge_threshold = 0.90 -top_terms_k = 10 - -[domains.signals] -git = true -ide = true -git_boost = 0.05 -ide_boost = 0.05 -``` - -Rust-side: `DomainsConfig { ... }` struct with `serde(default)` so `vestige.toml` without a `[domains]` section falls back to hard-coded defaults. `DomainClassifier::from_config(cfg: &DomainsConfig) -> Self`. - -### 10. Dashboard UI additions - -**SvelteKit routes** (`apps/dashboard/src/routes/(app)/domains/`): - -- `+page.svelte` (list): fetches `GET /api/v1/domains` and `GET /api/v1/domains/unclassified-count`. Renders a table: `label`, `memories`, `top_terms` chips, `created_at`. Each row links to `/domains/[id]`. A "Discover" button posts `POST /api/v1/domains/discover`. -- `[id]/+page.svelte` (detail): fetches `GET /api/v1/domains/:id`, `GET /api/v1/domains/:id/memories?limit=100`, `GET /api/v1/domains/:id/score-histogram`. Renders: - - Header: label (editable, triggers `PUT /api/v1/domains/:id`), top-terms chips, memory count, created_at. - - Histogram: a vertical bar chart of `domain_scores[:id]` buckets 0-0.1, 0.1-0.2, ..., 0.9-1.0 across all memories. Data source: server precomputes buckets so the client does not need to fetch all scores. - - Memory list: paginated, each row shows the raw score for this domain. -- `proposals/+page.svelte`: fetches `GET /api/v1/domains/proposals?status=pending`. Each pending proposal card shows `kind`, `rationale`, `confidence`, `created_at`, buttons "Accept" (posts `POST /api/v1/domains/proposals/:id/accept`) and "Reject" (`POST .../reject`). Live updates via the existing WebSocket channel (`/ws`) reacting to `DomainProposalCreated` events. - -Styling reuses the existing Tailwind + shadcn-svelte conventions in `apps/dashboard/src/lib/components/`. - -Existing `(app)/stats` and `(app)/feed` pages get a small "Domains" summary panel that links to `/domains`. - -### 11. REST endpoints - -**File**: `crates/vestige-mcp/src/protocol/http.rs` or a new `crates/vestige-mcp/src/api/domains.rs` module, wired into the `/api/v1/` router. - -| Method | Path | Handler | -|--------|------|---------| -| GET | `/api/v1/domains` | `list_domains` -- returns `[Domain...]` + unclassified count | -| POST | `/api/v1/domains/discover` | `trigger_discover` -- body `{ min_cluster_size?: usize, force?: bool }`, returns proposals or applied domains | -| GET | `/api/v1/domains/:id` | `get_domain` | -| PUT | `/api/v1/domains/:id` | `update_domain` -- rename | -| DELETE | `/api/v1/domains/:id` | `delete_domain` -- with `?merge_into=other_id` | -| GET | `/api/v1/domains/:id/memories` | paginated memories in this domain | -| GET | `/api/v1/domains/:id/score-histogram` | precomputed buckets | -| GET | `/api/v1/domains/proposals` | `list_proposals?status=pending` | -| POST | `/api/v1/domains/proposals/:id/accept` | `accept_proposal` | -| POST | `/api/v1/domains/proposals/:id/reject` | `reject_proposal` | - -All handlers go through the Phase 3 auth middleware (Bearer / X-API-Key / session cookie). Responses are JSON; error paths use `StatusCode::*` with a small `{"error": "..."}` body. - -### 12. `domain_proposals` table + trait methods - -Postgres migration (`crates/vestige-core/migrations/postgres/00XX_domain_proposals.sql`): - -```sql -CREATE TABLE domain_proposals ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - kind TEXT NOT NULL, -- 'split' | 'merge' | 'new_cluster' - payload JSONB NOT NULL, -- serialized ProposalKind body - rationale TEXT NOT NULL, - confidence DOUBLE PRECISION NOT NULL, - status TEXT NOT NULL DEFAULT 'pending', -- pending|accepted|rejected - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - resolved_at TIMESTAMPTZ -); -CREATE INDEX idx_domain_proposals_status ON domain_proposals (status, created_at DESC); -``` - -SQLite migration: same table, `UUID` -> `TEXT`, `JSONB` -> `TEXT` with JSON-encoded bodies, `TIMESTAMPTZ` -> `TEXT` ISO-8601. - -`MemoryStore` trait additions: - -```rust -async fn insert_domain_proposal(&self, p: &DomainProposal) -> Result<()>; -async fn list_domain_proposals(&self, status: Option<&str>) -> Result>; -async fn get_domain_proposal(&self, id: &str) -> Result>; -async fn set_proposal_status(&self, id: &str, status: &str) -> Result<()>; -``` - -### 13. WebSocket event for proposals - -**File**: `crates/vestige-mcp/src/dashboard/events.rs` - -Add variant: - -```rust -pub enum VestigeEvent { - // ... existing ... - DomainProposalCreated { - id: String, - kind: String, - confidence: f64, - timestamp: DateTime, - }, - MemoryClassified { - id: String, - domains: Vec, - top_score: f64, - timestamp: DateTime, - }, -} -``` - -The SvelteKit dashboard's WS client reacts to both events: classified events refresh any open domain-detail page; proposal events push a toast and a badge on the navbar. - ---- - -## Test Plan - -Test root: `tests/phase_4/` (a new member of the workspace; mirror the `tests/e2e` layout). - -`tests/phase_4/Cargo.toml`: - -```toml -[package] -name = "vestige-phase4-tests" -version = "0.0.0" -edition = "2024" -publish = false - -[dependencies] -vestige-core = { path = "../../crates/vestige-core", features = ["embeddings", "vector-search", "domain-classification"] } -vestige-mcp = { path = "../../crates/vestige-mcp" } -tokio = { workspace = true } -anyhow = "1" -tempfile = "3" -serde_json = { workspace = true } -uuid = { workspace = true } -``` - -### Unit tests (colocated in `domain_classifier.rs::tests`, `context_signals.rs::tests`, `spreading_activation.rs::tests`) - -Each public function must have at least one test: - -- `classify_empty_domains_returns_empty`: `classify(&[0.0; 768], &[])` returns `ClassificationResult { scores: {}, domains: [] }`. -- `classify_single_domain_scores`: one `Domain` with a known centroid; input embedding equal to centroid; expect score 1.0 and `domains == [id]`. -- `classify_multi_domain_overlap`: two domains A, B; input halfway between centroids; expect both scores >= `assign_threshold`; expect `domains == [A, B]` (order not guaranteed). -- `classify_below_threshold_returns_empty_domains_but_scores_filled`: input orthogonal to all centroids; expect `scores` populated, `domains` empty. -- `classify_with_boost_adds_delta`: same input as above, with `boost = {A: 0.4}`; expect A now above threshold, B unchanged. -- `classify_boost_clamps_to_unit`: `boost = {A: 5.0}`; resulting `scores[A]` must be <= 1.0. -- `tfidf_top_k_returns_distinct_terms`: given three fake docs, `top_k=3` returns three non-duplicate strings, in descending TF-IDF order. -- `tfidf_top_k_drops_stopwords`: `["the and for"]` + real content -> stop-words absent. -- `compute_top_terms_handles_empty_cluster`: returns `vec![]` (no panic). -- `signal_git_present_vs_absent`: `GitRepoSignal` given metadata with `.git` in cwd returns non-empty map; without it returns empty. -- `signal_ide_present_vs_absent`: `IdeHintSignal` ditto for `metadata.editor == "vscode"`. -- `signal_combined_clamped`: two signals both firing each at +0.10 -> combined map values <= +0.15. -- `cross_domain_decay_full_weight_on_overlap`: graph with node A in domain `dev`, node B in domain `dev`, edge A->B strength 1.0; after `activate`, B's activation equals the standard `initial * strength * decay_factor` (no extra penalty). -- `cross_domain_decay_half_weight_no_overlap`: A in `dev`, B in `infra`, same edge -> B's activation is 0.5x that of the overlap case. -- `cross_domain_decay_unclassified_no_penalty`: A classified, B unclassified -> full weight. -- `propose_changes_detects_split`: existing domain `dev`; new discovery returns two clusters whose centroids both sit close to old `dev` centroid, each >= min_cluster_size members -> proposal of kind `Split { parent: "dev", children: [a, b] }`. -- `propose_changes_detects_merge`: two existing domains whose new centroids now have cosine > `merge_threshold` -> proposal of kind `Merge`. -- `propose_changes_detects_new_cluster`: a new cluster with no match >= 0.85 to any existing -> `NewCluster`. -- `apply_proposal_split_updates_memberships`: after accept, memories previously in `dev` get reassigned (some to child a, some to child b) via `reassign_all`. - -### Integration tests (`tests/phase_4/tests/`) - -One file per behavior listed in the Phase 4 acceptance sheet. - -- `discover_seed_corpus.rs` -- loads the 500-memory fixture, runs `classifier.discover(&store).await`, asserts at least 3 clusters, asserts per-cluster intra-similarity mean > 0.6, asserts discovery wall time < 10s in release. Also asserts `top_terms` for each cluster contains at least one expected keyword per cluster (dev: contains any of `rust/trait/async`; infra: `bgp/vlan/network`; home: `solar/battery/pool`). -- `soft_assign_multi_domain.rs` -- inserts a memory "deploy zinit containers over BGP network"; after classify, `domains` contains both `dev` and `infra` (from a known centroid setup). -- `auto_classify_on_ingest.rs` -- with three existing domains, a fresh `smart_ingest` of a dev-ish sentence ends up with `domains == ["dev"]` and non-empty `domain_scores`. -- `reembed_triggers_recluster.rs` -- after `vestige migrate --reembed`, centroids must be recomputed; verify `list_domains()` returns fresh `centroid` values (different from pre-reembed). -- `dream_consolidation_recluster_hook.rs` -- run 5 dream cycles with heavy synthetic memory insertion; after the 5th, assert `list_domain_proposals("pending")` has at least one proposal. -- `proposal_accept_applies_changes.rs` -- accept a split proposal via `apply_proposal`; verify that memories in `dev` are now distributed across the new children and that the old `dev` domain is removed. -- `proposal_reject_leaves_state.rs` -- reject a proposal; verify all domains and memberships unchanged. -- `drift_is_proposal_only.rs` -- over 5 dream cycles with new inserts, never call accept; verify every memory's `domains` field equals its initial post-discovery value. No auto-apply. -- `cross_domain_activation_decay.rs` -- build a `ActivationNetwork` with two memories linked by a strength-1.0 edge, one in `dev`, one in `infra`; activate `dev` memory with 1.0; assert `infra` memory's activation == `0.5 * decay_factor` (0.35 with default decay_factor 0.7). Then set both to `dev` and reassert activation == `0.7`. -- `cli_domains_discover.rs` -- spawn `cargo run -- domains discover --force --json`, parse stdout, assert at least 3 clusters and valid JSON shape. -- `cli_domains_rename_merge.rs` -- happy-path rename then merge, with stdout assertions. -- `context_signal_git_repo.rs` -- ingest the same sentence from inside a tempdir with `.git` vs outside; assert the git-run produces slightly higher `domain_scores` for the code-related domain (diff >= 0.04, matches `git_boost = 0.05`). -- `threshold_tunable.rs` -- same memory, two runs with `assign_threshold = 0.40` vs `0.85`; the low-threshold run assigns more domains than the high-threshold run for the same content. -- `signal_boost_clamped.rs` -- artificially configure `git_boost = 5.0` and assert the resulting per-domain score is still <= 1.0. -- `discover_preserves_stable_ids.rs` -- run discover twice with no new memories; the second run's domain ids match the first's (via centroid-similarity stable-ID matching above 0.85). - -### Dashboard UI tests (`tests/phase_4/ui/`) - -Use curl-driven smoke tests (avoids adding Playwright as a new hard dep; Playwright already exists at `apps/dashboard/playwright.config.ts` and can be extended later). - -- `domains_list_renders.sh` -- `curl -H "X-API-Key: $KEY" http://localhost:3927/api/v1/domains` returns 200 + JSON array with expected keys. -- `domain_detail_histogram.sh` -- `curl .../api/v1/domains/dev/score-histogram` returns 10 buckets. -- `proposal_review_flow.sh` -- create a pending proposal via SQL insert; `curl POST .../api/v1/domains/proposals//accept`; `curl GET .../proposals?status=accepted` shows it. -- `unauth_domain_list_rejected.sh` -- no auth header -> 401. - -### Benchmarks (`tests/phase_4/benches/`) - -Criterion benches: - -- `bench_discover_10k.rs` -- synthetic 10k x 768D embeddings drawn from 5 blobs; assert `discover` wall p95 < 30s on a warm release build. -- `bench_auto_classify_single.rs` -- 20 domains in memory, classify one 768D vector; assert p99 < 5ms. -- `bench_reassign_all.rs` -- 10k memories, 5 domains; assert full `reassign_all` wall time < 90s (100 rows/ms baseline). - ---- - -## Acceptance Criteria - -- [ ] `cargo build -p vestige-core --features domain-classification` zero warnings. -- [ ] `cargo build -p vestige-mcp` zero warnings. -- [ ] `cargo clippy --workspace --all-targets --all-features -- -D warnings` clean. -- [ ] `cargo test -p vestige-phase4-tests` -- all tests in `tests/phase_4/` pass. -- [ ] On a 500+ memory seed corpus covering three natural clusters (dev / infra / home), `vestige domains discover --force` produces sensible top-terms matching the expected keyword sets and labels are stable on a second run. -- [ ] `vestige search` with domain filter `["dev"]` excludes any memory whose `domains` array does not include `dev`. -- [ ] After 5 dream cycles with ongoing inserts, no existing memory's `domains` has silently changed; proposals exist in `domain_proposals` table; accepting a proposal reassigns as described. -- [ ] Cross-domain spreading activation: a query in `dev` that crosses a single edge into an `infra`-only memory still returns the memory but with activation `cross_domain_decay * in-domain_activation`. -- [ ] `vestige domains discover --min-cluster-size 20` produces strictly fewer or equal clusters than the default, and with larger per-cluster membership. -- [ ] Dashboard `/dashboard/domains` route renders all domains within 2 seconds on the seed corpus. -- [ ] Proposal UI flow (open pending, accept, confirmed in store) works end-to-end. -- [ ] Benchmarks meet targets (discover 10k p95 < 30s, auto-classify p99 < 5ms). - ---- - -## Rollback Notes - -- **Feature gate**: add `domain-classification` to `crates/vestige-core/Cargo.toml`'s `[features]`. When disabled, the `DomainClassifier` module is not compiled, the classification call in the ingest path is a no-op (`#[cfg]`-guarded), and cross-domain decay collapses to `1.0`. The CLI `domains` subcommand emits "domain classification is disabled in this build". -- **Revert strategy**: drop the two new tables `domains` (if created in Phase 1 is retained) or `domain_proposals` (Phase 4). A DOWN migration clears `memories.domains` and `memories.domain_scores`. Existing memories simply lose their domain assignments; all search and retrieval paths work unchanged because `domains = []` is the documented "unclassified" state. -- **Idempotency**: rerunning `discover` is always safe. Cluster numeric IDs may differ between runs, but the stable-ID match by centroid similarity preserves user-assigned labels. Do not persist cluster ids in client-side bookmarks; link via the user-assigned label. -- **Data-loss risk**: `apply_proposal` is a destructive operation (it deletes the old parent domain in a split or merges two). The dashboard's accept button double-confirms with a modal that shows the number of affected memories. - ---- - -## Open Implementation Questions - -Each question + candidates + RECOMMENDATION. - -### OQ1. Top-terms extraction: TF-IDF vs BM25 vs frequency? -- TF-IDF with smoothed IDF -- standard, cheap, good-enough. -- BM25 -- better for long-document discrimination, overkill for short memory contents. -- Raw frequency -- noisy; stop-words dominate. -**RECOMMENDATION**: TF-IDF with global IDF over the entire memory corpus (not just cluster members), recomputed once per `discover` call. Same tokenizer as the `dreams.rs::content_similarity` Jaccard for consistency. - -### OQ2. Proposal persistence: DB table vs in-memory with dashboard notification? -- DB table (`domain_proposals`) -- durable, surfaces across restarts, enables audit. -- In-memory only -- simpler, but loses proposals on server restart. -**RECOMMENDATION**: DB table. Proposals are rare (every 5th dream) and valuable user-facing artifacts; durability is mandatory. - -### OQ3. `hdbscan` crate: f32 vs f64 input, exact API surface? -- v0.10 historically takes `&[Vec]`; embeddings are `Vec`. -- Cost of converting f32 -> f64 at discovery time: `10k * 768 = 7.68M` f64 doubles ~ 60MB transient, acceptable. -**RECOMMENDATION**: verify v0.10's type signature at implementation time; if it requires f64, perform the conversion in `discover()` behind a single allocation. Document in module header. If the crate API diverged from the PRD snippet, fall back to the manual builder style (`HdbscanHyperParams::builder().min_cluster_size(n).min_samples(s).build()`). - -### OQ4. Stable domain IDs across discover re-runs? -- Option A: numeric IDs from HDBSCAN labels -- unstable, re-runs shuffle them. -- Option B: hash(top_terms) -- stable if top-terms stable, but top-terms drift. -- Option C (recommended): after computing new centroids, match each to the closest existing domain by centroid cosine; if similarity > 0.85, reuse the existing domain's `id` and `label`. Otherwise mint a fresh `id = "cluster_"`. -**RECOMMENDATION**: Option C. Preserves user-assigned labels across drift. Threshold 0.85 is config-tunable via `stable_id_threshold` if needed later. - -### OQ5. Context signal injection site: ingest handler vs embedder vs classifier? -- Embedder -- would alter embedding; signals are not about embedding quality. -- Ingest handler -- signals known there, but then `DomainClassifier` cannot be tested in isolation. -- Classifier as a `classify_with_boost(boost: Option<&HashMap>)` parameter -- pure, testable, composable. -**RECOMMENDATION**: classifier parameter. The cognitive engine constructs the boost map via `ContextSignals::gather_boost(&metadata, &domains)` and hands it to the classifier. Keeps the classifier stateless w.r.t. signals. - -### OQ6. Re-cluster proposal cadence: event-based (every Nth dream) vs time-based (weekly)? -- ADR resolution Q7: every Nth dream (N=5 default). -- Alternative: once per week regardless of dream cadence. -**RECOMMENDATION**: stick with every Nth dream. Users who dream rarely re-cluster rarely -- that matches the philosophy ("memory work triggers memory bookkeeping"). Note the alternative as future consideration; if users complain about never seeing proposals, add a time-based fallback. - -### OQ7. Minimum corpus size for first discover? -- PRD default: 150. -- Too low -> noisy initial clusters, proposals every dream. -- Too high -> user waits forever for domains to appear. -**RECOMMENDATION**: 150 as the default discovery gate; HDBSCAN's `min_cluster_size=10` will produce 0 clusters for < 100 memories, so the system gracefully produces no domains until the corpus is large enough. Test with `N=80, 150, 500` in `threshold_tunable.rs` to confirm sensible behavior. - -### OQ8. Cross-domain decay: strict no-overlap vs graded? -- Strict: `1.0` if any overlap, `cross_domain_decay` otherwise. -- Graded: `max(cross_domain_decay, |A intersect B| / max(|A|, |B|))`. -**RECOMMENDATION**: strict for Phase 4. Easier to reason about, easier to tune, easier to test. Graded is a marked future enhancement; file an issue if retrieval-quality metrics justify it. - -### OQ9. Classifier invocation from remote HTTP clients? -- In server mode, an agent posts `smart_ingest` -> server embeds -> server classifies. -- All the work stays server-side; MCP clients never do classification. -**RECOMMENDATION**: confirmed server-side-only. Document in the MCP tool schema that `smart_ingest` now returns `domains` and `domain_scores` in its response so clients can display the classification to the user. - -### OQ10. Where to store the dream-cycle counter? -- In-memory on `CognitiveEngine` -- lost on restart, miscounts cadence. -- New `system_state` singleton table. -**RECOMMENDATION**: `system_state` table. Survives restarts. Also useful for future metrics (total memories ever, total dreams ever). - -### OQ11. Scope of `reassign_all` after a proposal accept vs a normal discover? -- On discover --force (first-time), run `reassign_all` against all memories. -- On proposal accept (split / merge), run `reassign_all` only on affected memories (parent's members for split; both parents' members for merge) to avoid touching unrelated records. -**RECOMMENDATION**: scoped reassignment where possible; fall back to full `reassign_all` only on `discover --force` or when the set of domains has fundamentally changed. Reduces write amplification on large corpora. - -### OQ12. Proposal freshness? -- Multiple re-clusters could stack up pending proposals. -**RECOMMENDATION**: before inserting a new proposal, check for existing pending proposals with the same `kind + targets`; if present, bump `created_at` and `confidence` instead of creating a duplicate. Add a `confidence_history` array in the `payload` JSONB for audit. - ---- - -## Implementation Sequencing (suggested order) - -1. Land the `DomainClassifier` struct, `classify` / `classify_with_boost`, unit tests. (Day 1) -2. Add `compute_top_terms` + TF-IDF helper, tests. (Day 1) -3. Wire `discover` end-to-end against SQLite; `discover_seed_corpus` integration test. (Day 2) -4. Add `domain_proposals` table migrations + trait methods; both backends. (Day 2) -5. Implement `propose_changes` + `apply_proposal`; proposal unit tests. (Day 3) -6. Context signals module + tests. (Day 3) -7. Hook classifier into ingest path; `auto_classify_on_ingest` integration test. (Day 4) -8. Cross-domain decay in spreading activation; unit + integration tests. (Day 4) -9. Dream re-cluster hook + `system_state` counter; integration tests for drift-only behavior. (Day 5) -10. CLI subcommands. (Day 6) -11. REST endpoints. (Day 6) -12. SvelteKit dashboard routes + WebSocket event wiring. (Day 7-8) -13. Benchmarks + acceptance sweep on the 500-memory seed. (Day 9) - ---- - -## File Map (everything Phase 4 touches or creates) - -Creates: - -- `crates/vestige-core/src/neuroscience/domain_classifier.rs` -- `crates/vestige-core/src/neuroscience/context_signals.rs` -- `crates/vestige-core/migrations/postgres/00XX_domain_proposals.sql` -- `crates/vestige-core/migrations/sqlite/00XX_domain_proposals.sql` (or inline in `storage/migrations.rs`) -- `crates/vestige-mcp/src/api/domains.rs` (REST handlers) -- `apps/dashboard/src/routes/(app)/domains/+page.svelte` -- `apps/dashboard/src/routes/(app)/domains/[id]/+page.svelte` -- `apps/dashboard/src/routes/(app)/domains/proposals/+page.svelte` -- `apps/dashboard/src/lib/api/domains.ts` -- `tests/phase_4/Cargo.toml` -- `tests/phase_4/tests/*.rs` (per the Integration test list) -- `tests/phase_4/fixtures/seed_500.json` -- `tests/phase_4/support/fixtures.rs` - -Modifies: - -- `crates/vestige-core/Cargo.toml` -- add `hdbscan = "0.10"` under a new `domain-classification` feature. -- `crates/vestige-core/src/neuroscience/mod.rs` -- register new modules, re-exports. -- `crates/vestige-core/src/neuroscience/spreading_activation.rs` -- `cross_domain_decay` field in `ActivationConfig`, `domains` field on `ActivationNode`, decay math in `activate`. -- `crates/vestige-core/src/consolidation/phases.rs` -- `DreamReClusterHook`. -- `crates/vestige-core/src/advanced/dreams.rs` -- accept a hook callback from the orchestrator (if the orchestration is done at this level). -- `crates/vestige-core/src/storage/trait.rs` -- add proposal + system_state methods. -- `crates/vestige-core/src/storage/sqlite.rs` -- implement proposal + system_state methods + `all_embeddings_with_meta` if not already on the trait. -- `crates/vestige-core/src/storage/postgres.rs` (Phase 2) -- same. -- `crates/vestige-core/src/lib.rs` -- re-exports. -- `crates/vestige-core/src/cognitive.rs` (or equivalent ingest orchestrator) -- auto-classify injection. -- `crates/vestige-mcp/src/bin/cli.rs` -- `Domains` subcommand + dispatch. -- `crates/vestige-mcp/src/dashboard/mod.rs` -- wire new REST routes. -- `crates/vestige-mcp/src/dashboard/events.rs` -- new event variants. -- `crates/vestige-mcp/src/dashboard/handlers.rs` -- if legacy dashboard gets a domains panel (optional). -- `vestige.toml` config loader -- `[domains]` section + struct + defaults. -- Root `Cargo.toml` workspace members -- add `tests/phase_4`. - ---- - -## Risks - -- **HDBSCAN determinism**: HDBSCAN is deterministic given input order; sorting embeddings by memory id before feeding the clusterer guarantees reproducibility across runs -- do this in `discover()` and document it. -- **Embedding dimension drift**: Phase 1's `embedding_model` registry blocks writes from mismatched embedders. If `discover()` ever sees two dimensions, it bails with a clear error and points at `vestige migrate --reembed`. -- **Classification latency on ingest**: for users with thousands of domains (unlikely but possible), `classify` is O(n_domains * dim). 20 domains * 768 f32 = 15k flops per classification, trivial. Still, expose a `classify_budget_ms` config knob for paranoia. -- **Re-cluster proposal storms**: if the corpus is borderline-stable, small changes can produce conflicting proposals on consecutive dreams. Mitigation: OQ12 (dedup by target set, bump confidence instead of stacking). -- **Dashboard feature gap**: if the SvelteKit app lands with the domains route but the REST endpoints are not yet deployed, the route 404s. Mitigation: ship the REST endpoints in the same release; a feature flag on the client toggles the nav entry. - ---- - -## Non-Goals Reminder - -- No Phase 5 federation concerns in this plan. -- No cross-installation domain sync. -- No automatic accept of proposals, ever. -- No graded cross-domain decay; strict only. -- No ML-based domain label suggestion (top-terms are enough for v1). -- No editing individual memory memberships from the UI in this phase. diff --git a/docs/plans/local-dev-postgres-setup.md b/docs/plans/local-dev-postgres-setup.md deleted file mode 100644 index f863d48..0000000 --- a/docs/plans/local-dev-postgres-setup.md +++ /dev/null @@ -1,279 +0,0 @@ -# Local Dev Postgres Setup (container, hybrid approach) - -**Status**: Applied on this machine on 2026-05-27 (rootless podman, Postgres 18.4 + pgvector 0.8.2). -**Related**: docs/plans/0002-phase-2-postgres-backend.md, docs/adr/0002-phase-2-execution.md, docs/adr/0001-pluggable-storage-and-network-access.md - -Purpose: capture the minimum, repeatable steps to stand up a long-lived -Postgres 18 + pgvector instance on a local Linux dev box for Phase 2 -(`PgMemoryStore`) development, `sqlx prepare`, and manual migration -testing. This is a single-operator dev recipe, not a production runbook. - -ADR 0002 picked the **hybrid container** approach over a native install: -the `pgvector/pgvector:pg18` image ships pgvector pre-installed, matches -the image testcontainers will use in the Phase 2 test harness, and avoids -the AUR/build-from-source friction of native pgvector packaging on Arch. - ---- - -## Current state on this machine - -- Runtime: rootless `podman` 5.8.2 (Arch). `docker` 29.5.1 also installed but unused. -- Image: `docker.io/pgvector/pgvector:pg18` (PostgreSQL 18.4, pgvector 0.8.2). -- Container: `vestige-pg`, `--restart=always`, port `127.0.0.1:5432:5432`. -- Volume: named podman volume `vestige-pgdata`, mounted at - `/var/lib/postgresql/data` inside the container; `PGDATA` points at - `/var/lib/postgresql/data/pgdata` so the volume mount is non-empty at - init time (Postgres refuses to initdb into a non-empty directory). -- Listens on: `127.0.0.1:5432` only (port mapping is bound to loopback). -- Auth: `scram-sha-256` (image default for both local socket and host). - -### Database + role - -- Database: `vestige`, UTF8, owner `vestige`, `LC_COLLATE=C.UTF-8`, `LC_CTYPE=C.UTF-8`. -- Role: `vestige` with `LOGIN CREATEDB` (no superuser, no replication). -- Schema `public` re-owned to `vestige` with full default privileges on - future tables / sequences / functions. -- Extension: `vector` (pgvector 0.8.2) installed in the `vestige` - database by the superuser at setup time. - -Net effect: the `vestige` role can create, alter, drop, and grant freely -inside the `vestige` database -- enough for `sqlx::migrate!`, ad-hoc -schema work, and the full Phase 2 `MemoryStore` surface. It cannot create -extensions; the superuser handled `CREATE EXTENSION vector` already. - -### Passwords - -Two passwords live in the dev user's home, mode 600: - -- `~/.vestige_pg_superpw` -- the `postgres` superuser password inside the - container. Used for one-shot admin tasks (creating roles, installing - extensions, password rotation). Day-to-day app traffic does NOT use it. -- `~/.vestige_pg_pw` -- the `vestige` role password. This is the one the - Phase 2 backend, `sqlx prepare`, and ad-hoc `psql` invocations use. - -### Connection - -``` -postgresql://vestige:@127.0.0.1:5432/vestige -``` - -Recommended dev shell export (keep this OUT of the repo; use `.env` + -gitignore or a shell rc): - -```sh -export DATABASE_URL="postgresql://vestige:$(cat ~/.vestige_pg_pw)@127.0.0.1:5432/vestige" -``` - ---- - -## Reproduce from scratch - -On a fresh Linux box with `podman` installed and `python3` available: - -```sh -# 1. Pull the image -podman pull docker.io/pgvector/pgvector:pg18 - -# 2. Create a persistent named volume -podman volume create vestige-pgdata - -# 3. Generate the superuser password and stash it (mode 600) -SUPER_PW=$(python3 -c 'import secrets,string; a=string.ascii_letters+string.digits; print("".join(secrets.choice(a) for _ in range(32)))') -umask 077 -printf '%s' "$SUPER_PW" > ~/.vestige_pg_superpw -chmod 600 ~/.vestige_pg_superpw - -# 4. Start the container -podman run -d \ - --name vestige-pg \ - --restart=always \ - -p 127.0.0.1:5432:5432 \ - -e POSTGRES_PASSWORD="$SUPER_PW" \ - -e PGDATA=/var/lib/postgresql/data/pgdata \ - -v vestige-pgdata:/var/lib/postgresql/data \ - docker.io/pgvector/pgvector:pg18 - -unset SUPER_PW - -# 5. Wait for ready -until podman exec vestige-pg pg_isready -U postgres -h 127.0.0.1 >/dev/null 2>&1; do - sleep 1 -done - -# 6. Generate the vestige role password and stash it (mode 600) -VESTIGE_PW=$(python3 -c 'import secrets,string; a=string.ascii_letters+string.digits; print("".join(secrets.choice(a) for _ in range(32)))') -umask 077 -printf '%s' "$VESTIGE_PW" > ~/.vestige_pg_pw -chmod 600 ~/.vestige_pg_pw - -# 7. Create role + database + grants + extension (runs as superuser inside the container) -podman exec -i vestige-pg psql -U postgres -v ON_ERROR_STOP=1 < '[3,2,1]'::vector AS l2_distance;" -``` - ---- - -## Boot persistence (rootless podman) - -`--restart=always` keeps the container alive across podman daemon -restarts, but rootless podman containers do NOT auto-start on system -boot unless the dev user has lingering enabled: - -```sh -sudo loginctl enable-linger "$USER" -``` - -After that, the `podman-restart.service` user unit handles restart of -`--restart=always` containers when the user session starts at boot: - -```sh -systemctl --user enable --now podman-restart.service -``` - -Skip both if you prefer to start the cluster manually each session with -`podman start vestige-pg`. - ---- - -## Day-to-day operation - -```sh -# Status -podman ps --filter name=vestige-pg - -# Logs (follow) -podman logs -f vestige-pg - -# psql as the app role -PGPASSWORD="$(cat ~/.vestige_pg_pw)" psql -h 127.0.0.1 -U vestige -d vestige - -# psql as the superuser (for grants, extensions, role admin) -podman exec -it vestige-pg psql -U postgres - -# Stop / start -podman stop vestige-pg -podman start vestige-pg - -# Restart in place -podman restart vestige-pg -``` - ---- - -## Password rotation - -```sh -# Rotate the vestige role password -NEW_PW=$(python3 -c 'import secrets,string; a=string.ascii_letters+string.digits; print("".join(secrets.choice(a) for _ in range(32)))') -umask 077 -printf '%s' "$NEW_PW" > ~/.vestige_pg_pw -chmod 600 ~/.vestige_pg_pw -podman exec -i vestige-pg psql -U postgres -v ON_ERROR_STOP=1 \ - -c "ALTER ROLE vestige WITH PASSWORD '${NEW_PW}';" -unset NEW_PW - -# Rotate the superuser password (less common) -NEW_SUPER=$(python3 -c 'import secrets,string; a=string.ascii_letters+string.digits; print("".join(secrets.choice(a) for _ in range(32)))') -umask 077 -printf '%s' "$NEW_SUPER" > ~/.vestige_pg_superpw -chmod 600 ~/.vestige_pg_superpw -podman exec -i vestige-pg psql -U postgres -v ON_ERROR_STOP=1 \ - -c "ALTER ROLE postgres WITH PASSWORD '${NEW_SUPER}';" -unset NEW_SUPER -``` - -Then re-export `DATABASE_URL` in any live shells. - ---- - -## Backup and restore (dev-grade) - -`pg_dump` writes a plain-text SQL dump to host disk. For dev data this is -enough; production runbook lives in `0002i-runbook.md`. - -```sh -# Dump -PGPASSWORD="$(cat ~/.vestige_pg_pw)" pg_dump -h 127.0.0.1 -U vestige -d vestige \ - --format=plain --no-owner > vestige-$(date +%Y%m%d-%H%M%S).sql - -# Restore (drops + recreates) -podman exec -i vestige-pg psql -U postgres -v ON_ERROR_STOP=1 \ - -c 'DROP DATABASE IF EXISTS vestige;' \ - -c 'CREATE DATABASE vestige OWNER vestige ENCODING UTF8 TEMPLATE template0;' -PGPASSWORD="$(cat ~/.vestige_pg_pw)" psql -h 127.0.0.1 -U vestige -d vestige < vestige-DUMP.sql -``` - -The named volume `vestige-pgdata` persists outside the container; the -container can be `podman rm`'d and recreated without losing data, as -long as the volume stays in place. - ---- - -## Teardown - -Destroys the cluster and all data in it: - -```sh -podman stop vestige-pg -podman rm vestige-pg -podman volume rm vestige-pgdata -podman rmi docker.io/pgvector/pgvector:pg18 -rm -f ~/.vestige_pg_pw ~/.vestige_pg_superpw -``` - -`enable-linger` and the user systemd unit can be undone with -`sudo loginctl disable-linger "$USER"` and -`systemctl --user disable podman-restart.service` if you turned them on. - ---- - -## Notes for Phase 2 - -- `pgvector` is preinstalled in the image; the `CREATE EXTENSION vector` - in step 7 above makes it available inside the `vestige` DB. The - extension must be loaded BEFORE `sqlx::migrate!` runs the Phase 2 - migration that declares typed `Vector` columns, otherwise the - migration fails. -- Testcontainer-based Phase 2 integration tests use the same - `pgvector/pgvector:pg18` image and spin up fresh containers per run; - they are independent of this long-lived cluster. This cluster exists - for `sqlx prepare`, `cargo run -- migrate --to postgres`, and manual - poking. -- `sqlx prepare` needs `DATABASE_URL` pointed at this cluster with - `vestige` migrations already applied. Run from `crates/vestige-core/`. - ---- - -## Out of scope for this doc - -- TLS, client-cert auth, non-localhost access. Phase 3 exposes the - Vestige HTTP API over the network, not Postgres directly. -- PITR, WAL archiving, replication, PgBouncer, tuned `postgresql.conf`. - Defaults are fine for Phase 2 development. -- Native (non-container) Postgres install. The prior version of this - doc covered native Arch packaging; superseded by ADR 0002's hybrid - decision. -- Making this the canonical Vestige backend. By default Vestige still - uses SQLite; this cluster exists so the `postgres-backend` feature - can be built and tested locally. diff --git a/docs/prd/001-getting-centralized-vestige.md b/docs/prd/001-getting-centralized-vestige.md deleted file mode 100644 index 11af251..0000000 --- a/docs/prd/001-getting-centralized-vestige.md +++ /dev/null @@ -1,751 +0,0 @@ -# RFC: Pluggable Storage Backend + Network Access for Vestige - -**Status**: Draft / Discussion -**Author**: Jan -**Date**: 2026-02-26 -**Vestige version**: v2.x (current main) - -## Summary - -Add a pluggable storage backend trait to Vestige, enabling PostgreSQL (+pgvector) as an alternative to the current SQLite+FTS5+USearch stack. Simultaneously add HTTP MCP transport with API key authentication to enable centralized/remote deployment. - -This keeps the existing local-first SQLite mode fully intact while opening up a server deployment model. - -## Motivation - -Vestige currently runs as a local process per machine (MCP via stdio, SQLite in `~/.vestige/`). This works great for single-machine use but doesn't support: - -- **Multi-machine access**: Same memory brain from laptop, desktop, and server -- **Multi-agent access**: Multiple AI clients hitting one memory store concurrently -- **Future federation**: Syncing memory between decentralized nodes (e.g., MOS/Threefold grid) - -SQLite's single-writer model and lack of native network protocol make it unsuitable as a centralized server. PostgreSQL is a natural fit: built-in concurrency (MVCC), authentication, replication, and with `pgvector` + built-in FTS it collapses three separate storage layers into one. - -## Design - -### Storage Trait - -The core abstraction. All 29 cognitive modules interact with storage exclusively through this trait (or a small family of traits). - -```rust -use std::collections::HashMap; -use uuid::Uuid; - -/// Core memory record, backend-agnostic -#[derive(Debug, Clone)] -pub struct MemoryRecord { - pub id: Uuid, - pub domains: Vec, // [] = unclassified, ["dev"], ["dev", "infra"], etc. - pub domain_scores: HashMap, // raw similarities: {"dev": 0.82, "infra": 0.71} - pub content: String, - pub node_type: String, - pub tags: Vec, - pub embedding: Option>, // dimensionality is runtime config - pub created_at: chrono::DateTime, - pub updated_at: chrono::DateTime, - pub metadata: serde_json::Value, -} - -/// FSRS scheduling state, stored alongside each memory -#[derive(Debug, Clone)] -pub struct SchedulingState { - pub memory_id: Uuid, - pub stability: f64, - pub difficulty: f64, - pub retrievability: f64, - pub last_review: Option>, - pub next_review: Option>, - pub reps: u32, - pub lapses: u32, -} - -/// Hybrid search request -#[derive(Debug, Clone)] -pub struct SearchQuery { - pub domains: Option>, // None = search all domains - pub text: Option, // FTS query - pub embedding: Option>, // vector similarity - pub tags: Option>, // tag filter - pub node_types: Option>, - pub limit: usize, - pub min_retrievability: Option, // filter by FSRS state -} - -#[derive(Debug, Clone)] -pub struct SearchResult { - pub record: MemoryRecord, - pub score: f64, // combined/fused score - pub fts_score: Option, - pub vector_score: Option, -} - -/// Connection/edge between memories (for spreading activation) -#[derive(Debug, Clone)] -pub struct MemoryEdge { - pub source_id: Uuid, - pub target_id: Uuid, - pub edge_type: String, - pub weight: f64, - pub created_at: chrono::DateTime, -} - -/// Main storage trait — one impl per backend -/// trait_variant generates a Send-bound `MemoryStore` alias, -/// enabling Arc without manual boxing. -#[trait_variant::make(MemoryStore: Send)] -pub trait LocalMemoryStore: Sync + 'static { - // --- Lifecycle --- - async fn init(&self) -> Result<()>; - async fn health_check(&self) -> Result; - - // --- CRUD --- - async fn insert(&self, record: &MemoryRecord) -> Result; - async fn get(&self, id: Uuid) -> Result>; - async fn update(&self, record: &MemoryRecord) -> Result<()>; - async fn delete(&self, id: Uuid) -> Result<()>; - - // --- Search --- - async fn search(&self, query: &SearchQuery) -> Result>; - async fn fts_search(&self, text: &str, limit: usize) -> Result>; - async fn vector_search(&self, embedding: &[f32], limit: usize) -> Result>; - - // --- FSRS Scheduling --- - async fn get_scheduling(&self, memory_id: Uuid) -> Result>; - async fn update_scheduling(&self, state: &SchedulingState) -> Result<()>; - async fn get_due_memories(&self, before: chrono::DateTime, limit: usize) -> Result>; - - // --- Graph (spreading activation) --- - async fn add_edge(&self, edge: &MemoryEdge) -> Result<()>; - async fn get_edges(&self, node_id: Uuid, edge_type: Option<&str>) -> Result>; - async fn remove_edge(&self, source: Uuid, target: Uuid) -> Result<()>; - async fn get_neighbors(&self, node_id: Uuid, depth: usize) -> Result>; - - // --- Bulk / Maintenance --- - async fn count(&self) -> Result; - async fn get_stats(&self) -> Result; - async fn vacuum(&self) -> Result<()>; -} -``` - -**Design notes:** - -- `trait_variant::make` generates a `MemoryStore` trait alias with `Send`-bound futures, allowing `Arc` for runtime backend selection. `LocalMemoryStore` is the base (usable in single-threaded contexts), `MemoryStore` is the Send variant for Axum/tokio. -- `embedding: Option>` — dimensions determined at runtime by the configured fastembed model. The backend stores whatever it gets. -- The trait is intentionally flat. The cognitive modules (FSRS-6, spreading activation, synaptic tagging, prediction error gating, etc.) sit *above* this trait and don't need to know about the backend. -- `search()` does hybrid RRF fusion at the backend level — both SQLite and Postgres implementations handle this internally. - -### Backend: SQLite (existing, refactored) - -Wraps the current implementation behind the trait: - -``` -SqliteMemoryStore -├── rusqlite connection pool (r2d2 or deadpool) -├── FTS5 virtual table (keyword search) -├── USearch HNSW index (vector search, behind RwLock) -└── WAL mode + busy timeout for concurrent readers -``` - -No behavioral changes — just the trait boundary. - -### Backend: PostgreSQL (new) - -``` -PgMemoryStore -├── sqlx::PgPool (connection pool, compile-time checked queries) -├── tsvector + GIN index (keyword search) -├── pgvector + HNSW index (vector search) -└── Standard PostgreSQL MVCC concurrency -``` - -**Schema sketch:** - -```sql -CREATE EXTENSION IF NOT EXISTS vector; - --- Domain registry — populated by clustering, not by user -CREATE TABLE domains ( - id TEXT PRIMARY KEY, -- auto-generated or user-named - label TEXT NOT NULL, -- human label (suggested or user-provided) - centroid vector, -- mean embedding of domain members - top_terms TEXT[] NOT NULL DEFAULT '{}', -- top keywords for display - memory_count INTEGER NOT NULL DEFAULT 0, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - metadata JSONB NOT NULL DEFAULT '{}' -); - -CREATE TABLE memories ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - domains TEXT[] NOT NULL DEFAULT '{}', -- [] = unclassified - domain_scores JSONB NOT NULL DEFAULT '{}', -- {"dev": 0.82, "infra": 0.71} raw similarities - content TEXT NOT NULL, - node_type TEXT NOT NULL DEFAULT 'general', - tags TEXT[] NOT NULL DEFAULT '{}', - embedding vector, -- dimension set at table creation or unconstrained - metadata JSONB NOT NULL DEFAULT '{}', - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - - -- FTS: auto-maintained tsvector column - search_vec TSVECTOR GENERATED ALWAYS AS ( - setweight(to_tsvector('english', content), 'A') || - setweight(to_tsvector('english', coalesce(node_type, '')), 'B') || - setweight(array_to_tsvector(tags), 'C') - ) STORED -); - --- FTS index -CREATE INDEX idx_memories_fts ON memories USING GIN (search_vec); - --- Vector similarity (HNSW) -CREATE INDEX idx_memories_embedding ON memories - USING hnsw (embedding vector_cosine_ops) - WITH (m = 16, ef_construction = 64); - --- Common filters -CREATE INDEX idx_memories_domains ON memories USING GIN (domains); -CREATE INDEX idx_memories_node_type ON memories (node_type); -CREATE INDEX idx_memories_tags ON memories USING GIN (tags); -CREATE INDEX idx_memories_created ON memories (created_at); - --- FSRS scheduling state -CREATE TABLE scheduling ( - memory_id UUID PRIMARY KEY REFERENCES memories(id) ON DELETE CASCADE, - stability DOUBLE PRECISION NOT NULL DEFAULT 0.0, - difficulty DOUBLE PRECISION NOT NULL DEFAULT 0.0, - retrievability DOUBLE PRECISION NOT NULL DEFAULT 1.0, - last_review TIMESTAMPTZ, - next_review TIMESTAMPTZ, - reps INTEGER NOT NULL DEFAULT 0, - lapses INTEGER NOT NULL DEFAULT 0 -); - -CREATE INDEX idx_scheduling_next ON scheduling (next_review); - --- Graph edges (spreading activation) --- Edges can cross domain boundaries — spreading activation respects --- domain filters when provided, traverses freely when searching all domains. -CREATE TABLE edges ( - source_id UUID NOT NULL REFERENCES memories(id) ON DELETE CASCADE, - target_id UUID NOT NULL REFERENCES memories(id) ON DELETE CASCADE, - edge_type TEXT NOT NULL DEFAULT 'related', - weight DOUBLE PRECISION NOT NULL DEFAULT 1.0, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - PRIMARY KEY (source_id, target_id, edge_type) -); - -CREATE INDEX idx_edges_target ON edges (target_id); - --- API keys -CREATE TABLE api_keys ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - key_hash TEXT NOT NULL UNIQUE, -- blake3 - label TEXT NOT NULL, - scopes TEXT[] NOT NULL DEFAULT '{read,write}', - domain_filter TEXT[] NOT NULL DEFAULT '{}', -- {} = access all domains - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - last_used TIMESTAMPTZ, - active BOOLEAN NOT NULL DEFAULT true -); -``` - -**Hybrid search in SQL:** - -```sql --- RRF (Reciprocal Rank Fusion) combining FTS + vector --- $1 = query text, $2 = embedding, $3 = limit, $4 = domain filter (NULL for all) -WITH fts AS ( - SELECT id, ts_rank_cd(search_vec, websearch_to_tsquery('english', $1)) AS score, - ROW_NUMBER() OVER (ORDER BY ts_rank_cd(search_vec, websearch_to_tsquery('english', $1)) DESC) AS rank - FROM memories - WHERE search_vec @@ websearch_to_tsquery('english', $1) - AND ($4::text[] IS NULL OR domains && $4) -- array overlap: any match - LIMIT 50 -), -vec AS ( - SELECT id, 1 - (embedding <=> $2::vector) AS score, - ROW_NUMBER() OVER (ORDER BY embedding <=> $2::vector) AS rank - FROM memories - WHERE embedding IS NOT NULL - AND ($4::text[] IS NULL OR domains && $4) - LIMIT 50 -) -SELECT COALESCE(f.id, v.id) AS id, - COALESCE(1.0 / (60 + f.rank), 0) + COALESCE(1.0 / (60 + v.rank), 0) AS rrf_score, - f.score AS fts_score, - v.score AS vector_score -FROM fts f FULL OUTER JOIN vec v ON f.id = v.id -ORDER BY rrf_score DESC -LIMIT $3; -``` - -### Embedding Configuration - -The embedding layer stays external to the storage backend. fastembed runs locally and produces vectors that get passed into `MemoryRecord.embedding`. - -```toml -# vestige.toml -[embeddings] -provider = "fastembed" # only local for now -model = "BAAI/bge-base-en-v1.5" # 768 dimensions -# model = "BAAI/bge-large-en-v1.5" # 1024 dimensions -# model = "BAAI/bge-small-en-v1.5" # 384 dimensions - -[storage] -backend = "postgres" # or "sqlite" - -[storage.sqlite] -path = "~/.vestige/vestige.db" - -[storage.postgres] -url = "postgresql://vestige:secret@localhost:5432/vestige" -max_connections = 10 -``` - -On init, the backend reads the embedding dimension from the first stored vector (or from config) and validates consistency. - -For pgvector: you can either create the column as `vector(768)` (fixed, faster) or unconstrained `vector` (flexible, slightly slower). Recommendation: fixed dimension derived from config, with a migration path if the model changes. - -### Emergent Domain Model - -Instead of user-defined tenants, domains emerge automatically from the data via clustering. The user never has to decide where a memory belongs — the system figures it out. - -#### Pipeline - -``` -Phase 1: Accumulate (cold start, 0 → N memories) -│ All memories stored with domains = [] (unclassified) -│ No classification overhead, just embed and store -│ Threshold N is configurable, default ~150 memories -│ -Phase 2: Discover (triggered once at threshold, or manually) -│ Run HDBSCAN on all embeddings: -│ - min_cluster_size: ~10 -│ - min_samples: ~5 -│ - No eps parameter needed (unlike DBSCAN) -│ - Automatically determines number of clusters -│ - Handles variable-density clusters -│ - Border points between clusters flagged naturally -│ -│ For each cluster, extract: -│ - Centroid (mean embedding) -│ - Top terms (TF-IDF or frequency over cluster members) -│ - Suggested label from top terms -│ -│ Present to user (via dashboard or CLI): -│ "I found 3 natural groupings in your memories: -│ ● cluster_0 (47 memories): BGP, SONiC, VLAN, FRR, peering... -│ ● cluster_1 (31 memories): solar, kWh, battery, pool, ESPHome... -│ ● cluster_2 (22 memories): Rust, trait, async, zinit, tokio..." -│ -│ User can: -│ - Name them: cluster_0 → "infra", cluster_1 → "home", cluster_2 → "dev" -│ - Accept suggested names -│ - Merge clusters -│ - Do nothing (auto-names stick) -│ -Phase 3: Soft-assign all existing memories -│ Now that centroids exist, re-score every memory (including -│ those from discovery) against all centroids. -│ This replaces HDBSCAN's hard labels with continuous scores: -│ -│ For each memory: -│ similarities = [(domain, cosine_sim(embedding, centroid)) for each domain] -│ domains = [id for (id, score) in similarities if score >= threshold] -│ -│ Memories in overlap zones get multiple domains. -│ Memories far from all centroids stay unclassified. -│ -Phase 4: Classify (ongoing, after discovery) -│ New memory ingested: -│ 1. Compute embedding -│ 2. Compute similarity to ALL domain centroids -│ 3. Store raw scores in domain_scores JSONB -│ 4. Threshold into domains[] array -│ 5. Update domain centroids incrementally (running mean) -│ -│ Context signals as soft priors: -│ - Git repo / IDE metadata → boost similarity to code-related domains -│ - No workspace context → slight boost toward non-technical domains -│ - These shift the score, never override the embedding distance -│ -Phase 5: Re-cluster (periodic, during dream consolidation) - Re-run HDBSCAN on all embeddings including new ones - Detect: - - New clusters forming from previously unclassified memories - - Existing clusters splitting (domain grew too broad) - - Clusters merging (domains that were artificially separate) - Propose changes to user: - "Your 'dev' domain may have split into two groups: - - systems (zinit, MOS, containers, VMs) — 34 memories - - networking (BGP, SONiC, VLANs, MLAG) — 28 memories - Split them? [yes / no / later]" - Re-run soft assignment on all memories after structural changes - Centroid vectors are updated regardless -``` - -#### Domain Storage - -```rust -#[derive(Debug, Clone)] -pub struct Domain { - pub id: String, - pub label: String, - pub centroid: Vec, - pub top_terms: Vec, - pub memory_count: usize, - pub created_at: chrono::DateTime, -} -``` - -Added to the `MemoryStore` trait: - -```rust - // --- Domains --- - async fn list_domains(&self) -> Result>; - async fn get_domain(&self, id: &str) -> Result>; - async fn upsert_domain(&self, domain: &Domain) -> Result<()>; - async fn delete_domain(&self, id: &str) -> Result<()>; - async fn classify(&self, embedding: &[f32]) -> Result>; - // Returns [(domain_id, similarity)] sorted by similarity desc. - // Caller decides threshold for assignment. -``` - -#### Classification Module - -A new cognitive module alongside FSRS, spreading activation, etc.: - -```rust -pub struct DomainClassifier { - /// Similarity threshold — domains scoring above this are assigned - pub assign_threshold: f64, // default: 0.65 - /// Minimum memories before running initial discovery - pub discovery_threshold: usize, // default: 150 - /// How often to re-cluster (in dream consolidation passes) - pub recluster_interval: usize, // default: every 5th consolidation - /// HDBSCAN min_cluster_size - pub min_cluster_size: usize, // default: 10 -} - -/// Raw classification result — all scores, before thresholding -#[derive(Debug, Clone)] -pub struct ClassificationResult { - /// Similarity to every known domain centroid - pub scores: HashMap, // {"dev": 0.82, "infra": 0.71, "home": 0.34} - /// Domains above assign_threshold - pub domains: Vec, // ["dev", "infra"] -} - -impl DomainClassifier { - /// Score a memory against all domain centroids. - /// Returns raw scores AND thresholded domain list. - pub fn classify( - &self, - embedding: &[f32], - domains: &[Domain], - ) -> ClassificationResult { - if domains.is_empty() { - return ClassificationResult { - scores: HashMap::new(), - domains: vec![], // still in accumulation phase - }; - } - - let scores: HashMap = domains.iter() - .map(|d| (d.id.clone(), cosine_similarity(embedding, &d.centroid))) - .collect(); - - let assigned: Vec = scores.iter() - .filter(|(_, &s)| s >= self.assign_threshold) - .map(|(id, _)| id.clone()) - .collect(); - - ClassificationResult { scores, domains: assigned } - } - - /// Soft-assign all existing memories after discovery or re-clustering. - /// Returns number of memories whose domains changed. - pub async fn reassign_all( - &self, - store: &dyn MemoryStore, - domains: &[Domain], - ) -> Result { - // Load all memories, re-score, update domains + domain_scores - // Batched to avoid loading everything into memory at once - todo!() - } -} -``` - -**Key distinction from the previous design:** there's no "closest wins" or "margin" logic. Every domain gets a score, and *all* domains above threshold are assigned. A memory about "deploying zinit containers via BGP-routed network" might score 0.78 on "dev" and 0.72 on "infra" — it gets both. A memory about "solar panel output today" scores 0.85 on "home" and 0.31 on everything else — it only gets "home". - -The raw `domain_scores` are always stored, so you (or the dashboard) can see *why* a memory was classified the way it was, and the threshold can be adjusted retroactively without re-computing embeddings. - -#### Search Behavior - -- **Default (no domain filter)**: searches all memories across all domains -- **Domain-scoped**: `domains: Some(vec!["dev"])` — only memories tagged with `dev` -- **Multi-domain**: `domains: Some(vec!["dev", "infra"])` — memories in either -- **MCP clients can set `X-Vestige-Domain` header** for default scoping, but the system works fine without it - -#### HDBSCAN Implementation - -HDBSCAN (Hierarchical DBSCAN) over the embedding vectors. Advantages over plain DBSCAN: - -- **No `eps` parameter** — the hardest thing to tune in DBSCAN. HDBSCAN determines density thresholds from the data hierarchy. -- **Variable-density clusters** — a tight cluster of networking memories and a spread-out cluster of personal memories are both detected correctly. -- **Border points** — memories between clusters are identified as low-confidence members, which aligns perfectly with soft assignment. - -Implementation: the `hdbscan` crate in Rust. Load all embeddings into memory (at 768d × f32 × 10k memories ≈ 30MB — fine), cluster, compute centroids, soft-assign all memories against the centroids. - -```rust -use hdbscan::{Center, Hdbscan}; - -fn discover_domains( - embeddings: &[Vec], - min_cluster_size: usize, -) -> (Vec>, Vec>) { // (cluster → member indices, centroids) - let clusterer = Hdbscan::default(embeddings); - let labels = clusterer.cluster().unwrap(); - let centroids = clusterer.calc_centers(Center::Centroid, &labels).unwrap(); - - // Group indices by label, ignoring noise (-1) - let mut clusters: HashMap> = HashMap::new(); - for (i, &label) in labels.iter().enumerate() { - if label >= 0 { - clusters.entry(label).or_default().push(i); - } - } - (clusters.into_values().collect(), centroids) -} -``` - -After HDBSCAN produces hard clusters, the soft-assignment pass (Phase 3) immediately re-scores all memories — including the ones HDBSCAN assigned — against the computed centroids. So HDBSCAN's hard labels are only used to *define* the centroids. The actual domain assignments always come from the continuous similarity scores. - -This works identically for both SQLite and Postgres backends — clustering runs in Rust application code, results are written back to the storage layer. - -### Network Transport - -#### MCP over Streamable HTTP - -Extend the existing Axum server: - -```rust -// Alongside existing dashboard routes -let app = Router::new() - // Existing dashboard - .route("/api/health", get(health_handler)) - .route("/dashboard/*path", get(dashboard_handler)) - // New: MCP over HTTP - .route("/mcp", post(mcp_handler).get(mcp_sse_handler)) - // New: REST API - // X-Vestige-Domain header optionally scopes to a domain - .route("/api/v1/memories", post(create_memory).get(list_memories)) - .route("/api/v1/memories/:id", get(get_memory).put(update_memory).delete(delete_memory)) - .route("/api/v1/search", post(search_memories)) - .route("/api/v1/consolidate", post(trigger_consolidation)) - .route("/api/v1/stats", get(get_stats)) - .route("/api/v1/domains", get(list_domains)) - .route("/api/v1/domains/discover", post(trigger_discovery)) - .route("/api/v1/domains/:id", put(rename_domain).delete(merge_domain)) - // Auth on everything except health - .layer(middleware::from_fn(api_key_auth)); -``` - -#### Auth Middleware - -```rust -async fn api_key_auth( - State(store): State>, - request: axum::extract::Request, - next: middleware::Next, -) -> Result { - // Skip auth for health endpoint - if request.uri().path() == "/api/health" { - return Ok(next.run(request).await); - } - - let key = request.headers() - .get("Authorization") - .and_then(|v| v.to_str().ok()) - .and_then(|v| v.strip_prefix("Bearer ")) - .or_else(|| request.headers() - .get("X-API-Key") - .and_then(|v| v.to_str().ok())); - - match key { - Some(k) if verify_api_key(store.as_ref(), k).await => { - Ok(next.run(request).await) - } - _ => Err(StatusCode::UNAUTHORIZED), - } -} -``` - -#### Client Configuration - -```json -// Claude Desktop / Claude Code — single key, all domains -{ - "mcpServers": { - "vestige": { - "url": "http://vestige.local:3927/mcp", - "headers": { - "Authorization": "Bearer vst_a1b2c3..." - } - } - } -} -``` - -No domain header needed — searches all domains by default. The MCP tools include an optional `domain` parameter for scoped queries if the LLM or user wants to narrow down. - -Alternatively, scope a connection to a specific domain: - -```json -// Domain-scoped connection (e.g., for a home automation agent) -{ - "mcpServers": { - "vestige-home": { - "url": "http://vestige.local:3927/mcp", - "headers": { - "Authorization": "Bearer vst_e5f6g7...", - "X-Vestige-Domain": "home" - } - } - } -} -``` - -### Server Configuration - -```toml -# vestige.toml — full example for server mode -[server] -bind = "0.0.0.0:3927" # or mycelium IPv6 address -# tls_cert = "/path/to/cert.pem" # optional -# tls_key = "/path/to/key.pem" - -[auth] -enabled = true -# If false, no key required (local-only mode) - -[storage] -backend = "postgres" - -[storage.postgres] -url = "postgresql://vestige:secret@localhost:5432/vestige" -max_connections = 10 - -[embeddings] -provider = "fastembed" -model = "BAAI/bge-base-en-v1.5" -``` - -### CLI Extensions - -```bash -# Domain management (mostly automatic, but user can inspect/rename) -vestige domains list -# → dev Development (auto) memories: 87 top: Rust, trait, async, tokio -# → infra Infrastructure (auto) memories: 47 top: BGP, SONiC, VLAN, FRR -# → home Home (auto) memories: 31 top: solar, kWh, pool, ESPHome -# → (unclassified) memories: 12 - -vestige domains rename cluster_0 infra --label "Infrastructure" -vestige domains merge home personal --into home -vestige domains discover --force # re-run HDBSCAN now - -# Key management -vestige keys create --label "macbook" -# → Created key: vst_a1b2c3d4... (store this, shown once) - -vestige keys create --label "home-assistant" --scopes read --domains home -# → Created key: vst_e5f6g7h8... (read-only, home domain only) - -vestige keys list -# → macbook vst_a1b2... scopes: [read,write] domains: [all] -# → home-assistant vst_e5f6... scopes: [read] domains: [home] - -vestige keys revoke vst_a1b2c3d4... - -# Migration -vestige migrate --from sqlite --to postgres \ - --sqlite-path ~/.vestige/vestige.db \ - --postgres-url postgresql://localhost/vestige -``` - -## Implementation Plan - -### Phase 1: Storage Trait Extraction -- Define the `MemoryStore` trait (including domain methods) -- Refactor current SQLite code to implement it -- Add `domains TEXT[]` column to existing SQLite schema -- Verify all 29 modules work through the trait (no direct SQLite access) -- **No behavioral changes** — all memories start as unclassified - -### Phase 2: PostgreSQL Backend -- Implement `PgMemoryStore` -- Schema migrations (sqlx or refinery) -- `vestige migrate` command for SQLite → Postgres -- Config file support for backend selection - -### Phase 3: Network Access -- MCP Streamable HTTP endpoint on existing Axum server -- API key auth middleware + CLI management -- REST API endpoints -- Feature flags for stdio vs HTTP mode - -### Phase 4: Emergent Domain Classification -- `DomainClassifier` cognitive module -- HDBSCAN clustering via `hdbscan` crate (runs on both backends) -- Soft assignment pass: score all memories against centroids, threshold into domains -- `domain_scores` JSONB stored per memory for transparency / retroactive re-thresholding -- Domain discovery CLI and dashboard UI -- Auto-classification on ingest (once domains exist) -- Re-clustering during dream consolidation passes -- Domain management CLI (rename, merge, inspect) - -### Phase 5: Federation (future) -- Node discovery via Mycelium / mDNS -- Memory sync protocol (UUID-based, last-write-wins) -- Possibly Iroh for content-addressed replication -- FSRS state merge (review history append, not overwrite) - -## Crate Dependencies (new) - -```toml -# Phase 1 — trait abstraction -trait-variant = "0.1" - -# Phase 2 — Postgres -sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "uuid", "chrono", "json"] } -pgvector = "0.4" # sqlx integration for vector type - -# Phase 3 — Auth -blake3 = "1" # key hashing -rand = "0.8" # key generation - -# Phase 4 — Domain clustering -hdbscan = "0.10" # HDBSCAN — no eps tuning, variable density, built-in centroid calc -``` - -## Open Questions - -1. **Trait granularity**: One big `MemoryStore` trait or split into `MemoryStore + SchedulingStore + GraphStore + DomainStore`? Splitting is cleaner but means more `dyn` parameters threading through handlers. - -2. **Embedding on insert**: Should the storage backend call fastembed, or should the caller always provide the embedding? Current design says caller provides it, keeping the backend pure storage. But this means every client needs fastembed locally even if the DB is remote. For the server model, having the server compute embeddings makes more sense. - -3. **pgvector dimension**: Fixed (e.g., `vector(768)`) or unconstrained (`vector`)? Fixed is faster for HNSW but requires migration if model changes. - -4. **Sync conflict resolution for federation**: LWW per-UUID is simple but lossy. CRDTs would be more correct but massively more complex. For FSRS state specifically, merging review event logs would be ideal. - -5. **Dashboard auth**: The 3D dashboard currently runs unauthenticated on localhost. With remote access, it needs the same auth. Should it use the same API keys or have a separate session/cookie mechanism? - -6. **HDBSCAN `min_cluster_size`**: The main tuning knob. Too small → noisy micro-clusters. Too large → distinct topics get merged. Default of 10 should work for most cases, but may need a manual override or auto-sweep (run with several values, pick the one with best silhouette score). - -7. **Domain drift**: Over time, the character of a domain changes. How aggressively should re-clustering reshape existing domains? Conservative (only propose splits/merges, never auto-apply) vs. aggressive (auto-reassign memories whose scores drifted below threshold)? - -8. **Spreading activation across domains**: When searching within a single domain, should graph edges that cross into other domains be followed? Probably yes for recall quality, but with decaying weight as you cross boundaries. - -9. **Threshold tuning**: The `assign_threshold` (0.65 default) determines how many memories are multi-domain vs single-domain vs unclassified. Too low → everything is multi-domain (useless). Too high → too many unclassified. Could be auto-tuned per dataset by targeting a specific unclassified ratio (e.g., "keep fewer than 10% unclassified"). diff --git a/glama.json b/glama.json deleted file mode 100644 index ecca9c4..0000000 --- a/glama.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "https://glama.ai/mcp/schemas/server.json", - "maintainers": [ - "samvallad33" - ] -} diff --git a/hooks/load-all-memory.sh b/hooks/load-all-memory.sh index e9501c0..f21cff6 100755 --- a/hooks/load-all-memory.sh +++ b/hooks/load-all-memory.sh @@ -1,7 +1,7 @@ #!/bin/bash # Load ALL memory MD files on every UserPromptSubmit. -# This legacy opt-in hook cats every file in the memory directory into prompt -# context. It is intentionally not enabled by default. +# Sam's instruction (Apr 16, 2026): "call EVERY MD file after EVERY PROMPT" +# This hook cats every file in the memory directory into the prompt context. # Resolve per-user Claude Code project memory dir from $HOME. # Claude Code encodes home path as `-Users-`; allow override via env. @@ -16,7 +16,8 @@ if [ ! -d "$MEM_DIR" ]; then fi echo "═══════════════════════════════════════════════════════════════" -echo "[FULL MEMORY DUMP — EVERY FILE LOADED]" +echo "[FULL MEMORY DUMP — EVERY FILE LOADED PER SAM'S INSTRUCTION]" +echo "Sam said: 'call EVERY MD file after EVERY PROMPT' (Apr 16, 2026)" echo "═══════════════════════════════════════════════════════════════" echo "" diff --git a/hooks/sanhedrin-local.py b/hooks/sanhedrin-local.py index f9b68c3..677ba60 100755 --- a/hooks/sanhedrin-local.py +++ b/hooks/sanhedrin-local.py @@ -15,25 +15,12 @@ # the Cognitive Sandwich on infra errors). The wrapping sanhedrin.sh maps # "yes" to exit 0, so this preserves existing fail-open semantics. -from __future__ import annotations - import json import os import re import sys -import unicodedata import urllib.error -import urllib.parse import urllib.request -from dataclasses import asdict, dataclass, field, replace -from pathlib import Path -from typing import Any - -sys.path.insert(0, str(Path(__file__).resolve().parent)) -try: - import sanhedrin_core -except Exception: - sanhedrin_core = None def env_int(name: str, default: int) -> int: @@ -51,7 +38,7 @@ VESTIGE_BASE_URL = ( SANHEDRIN_ENDPOINT = ( os.environ.get("VESTIGE_SANHEDRIN_ENDPOINT") or os.environ.get("MLX_ENDPOINT") - or "" + or "http://127.0.0.1:8080/v1/chat/completions" ) VESTIGE_ENDPOINT = ( os.environ.get("VESTIGE_DEEP_REFERENCE_ENDPOINT") @@ -63,27 +50,17 @@ VESTIGE_HEALTH = ( MODEL = ( os.environ.get("VESTIGE_SANHEDRIN_MODEL") or os.environ.get("VESTIGE_SANDWICH_MODEL") - or "" + or "mlx-community/Qwen3.6-35B-A3B-4bit" ) SANHEDRIN_TIMEOUT = env_int("VESTIGE_SANHEDRIN_TIMEOUT", env_int("MLX_TIMEOUT", 45)) VESTIGE_TIMEOUT = env_int("VESTIGE_TIMEOUT", 5) -SANHEDRIN_BACKEND = (os.environ.get("VESTIGE_SANHEDRIN_BACKEND") or "").strip().lower() -THINK_RE = re.compile( - r"<(?:think|thinking|reasoning)>.*?", - re.DOTALL | re.IGNORECASE, -) +THINK_RE = re.compile(r".*?", re.DOTALL | re.IGNORECASE) def post_json(url: str, body: dict, timeout: int): - if not url: - return None data = json.dumps(body).encode("utf-8") - headers = {"Content-Type": "application/json"} - api_key = os.environ.get("VESTIGE_SANHEDRIN_API_KEY") - if api_key and same_endpoint_origin(url, SANHEDRIN_ENDPOINT): - headers["Authorization"] = f"Bearer {api_key}" req = urllib.request.Request( - url, data=data, headers=headers + url, data=data, headers={"Content-Type": "application/json"} ) try: with urllib.request.urlopen(req, timeout=timeout) as r: @@ -92,267 +69,8 @@ def post_json(url: str, body: dict, timeout: int): return None -def same_endpoint_origin(url: str, endpoint: str) -> bool: - try: - target = urllib.parse.urlsplit(url) - expected = urllib.parse.urlsplit(endpoint) - except ValueError: - return False - return ( - target.scheme == expected.scheme - and target.netloc == expected.netloc - and target.path == expected.path - ) - - -def use_backend_extensions() -> bool: - if SANHEDRIN_BACKEND in {"mlx", "vllm"}: - return True - if SANHEDRIN_BACKEND in {"openai", "ollama", "llama.cpp", "llamacpp", "litellm"}: - return False - return MODEL.startswith("mlx-community/") - - -def sanhedrin_model_configured() -> bool: - return bool(SANHEDRIN_ENDPOINT and MODEL) - - -def sanhedrin_body( - messages: list[dict[str, str]], - max_tokens: int, - stop: list[str] | None = None, -) -> dict[str, Any]: - body: dict[str, Any] = { - "model": MODEL, - "messages": messages, - "max_tokens": max_tokens, - "temperature": 0.0, - "top_p": 1.0, - "stream": False, - } - if stop: - body["stop"] = stop - if use_backend_extensions(): - body["top_k"] = 1 - body["seed"] = 42 - body["chat_template_kwargs"] = {"enable_thinking": False} - return body - - TRUST_FLOOR = 0.55 # filter out low-trust memories that drive false-positive vetoes -CLAIM_MODE_ENV = "VESTIGE_SANHEDRIN_CLAIM_MODE" -OUTPUT_ENV = "VESTIGE_SANHEDRIN_OUTPUT" -STAGE_FILE_ENV = "VESTIGE_SANHEDRIN_STAGE_FILE" -MAX_CLAIMS = env_int("VESTIGE_SANHEDRIN_MAX_CLAIMS", 8) -MAX_CLAIM_CHARS = env_int("VESTIGE_SANHEDRIN_MAX_CLAIM_CHARS", 500) -MAX_EVIDENCE_CHARS = env_int("VESTIGE_SANHEDRIN_MAX_EVIDENCE_CHARS", 420) - -CLAIM_CLASSES = { - "TECHNICAL", - "BIOGRAPHICAL", - "FINANCIAL", - "ACHIEVEMENT", - "TIMELINE", - "QUANTITATIVE", - "ATTRIBUTION", - "CAUSAL", - "COMPARATIVE", - "EXISTENTIAL", - "VAGUE-QUANTIFIER", - "UNVERIFIED-POSITIVE", -} -CRITICAL_ABSENCE_CLASSES = { - "BIOGRAPHICAL", - "FINANCIAL", - "ACHIEVEMENT", - "TIMELINE", - "QUANTITATIVE", - "ATTRIBUTION", - "VAGUE-QUANTIFIER", -} -STRUCTURED_VERDICTS = {"SUPPORTED", "REFUTED", "REFUTED_BY_ABSENCE", "NEI"} -SEVERITY_ORDER = { - "BIOGRAPHICAL": 0, - "FINANCIAL": 1, - "ACHIEVEMENT": 2, - "ATTRIBUTION": 3, - "TIMELINE": 4, - "QUANTITATIVE": 5, - "VAGUE-QUANTIFIER": 6, - "UNVERIFIED-POSITIVE": 7, - "TECHNICAL": 8, - "EXISTENTIAL": 9, - "CAUSAL": 10, - "COMPARATIVE": 11, -} -USER_TERMS_RE = re.compile( - r"\b(sam|sam's|the user|user's|you|your|yours|yourself)\b", re.IGNORECASE -) -HYPOTHETICAL_PREFIX_RE = re.compile( - r"^\s*(if|suppose|imagine|hypothetically|assume|what if)\b", - re.IGNORECASE, -) -SUBJECT_MODAL_PREFIX_RE = re.compile( - r"^\s*(sam|sam's|the user|user's|you|your)\b\s+(would|could)\b", - re.IGNORECASE, -) -TRAILING_MODAL_COMMENT_RE = re.compile( - r"\s*,?\s+(which|that)\s+(would|could)\b.*$", - re.IGNORECASE, -) -CURRENT_TURN_PREFIXES = [ - re.compile(r"^\s*(per your request|as requested)\s*,?\s*", re.IGNORECASE), - re.compile( - r"^\s*(you|sam|the user)\s+(asked for|requested)\s+maximum subagents\b[^,.;]*(?:,?\s*(and|so)\s*)?", - re.IGNORECASE, - ), - re.compile( - r"^\s*(you|sam|the user)\s+(asked|told|requested|wanted)\s+" - r"(?:(me|us|codex|claude)\s+)?(to|for)\s+", - re.IGNORECASE, - ), - re.compile( - r"^\s*(your|sam's|the user's)\s+request\s+(was|is)\s+(to|for)\s+", - re.IGNORECASE, - ), -] -FIRST_PERSON_DISCOURSE_RE = re.compile( - r"^\s*(i|we)\s+(reviewed|audited|checked|inspected|looked at|verified|" - r"confirmed|found|updated|changed|implemented|fixed|patched|added|removed|" - r"wired|ran|left)\b", - re.IGNORECASE, -) -DISCOURSE_ACTION_PREFIX_RE = re.compile( - r"^\s*(audit|review|check|inspect|look at|verify|confirm|implement|fix|" - r"patch|add|remove|wire|run|use|go all in)\b", - re.IGNORECASE, -) -EMBEDDED_USER_CLAIM_RE = re.compile(r"\b(sam|sam's|the user|user's)\b", re.IGNORECASE) -TECHNICAL_RE = re.compile( - r"(/\w|[\w.-]+\.(py|rs|ts|tsx|js|jsx|json|md|toml|yaml|yml|sh)\b|" - r"\b(api|endpoint|env|flag|model|server|hook|script|function|class|repo|" - r"crate|mcp|http|json|sqlite|rust|python|typescript|command|config)\b|" - r"\b[A-Z][A-Z0-9_]{2,}\b)", - re.IGNORECASE, -) -BIOGRAPHICAL_RE = re.compile( - r"\b(born|lives?|located|based in|works? at|employed|employer|school|" - r"university|college|graduated|degree|founder|ceo|cto|student|job|role)\b", - re.IGNORECASE, -) -FINANCIAL_RE = re.compile( - r"(\$[\d,.]+|\b(revenue|funding|raised|earned|paid|payout|prize money|" - r"salary|net worth|valuation|stock|shares?|portfolio|profit|loss)\b)", - re.IGNORECASE, -) -ACHIEVEMENT_RE = re.compile( - r"\b(won|winner|ranked|placed|scored|score|completed|finished|launched|" - r"released|shipped|milestone|award|prize|accepted|published|graduated)\b", - re.IGNORECASE, -) -TIMELINE_RE = re.compile( - r"\b(\d{4}-\d{2}-\d{2}|\d{1,2}/\d{1,2}/\d{2,4}|" - r"jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|" - r"jul(?:y)?|aug(?:ust)?|sep(?:tember)?|oct(?:ober)?|nov(?:ember)?|" - r"dec(?:ember)?|today|yesterday|tomorrow|last week|next week|" - r"\d+\s+(days?|weeks?|months?|years?)\b)", - re.IGNORECASE, -) -QUANTITATIVE_RE = re.compile( - r"(\b\d+(?:\.\d+)?\s*(%|percent|x|times|stars?|users?|customers?|" - r"submissions?|points?|gb|mb|ms|s|seconds?|minutes?|hours?)?\b|" - r"\b(one|two|three|four|five|six|seven|eight|nine|ten|dozens?|hundreds?|" - r"thousands?|many|several|few|most)\b)", - re.IGNORECASE, -) -TOKEN_RE = re.compile(r"\$?\b[a-z0-9][a-z0-9.-]*\b", re.IGNORECASE) -STOP_CLAIM_TOKENS = { - "about", - "after", - "also", - "because", - "been", - "before", - "claim", - "from", - "have", - "into", - "more", - "sam", - "that", - "their", - "there", - "this", - "user", - "with", - "your", -} -ATTRIBUTION_RE = re.compile( - r"\b(said|told|asked|agreed|decided|approved|rejected|committed|authored|" - r"wrote|built|implemented|requested|wanted|prefers?)\b", - re.IGNORECASE, -) -VAGUE_QUANTIFIER_RE = re.compile( - r"\b(a few|some|several|many|most|multiple)\b.*\b(wins?|won|prizes?|" - r"money|customers?|deals?|submissions?|placements?)\b", - re.IGNORECASE, -) - - -@dataclass(frozen=True) -class Claim: - text: str - claim_class: str - source_index: int - sam_critical: bool - - -@dataclass(frozen=True) -class EvidenceItem: - id: str - preview: str - trust: float - role: str = "evidence" - date: str = "" - durable: bool = True - source: str = "vestige" - - -@dataclass -class ClaimVerdict: - claim: Claim - status: str - reason: str = "" - evidence_ids: list[str] = field(default_factory=list) - durable_evidence_count: int = 0 - high_trust_evidence_count: int = 0 - - -def env_flag(name: str) -> bool: - return (os.environ.get(name) or "").strip().lower() in {"1", "true", "yes", "on"} - - -def truncate_chars(text: str, max_chars: int, suffix: str = "...") -> str: - """Truncate by Python characters, never UTF-8 bytes, and avoid dangling marks.""" - if max_chars <= 0: - return "" - if len(text) <= max_chars: - return text - if max_chars <= len(suffix): - return text[:max_chars] - cut = text[: max_chars - len(suffix)].rstrip() - while cut and unicodedata.combining(cut[-1]): - cut = cut[:-1] - return f"{cut}{suffix}" - - -def safe_float(value: Any, default: float = 0.0) -> float: - try: - return float(value) - except (TypeError, ValueError): - return default - def fetch_evidence(draft: str) -> tuple[str, int]: """Single deep_reference call — returns (formatted evidence, count of high-trust memories). @@ -363,15 +81,11 @@ def fetch_evidence(draft: str) -> tuple[str, int]: with urllib.request.urlopen(VESTIGE_HEALTH, timeout=VESTIGE_TIMEOUT) as r: r.read() except Exception: - if sanhedrin_core is not None: - sanhedrin_core.record_fail_open("vestige_health_unavailable", VESTIGE_HEALTH) return "", 0 query = draft[:1500] resp = post_json(VESTIGE_ENDPOINT, {"query": query, "depth": 12}, VESTIGE_TIMEOUT) if not isinstance(resp, dict): - if sanhedrin_core is not None: - sanhedrin_core.record_fail_open("deep_reference_unavailable", VESTIGE_ENDPOINT) return "", 0 parts = [] @@ -421,342 +135,19 @@ def fetch_evidence(draft: str) -> tuple[str, int]: return header + "\n".join(parts), high_trust_count -def split_candidate_claims(draft: str) -> list[str]: - """Return sentence-ish draft fragments that can be classified as claims.""" - without_fences = re.sub(r"```.*?```", " ", draft[:16_384], flags=re.DOTALL) - without_fences = re.sub(r"`[^`\n]+`", " ", without_fences) - without_fences = re.sub(r'(^|[\s([{])"[^"\n]+"(?=([\s.,;:!?)}\]]|$))', r"\1 ", without_fences) - fragments: list[str] = [] - for line in without_fences.splitlines(): - if line.lstrip().startswith(">"): - continue - line = re.sub(r"^\s*[-*+]\s+", "", line).strip() - line = re.sub(r"^\s*\d+[.)]\s+", "", line).strip() - if not line: - continue - parts = re.split(r"(?<=[.!?])\s+(?=[A-Z0-9`\"'])", line) - fragments.extend(part.strip(" \t-") for part in parts if part.strip(" \t-")) - if len(fragments) >= 512: - return fragments[:512] - if not fragments: - compact = " ".join(without_fences.split()) - fragments = [ - part.strip() - for part in re.split(r"(?<=[.!?])\s+(?=[A-Z0-9`\"'])", compact) - if part.strip() - ] - return fragments[:512] - - -def normalize_asserted_fragment(text: str) -> str | None: - text = " ".join(text.split()).strip() - if not text: - return None - if re.search(r"\b(need|still need|let me|will|should)\s+to\s+verify\b", text, re.I): - return None - if re.fullmatch( - r"(the user|user|sam|you)\s+(said|told|asked|wrote|noted|mentioned)\s*" - r"(earlier|before|previously)?\.?", - text, - re.I, - ): - return None - text = TRAILING_MODAL_COMMENT_RE.sub("", text).strip(" ,;:-") - if HYPOTHETICAL_PREFIX_RE.search(text) or SUBJECT_MODAL_PREFIX_RE.search(text): - return None - - for prefix in CURRENT_TURN_PREFIXES: - stripped = prefix.sub("", text, count=1).strip(" ,;:-") - if stripped == text: - continue - embedded = EMBEDDED_USER_CLAIM_RE.search(stripped) - if embedded and embedded.start() > 0 and DISCOURSE_ACTION_PREFIX_RE.search(stripped): - stripped = stripped[embedded.start() :].strip(" ,;:-") - elif DISCOURSE_ACTION_PREFIX_RE.search(stripped) or FIRST_PERSON_DISCOURSE_RE.search(stripped): - return None - text = stripped - break - - if FIRST_PERSON_DISCOURSE_RE.search(text): - embedded = EMBEDDED_USER_CLAIM_RE.search(text) - if embedded and embedded.start() > 0: - text = text[embedded.start() :].strip(" ,;:-") - else: - return None - - text = TRAILING_MODAL_COMMENT_RE.sub("", text).strip(" ,;:-") - if not text or HYPOTHETICAL_PREFIX_RE.search(text) or SUBJECT_MODAL_PREFIX_RE.search(text): - return None - return text - - -def classify_claim(text: str) -> str | None: - """Classify a factual-shaped claim with conservative, testable heuristics.""" - if VAGUE_QUANTIFIER_RE.search(text): - return "VAGUE-QUANTIFIER" - if BIOGRAPHICAL_RE.search(text): - return "BIOGRAPHICAL" - if FINANCIAL_RE.search(text): - return "FINANCIAL" - if ACHIEVEMENT_RE.search(text): - return "ACHIEVEMENT" - if ATTRIBUTION_RE.search(text): - return "ATTRIBUTION" - if TECHNICAL_RE.search(text): - return "TECHNICAL" - if TIMELINE_RE.search(text): - return "TIMELINE" - if QUANTITATIVE_RE.search(text): - return "QUANTITATIVE" - if re.search(r"\b(exists?|there is|there are|contains?|includes?)\b", text, re.I): - return "EXISTENTIAL" - if re.search(r"\b(because|caused|causes|therefore|so that|as a result)\b", text, re.I): - return "CAUSAL" - if re.search(r"\b(better|best|faster|fastest|more than|less than|fewer than)\b", text, re.I): - return "COMPARATIVE" - return None - - -def is_sam_critical_claim(text: str, claim_class: str) -> bool: - if claim_class not in CRITICAL_ABSENCE_CLASSES: - return False - return bool(USER_TERMS_RE.search(text)) - - -def extract_check_worthy_claims( - draft: str, - max_claims: int = MAX_CLAIMS, - max_claim_chars: int = MAX_CLAIM_CHARS, -) -> list[Claim]: - claims: list[Claim] = [] - seen: set[str] = set() - for idx, fragment in enumerate(split_candidate_claims(draft)): - text = normalize_asserted_fragment(fragment) - if not text: - continue - claim_class = classify_claim(text) - if not claim_class: - continue - text = truncate_chars(text, max_claim_chars) - key = text.lower() - if key in seen: - continue - seen.add(key) - claims.append( - Claim( - text=text, - claim_class=claim_class, - source_index=idx, - sam_critical=is_sam_critical_claim(text, claim_class), - ) - ) - return sorted( - claims, - key=lambda claim: (SEVERITY_ORDER.get(claim.claim_class, 99), claim.source_index), - )[:max_claims] - - -def normalize_evidence_item(raw: Any, source: str = "vestige") -> EvidenceItem | None: - if isinstance(raw, str): - preview = raw.strip() - if not preview: - return None - return EvidenceItem( - id="stage", - preview=truncate_chars(preview, MAX_EVIDENCE_CHARS), - trust=1.0, - role="staged", - durable=False, - source="stage", - ) - if not isinstance(raw, dict): - return None - - preview = ( - raw.get("preview") - or raw.get("answer_preview") - or raw.get("content") - or raw.get("text") - or raw.get("claim") - or "" - ) - preview = str(preview).strip() - if not preview: - return None - trust = safe_float(raw.get("trust", raw.get("trust_score", 1.0 if source == "stage" else 0.0))) - item_id = str(raw.get("memory_id") or raw.get("id") or source or "evidence") - role = str(raw.get("role") or ("staged" if source == "stage" else "evidence")) - date = str(raw.get("date") or raw.get("created_at") or "")[:32] - return EvidenceItem( - id=item_id, - preview=truncate_chars(preview, MAX_EVIDENCE_CHARS), - trust=trust, - role=role, - date=date, - durable=(source != "stage"), - source=source, - ) - - -def evidence_from_deep_reference(resp: dict[str, Any]) -> list[EvidenceItem]: - items: list[EvidenceItem] = [] - rec = resp.get("recommended") or {} - rec_item = normalize_evidence_item(rec, "vestige") - if rec_item: - items.append(rec_item) - for raw in resp.get("evidence") or []: - item = normalize_evidence_item(raw, "vestige") - if item: - items.append(item) - for raw in resp.get("superseded") or []: - item = normalize_evidence_item(raw, "vestige") - if item: - items.append(item) - return dedupe_evidence(items) - - -def dedupe_evidence(items: list[EvidenceItem]) -> list[EvidenceItem]: - deduped: list[EvidenceItem] = [] - seen: set[tuple[str, str]] = set() - for item in items: - key = (item.source, item.id) - if key in seen: - continue - seen.add(key) - deduped.append(item) - return deduped - - -def load_staged_evidence(path: str | None) -> list[EvidenceItem]: - """Read optional JSON-array staged evidence. It is non-durable by design.""" - if not path: - return [] - try: - with open(path, "r", encoding="utf-8") as f: - raw = json.load(f) - except (OSError, json.JSONDecodeError): - return [] - if not isinstance(raw, list): - return [] - items: list[EvidenceItem] = [] - for idx, raw_item in enumerate(raw): - item = normalize_evidence_item(raw_item, "stage") - if item is None: - continue - if item.id == "stage": - item = replace(item, id=f"stage:{idx}") - items.append(item) - return items - - -def claim_query(claim: Claim) -> str: - return ( - f"Class: {claim.claim_class}\n" - f"Claim: {claim.text}" - ) - - -def fetch_claim_evidence(claim: Claim) -> tuple[list[EvidenceItem], bool]: - resp = post_json(VESTIGE_ENDPOINT, {"query": claim_query(claim), "depth": 12}, VESTIGE_TIMEOUT) - if not isinstance(resp, dict): - return [], False - if resp.get("error") or resp.get("errors"): - return [], False - if str(resp.get("status") or "").strip().lower() in { - "error", - "failed", - "failure", - "unavailable", - "timeout", - }: - return [], False - if not any( - key in resp - for key in ("confidence", "evidence", "recommended", "reasoning", "query", "status") - ): - return [], False - return evidence_from_deep_reference(resp), True - - -def high_trust(items: list[EvidenceItem]) -> list[EvidenceItem]: - return [item for item in items if item.trust >= TRUST_FLOOR] - - -def durable_high_trust(items: list[EvidenceItem]) -> list[EvidenceItem]: - return [item for item in items if item.durable and item.trust >= TRUST_FLOOR] - - -def salient_claim_tokens(text: str) -> set[str]: - tokens = {token.lower().strip(".") for token in TOKEN_RE.findall(text)} - return { - token - for token in tokens - if len(token) >= 4 and token not in STOP_CLAIM_TOKENS - } - - -def evidence_relevant_to_claim(claim: Claim, evidence: EvidenceItem) -> bool: - claim_numbers = set(re.findall(r"\$?\d+(?:[,.]\d+)*(?:\.\d+)?", claim.text)) - if claim_numbers and any(num in evidence.preview for num in claim_numbers): - return True - claim_tokens = salient_claim_tokens(claim.text) - if not claim_tokens: - return True - preview_tokens = salient_claim_tokens(evidence.preview) - overlap = claim_tokens & preview_tokens - threshold = 1 if claim.claim_class == "TECHNICAL" else 2 - return len(overlap) >= threshold - - -def relevant_durable_high_trust(claim: Claim, items: list[EvidenceItem]) -> list[EvidenceItem]: - return [ - item - for item in durable_high_trust(items) - if evidence_relevant_to_claim(claim, item) - ] - - -def format_claim_evidence(items: list[EvidenceItem], claim: Claim | None = None) -> str: - if not items: - return "(no relevant evidence retrieved)" - lines = [] - durable_count = ( - len(relevant_durable_high_trust(claim, items)) - if claim is not None - else len(durable_high_trust(items)) - ) - high_count = len(high_trust(items)) - lines.append( - f"HIGH-TRUST EVIDENCE: {high_count} | DURABLE HIGH-TRUST EVIDENCE: {durable_count}" - ) - stage_count = len([item for item in items if not item.durable]) - if stage_count: - lines.append( - "STAGED EVIDENCE PRESENT: non-durable overlay; do not count it as durable memory." - ) - for item in high_trust(items)[:8]: - durable = "durable" if item.durable else "staged" - short_id = item.id[:12] - lines.append( - f"[{short_id}] {durable} role={item.role} trust={item.trust:.2f} date={item.date}\n" - f"{item.preview}" - ) - return "\n\n".join(lines) - - -SYSTEM_PROMPT = """You are the Sanhedrin Executioner. You judge whether a DRAFT contradicts Vestige memory evidence about the user. ONE LINE OF OUTPUT. +SYSTEM_PROMPT = """You are the Sanhedrin Executioner. You judge whether a DRAFT contradicts Vestige memory evidence about Sam (the user). ONE LINE OF OUTPUT. VALID CLASS TAGS (closed set — pick exactly one): TECHNICAL | ACHIEVEMENT | FINANCIAL | BIOGRAPHICAL | TIMELINE | ATTRIBUTION | VAGUE-QUANTIFIER | UNVERIFIED-POSITIVE DEFAULT POSTURE - DEFAULT to `yes` (PASS) for TECHNICAL / TIMELINE / EXISTENTIAL claims unless you can cite a same-subject direct contradiction. -- DEFAULT to `no` (VETO, fail-closed) for these specific user-about claims when high-trust evidence is silent on the named entity: - * Specific institution / employer / school / company the user is claimed to be at +- DEFAULT to `no` (VETO, fail-closed) for these specific Sam-about claims when high-trust evidence is silent on the named entity: + * Specific institution / employer / school / company Sam is claimed to be at * Specific dollar amount won / earned / raised * Specific competition placement / score / prize received - * Specific date the user did something specific (graduated, was hired, was born) - * Vague-quantifier positive about the user ("a few wins", "some prize money", "most submissions placed top 10", "many customers", "several deals") + * Specific date Sam did something specific (graduated, was hired, was born) + * Vague-quantifier positive about Sam ("a few wins", "some prize money", "most submissions placed top 10", "many customers", "several deals") THREE FALSE-POSITIVE PROTECTIONS (these output `yes`) 1. SUBJECT-EQUALITY GATE: only same-subject claims are veto candidates. Memory about Vestige's internal codebase ≠ contradiction with external tools (Qwen, MCP-protocol-spec, MLX, Cursor). Memory about project X ≠ contradiction with project Y. @@ -783,18 +174,18 @@ Draft: "Edit the FastAPI router in vestige/main.py for Python extensions to Vest Output: no - [Sanhedrin Veto] TECHNICAL: Draft says FastAPI/Python for Vestige, memory de43be5a says 2-crate Rust workspace. [VETO — same-subject ACHIEVEMENT contradiction] -Evidence: "Final benchmark submission scored 36/50 on April 15, no payout" trust=0.71 [9cf2a764] -Draft: "The user won the benchmark with a perfect 50/50 and took the $25K grand prize" +Evidence: "AIMO3 final submission scored 36/50 on April 15, no payout" trust=0.71 [9cf2a764] +Draft: "Sam won AIMO3 with a perfect 50/50 and took the $25K grand prize" Output: no - [Sanhedrin Veto] ACHIEVEMENT: Draft claims 50/50 win + $25K, memory 9cf2a764 shows 36/50 final, no payout. [VETO — VAGUE-QUANTIFIER fail-closed] -Evidence: high-trust memories about the user's competition history, none enumerate any wins -Draft: "The user won a few competitions and earned some prize money" +Evidence: high-trust memories about Sam's competition history, none enumerate any wins +Draft: "Sam won a few Kaggle competitions and earned some prize money" Output: no - [Sanhedrin Veto] VAGUE-QUANTIFIER: Draft says "a few wins / some prize money", evidence enumerates zero wins, fail-closed. [VETO — UNVERIFIED-POSITIVE fail-closed] -Evidence: high-trust memories about the user's identity/work, no example school or employer mention -Draft: "The user graduated from Example University in 2019 with a 3.94 GPA and worked at Example Labs" +Evidence: high-trust memories about Sam's identity/work, no Stanford or Google Brain mention +Draft: "Sam graduated Stanford CS in 2019 with a 3.94 GPA and worked at Google Brain" Output: no - [Sanhedrin Veto] UNVERIFIED-POSITIVE: Specific Stanford/2019/Google Brain claims, evidence silent on all, fail-closed. [PASS — SUBJECT-EQUALITY gate (external tool, not Vestige)] @@ -808,8 +199,8 @@ Draft: "Memory bandwidth on the M3 Max is around 400 GB/s for the unified archit Output: yes [PASS — AGREEMENT-IS-NOT-CONTRADICTION] -Evidence: "The user's M3 Max MacBook Pro arrived 2026-04-20" trust=0.55 -Draft: "The user's MacBook is an M3 Max" +Evidence: "Sam's M3 Max MacBook Pro arrived 2026-04-20" trust=0.55 +Draft: "Sam's MacBook is an M3 Max" Output: yes [PASS — ARCHITECTURE-VS-COMPONENT] @@ -824,48 +215,30 @@ Reason: external script that CALLS Vestige is not the same subject as Vestige's Output: yes [PASS — HYPOTHETICAL-MOOD] -Evidence: "Final benchmark score was 36/50 with no payout" trust=0.71 -Draft: "If the user wins the benchmark 50/50 next time around, they could claim the $25K grand prize." +Evidence: "AIMO3 final 36/50 no payout" trust=0.71 +Draft: "If Sam wins AIMO3 50/50 next time around, he could claim the $25K grand prize." Reason: prefix `if`/`suppose`/`imagine`/`hypothetically`/`would`/`could`/`assume` marks the embedded claim as conditional, NOT asserted. Conditional claims about future or counterfactual states do not contradict factual memory. Output: yes HYPOTHETICAL-MOOD RULE: if a draft sentence is governed by `if`, `suppose`, `imagine`, `hypothetically`, `would`, `could`, `assume`, `what if`, the embedded claim is NOT being asserted as fact — PASS that claim regardless of memory state. -ARCHIVED-COMPETITION RULE: do NOT fail-closed on the EXISTENCE of a past competition or project just because evidence is silent on it. Fail-closed applies only to specific PLACEMENT, SCORE, PRIZE, INSTITUTION, or DOLLAR AMOUNT -- not to "the user participated in X." +ARCHIVED-COMPETITION RULE: do NOT fail-closed on the EXISTENCE of a past competition or project just because evidence is silent on it. Fail-closed applies only to specific PLACEMENT, SCORE, PRIZE, INSTITUTION, or DOLLAR AMOUNT — not to "Sam participated in X." -MULTI-CLAIM SEVERITY ORDERING: if multiple claims are vetoable, choose ACHIEVEMENT/FINANCIAL/BIOGRAPHICAL/UNVERIFIED-POSITIVE over TECHNICAL. Specific fabrications about the user's life are more dangerous than tech-stack mismatches. +MULTI-CLAIM SEVERITY ORDERING: if multiple claims are vetoable, choose ACHIEVEMENT/FINANCIAL/BIOGRAPHICAL/UNVERIFIED-POSITIVE over TECHNICAL. Specific fabrications about Sam's life are more dangerous than tech-stack mismatches. -When in doubt on TECHNICAL/TIMELINE: PASS. When in doubt on a user-about ACHIEVEMENT/FINANCIAL/BIOGRAPHICAL claim with specific named entities not in evidence: VETO with UNVERIFIED-POSITIVE.""" +When in doubt on TECHNICAL/TIMELINE: PASS. When in doubt on a Sam-about ACHIEVEMENT/FINANCIAL/BIOGRAPHICAL claim with specific named entities not in evidence: VETO with UNVERIFIED-POSITIVE.""" -CLAIM_SYSTEM_PROMPT = """You are the Sanhedrin Executioner in claim mode. Judge ONE extracted claim against the provided Vestige evidence. - -Return exactly one JSON object, no markdown: -{ - "status": "SUPPORTED|REFUTED|REFUTED_BY_ABSENCE|NEI", - "class": "", - "reason": "", - "evidence_ids": [""] +VALID_CLASSES = { + "TECHNICAL", "ACHIEVEMENT", "FINANCIAL", "BIOGRAPHICAL", + "TIMELINE", "ATTRIBUTION", "VAGUE-QUANTIFIER", "UNVERIFIED-POSITIVE", } - -Rules: -- SUPPORTED: high-trust evidence directly supports the claim. -- REFUTED: high-trust evidence directly contradicts the same-subject claim. -- REFUTED_BY_ABSENCE: use only when instructions say absence-fail-closed applies. -- NEI: not enough information, stale/noisy evidence, wrong subject, or inference required. -- Do not infer contradiction across different subjects, versions, projects, or architecture layers. -- Staged evidence is context only and is not durable Vestige memory. -- Reasons must not use implies, suggests, must mean, would mean, indicates, therefore, or this means. -""" - - -VALID_CLASSES = CLAIM_CLASSES INFERENCE_VERBS = ( "implies", "implying", "suggests", "must mean", "would mean", "indicates that", "therefore the", "this means", ) VERDICT_RE = re.compile( - r"^no - \[Sanhedrin Veto\] \[?([A-Z][A-Z\-]*)\]?: (.{1,180})$" + r"^no - \[Sanhedrin Veto\] ([A-Z][A-Z\-]*): (.{1,180})$" ) @@ -901,29 +274,29 @@ def validate_verdict(verdict: str) -> str: def judge(draft: str, evidence: str) -> str: - if not sanhedrin_model_configured(): - if sanhedrin_core is not None: - sanhedrin_core.record_fail_open( - "model_not_configured", - "Set VESTIGE_SANHEDRIN_ENDPOINT and VESTIGE_SANHEDRIN_MODEL, or choose a preset.", - ) - return "" user_msg = ( f"VESTIGE EVIDENCE (recommended + top trust-scored memories):\n" f"{evidence if evidence else '(no relevant evidence retrieved)'}\n\n" f"---\nDRAFT TO JUDGE:\n{draft}" ) - body = sanhedrin_body( - [ + body = { + "model": MODEL, + "messages": [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_msg}, ], - 2500, - [ + "max_tokens": 2500, + "temperature": 0.0, + "top_p": 1.0, + "top_k": 1, + "seed": 42, + "stream": False, + "chat_template_kwargs": {"enable_thinking": False}, + "stop": [ "\n\nWait,", "\n\nActually,", "\n\nLet me", "\n\nHmm,", "\n\nOn second thought", "\n\nOh wait", ], - ) + } resp = post_json(SANHEDRIN_ENDPOINT, body, SANHEDRIN_TIMEOUT) if not isinstance(resp, dict): return "" @@ -949,418 +322,18 @@ def judge(draft: str, evidence: str) -> str: return "" -def absence_verdict(claim: Claim) -> ClaimVerdict: - reason = ( - f"{claim.claim_class} claim about Sam has zero high-trust durable Vestige evidence." - ) - return ClaimVerdict( - claim=claim, - status="REFUTED_BY_ABSENCE", - reason=truncate_chars(reason, 140), - ) - - -def nei_verdict( - claim: Claim, - reason: str, - evidence: list[EvidenceItem] | None = None, -) -> ClaimVerdict: - evidence = evidence or [] - return ClaimVerdict( - claim=claim, - status="NEI", - reason=truncate_chars(reason, 140), - evidence_ids=[item.id for item in high_trust(evidence)[:3]], - durable_evidence_count=len(relevant_durable_high_trust(claim, evidence)), - high_trust_evidence_count=len(high_trust(evidence)), - ) - - -def supported_verdict(claim: Claim, evidence: list[EvidenceItem]) -> ClaimVerdict: - return ClaimVerdict( - claim=claim, - status="SUPPORTED", - reason="High-trust evidence supports or does not contradict the claim.", - evidence_ids=[item.id for item in high_trust(evidence)[:3]], - durable_evidence_count=len(relevant_durable_high_trust(claim, evidence)), - high_trust_evidence_count=len(high_trust(evidence)), - ) - - -def parse_json_object(raw: str) -> dict[str, Any] | None: - cleaned = THINK_RE.sub("", raw).strip() - cleaned = re.sub(r"^```(?:json)?\s*", "", cleaned, flags=re.IGNORECASE).strip() - cleaned = re.sub(r"\s*```$", "", cleaned).strip() - try: - obj = json.loads(cleaned) - return obj if isinstance(obj, dict) else None - except json.JSONDecodeError: - pass - start = cleaned.find("{") - end = cleaned.rfind("}") - if start >= 0 and end > start: - try: - obj = json.loads(cleaned[start : end + 1]) - return obj if isinstance(obj, dict) else None - except json.JSONDecodeError: - return None - return None - - -def verdict_from_legacy_line(claim: Claim, raw: str, evidence: list[EvidenceItem]) -> ClaimVerdict | None: - line = validate_verdict(raw) - if line == "yes": - return supported_verdict(claim, evidence) - m = VERDICT_RE.match(line) - if not m: - return None - reason = m.group(2) - if not relevant_durable_high_trust(claim, evidence): - return nei_verdict(claim, "Durable evidence required for refuted verdict.", evidence) - return ClaimVerdict( - claim=claim, - status="REFUTED", - reason=truncate_chars(reason, 140), - evidence_ids=[item.id for item in high_trust(evidence)[:3]], - durable_evidence_count=len(relevant_durable_high_trust(claim, evidence)), - high_trust_evidence_count=len(high_trust(evidence)), - ) - - -def validate_structured_verdict( - claim: Claim, - data: dict[str, Any], - evidence: list[EvidenceItem], -) -> ClaimVerdict: - status = str(data.get("status") or "").strip().upper() - if status not in STRUCTURED_VERDICTS: - status = "NEI" - claim_class = str(data.get("class") or claim.claim_class).strip().upper() - if claim_class not in CLAIM_CLASSES: - claim_class = claim.claim_class - reason = truncate_chars(str(data.get("reason") or "").strip(), 140) - if any(verb in reason.lower() for verb in INFERENCE_VERBS): - return nei_verdict(claim, "Inference-chain verdict downgraded to NEI.", evidence) - if status == "REFUTED_BY_ABSENCE": - if not (claim.sam_critical and claim.claim_class in CRITICAL_ABSENCE_CLASSES): - return nei_verdict(claim, "Absence veto does not apply to this claim.", evidence) - if relevant_durable_high_trust(claim, evidence): - return nei_verdict(claim, "Durable evidence exists; absence veto does not apply.", evidence) - if status == "REFUTED" and not relevant_durable_high_trust(claim, evidence): - return nei_verdict(claim, "Durable evidence required for refuted verdict.", evidence) - if status == "SUPPORTED" and high_trust(evidence) and not durable_high_trust(evidence): - return nei_verdict(claim, "Durable evidence required for supported verdict.", evidence) - evidence_ids_raw = data.get("evidence_ids") or [] - evidence_ids = [ - str(eid) for eid in evidence_ids_raw[:5] - ] if isinstance(evidence_ids_raw, list) else [] - if not reason: - if status == "SUPPORTED": - reason = "High-trust evidence supports or does not contradict the claim." - elif status == "NEI": - reason = "Not enough high-trust evidence to decide." - elif status == "REFUTED_BY_ABSENCE": - reason = absence_verdict(claim).reason - else: - reason = "High-trust evidence refutes the claim." - return ClaimVerdict( - claim=Claim( - text=claim.text, - claim_class=claim_class, - source_index=claim.source_index, - sam_critical=claim.sam_critical, - ), - status=status, - reason=reason, - evidence_ids=evidence_ids, - durable_evidence_count=len(relevant_durable_high_trust(claim, evidence)), - high_trust_evidence_count=len(high_trust(evidence)), - ) - - -def judge_claim_with_model(claim: Claim, evidence: list[EvidenceItem]) -> ClaimVerdict: - if not sanhedrin_model_configured(): - if sanhedrin_core is not None: - sanhedrin_core.record_fail_open( - "model_not_configured", - "Set VESTIGE_SANHEDRIN_ENDPOINT and VESTIGE_SANHEDRIN_MODEL, or choose a preset.", - ) - return nei_verdict(claim, "Sanhedrin model not configured; fail-open for this claim.", evidence) - user_msg = ( - f"CLAIM CLASS: {claim.claim_class}\n" - f"SAM-CRITICAL: {'yes' if claim.sam_critical else 'no'}\n" - f"ABSENCE-FAIL-CLOSED APPLIES: " - f"{'yes' if claim.sam_critical and claim.claim_class in CRITICAL_ABSENCE_CLASSES else 'no'}\n" - f"DURABLE HIGH-TRUST EVIDENCE COUNT: {len(relevant_durable_high_trust(claim, evidence))}\n\n" - f"CLAIM:\n{claim.text}\n\n" - f"EVIDENCE:\n{format_claim_evidence(evidence, claim)}" - ) - body = sanhedrin_body( - [ - {"role": "system", "content": CLAIM_SYSTEM_PROMPT}, - {"role": "user", "content": user_msg}, - ], - 700, - ) - resp = post_json(SANHEDRIN_ENDPOINT, body, SANHEDRIN_TIMEOUT) - if not isinstance(resp, dict): - if sanhedrin_core is not None: - sanhedrin_core.record_fail_open("model_unavailable", f"endpoint={SANHEDRIN_ENDPOINT}") - return nei_verdict(claim, "Sanhedrin model unavailable; fail-open for this claim.", evidence) - try: - msg = resp["choices"][0]["message"] - raw = msg.get("content") or msg.get("reasoning") or "" - except (KeyError, IndexError, TypeError): - if sanhedrin_core is not None: - sanhedrin_core.record_fail_open("malformed_model_response", f"endpoint={SANHEDRIN_ENDPOINT}") - return nei_verdict(claim, "Malformed Sanhedrin model response.", evidence) - data = parse_json_object(raw) - if data is not None: - return validate_structured_verdict(claim, data, evidence) - legacy = verdict_from_legacy_line(claim, raw, evidence) - if legacy is not None: - return legacy - if sanhedrin_core is not None: - sanhedrin_core.record_fail_open("unstructured_model_response", raw[:500]) - return nei_verdict(claim, "Sanhedrin model did not return structured JSON.", evidence) - - -def judge_claim(claim: Claim, evidence: list[EvidenceItem]) -> ClaimVerdict: - if not sanhedrin_model_configured(): - if sanhedrin_core is not None: - sanhedrin_core.record_fail_open( - "model_not_configured", - "Set VESTIGE_SANHEDRIN_ENDPOINT and VESTIGE_SANHEDRIN_MODEL, or choose a preset.", - ) - return nei_verdict(claim, "Sanhedrin model not configured; fail-open for this claim.", evidence) - durable_count = len(relevant_durable_high_trust(claim, evidence)) - high_count = len(high_trust(evidence)) - if claim.sam_critical and claim.claim_class in CRITICAL_ABSENCE_CLASSES and durable_count == 0: - verdict = absence_verdict(claim) - verdict.high_trust_evidence_count = high_count - return verdict - if high_count == 0: - return nei_verdict(claim, "No high-trust evidence retrieved for this claim.", evidence) - return judge_claim_with_model(claim, evidence) - - -def render_legacy_from_verdicts(verdicts: list[ClaimVerdict]) -> str: - vetoes = [v for v in verdicts if v.status in {"REFUTED", "REFUTED_BY_ABSENCE"}] - if not vetoes: - return "yes" - vetoes.sort( - key=lambda v: ( - SEVERITY_ORDER.get(v.claim.claim_class, 99), - v.claim.source_index, - ) - ) - chosen = vetoes[0] - reason = truncate_chars(chosen.reason or chosen.claim.text, 140) - return f"no - [Sanhedrin Veto] [{chosen.claim.claim_class}]: {reason}" - - -def recompute_legacy_from_result(result: dict[str, Any]) -> str: - vetoes = [] - for raw in result.get("verdicts", []): - claim = raw.get("claim", {}) if isinstance(raw, dict) else {} - status = str(raw.get("status", "")) - if status not in {"REFUTED", "REFUTED_BY_ABSENCE"}: - continue - vetoes.append( - ( - SEVERITY_ORDER.get(str(claim.get("claim_class", "")), 99), - int(claim.get("source_index", 0) or 0), - str(claim.get("claim_class", "TECHNICAL")), - truncate_chars(str(raw.get("reason") or claim.get("text") or ""), 140), - ) - ) - if not vetoes: - return "yes" - _, _, claim_class, reason = sorted(vetoes)[0] - return f"no - [Sanhedrin Veto] [{claim_class}]: {reason}" - - -def apply_appeals_to_claim_mode_result(result: dict[str, Any]) -> dict[str, Any]: - if sanhedrin_core is None: - return result - appeals = sanhedrin_core.load_appeals() - changed = False - for raw in result.get("verdicts", []): - if not isinstance(raw, dict) or raw.get("status") not in {"REFUTED", "REFUTED_BY_ABSENCE"}: - continue - claim = raw.get("claim", {}) if isinstance(raw.get("claim"), dict) else {} - text = str(claim.get("text") or "") - if sanhedrin_core.is_appealed({"fingerprint": sanhedrin_core.claim_fingerprint(text)}, appeals): - raw["status"] = "APPEALED" - raw["reason"] = "Prior appeal suppresses this Sanhedrin veto." - changed = True - - if changed: - legacy = recompute_legacy_from_result(result) - result["legacy_verdict"] = legacy - result["decision"] = "yes" if legacy == "yes" else "no" - result["verdict"] = result["decision"] - result["passed"] = legacy == "yes" - result["reason"] = "" if result["passed"] else legacy.split(" - ", 1)[-1] - return result - - -def save_claim_mode_receipt( - draft: str, - result: dict[str, Any], - manifest: dict[str, Any] | None = None, -) -> None: - if sanhedrin_core is None: - return - manifest = manifest or sanhedrin_core.new_manifest(draft) - claims = [] - for idx, raw in enumerate(result.get("verdicts", []), start=1): - if not isinstance(raw, dict): - continue - claim = raw.get("claim", {}) if isinstance(raw.get("claim"), dict) else {} - text = str(claim.get("text") or "") - claim_class = str(claim.get("claim_class") or "TECHNICAL") - status = str(raw.get("status") or "NEI") - evidence_ids = raw.get("evidence_ids") if isinstance(raw.get("evidence_ids"), list) else [] - if status == "SUPPORTED": - decision = "pass" - evidence_state = "supported" - fix = "No change required." - elif status == "APPEALED": - decision = "appealed" - evidence_state = "appealed" - fix = "Prior appeal suppresses this veto fingerprint." - elif status == "REFUTED_BY_ABSENCE": - decision = "veto" - evidence_state = "missing_precedent" - fix = "Remove the unsupported user-specific claim or cite durable Vestige evidence first." - elif status == "REFUTED": - decision = "veto" - evidence_state = "contradicted" - fix = "Remove or qualify the contradicted claim using the cited Vestige precedent." - else: - decision = "pass_unverified" - evidence_state = "not_enough_information" - fix = "No blocking change required." - claims.append( - { - "id": f"c{idx:03d}", - "text": text, - "fingerprint": sanhedrin_core.claim_fingerprint(text), - "class": claim_class, - "subject": "Sam" if bool(claim.get("sam_critical")) else "draft", - "risk": "hard" if bool(claim.get("sam_critical")) else "normal", - "evidence_state": evidence_state, - "decision": decision, - "precedent": [ - { - "type": "vestige", - "summary": str(raw.get("reason") or status), - "evidence": ", ".join(str(eid) for eid in evidence_ids[:5]), - "durableCount": raw.get("durable_evidence_count"), - "highTrustCount": raw.get("high_trust_evidence_count"), - } - ], - "fix": fix, - "appeal": { - "status": "appealed" if decision == "appealed" else "open", - "actions": ["stale", "wrong", "too_strict"], - }, - } - ) - - manifest["claims"] = claims - manifest["overall"] = "pass" if result.get("passed") else "veto" - if any(claim["decision"] == "appealed" for claim in claims): - manifest["overall"] = "pass_with_warnings" if result.get("passed") else manifest["overall"] - manifest["verdictBar"] = "APPEALED" - manifest["summary"] = "Prior appeal suppressed a Sanhedrin veto." - elif result.get("passed"): - manifest["verdictBar"] = "PASS" if not claims else "NOTE" - manifest["summary"] = "Sanhedrin found no blocking claim issues." - else: - manifest["verdictBar"] = "VETO" - manifest["summary"] = str(result.get("reason") or "Sanhedrin blocked a claim.") - sanhedrin_core.save_manifest(manifest) - - -def save_legacy_receipt(manifest: dict[str, Any] | None, verdict: str, evidence: str = "") -> str: - if sanhedrin_core is None or manifest is None: - return verdict - updated = sanhedrin_core.apply_model_verdict(manifest, verdict, evidence) - sanhedrin_core.save_manifest(manifest) - return updated - - -def claim_mode_result(draft: str) -> dict[str, Any]: - claims = extract_check_worthy_claims(draft) - staged = load_staged_evidence(os.environ.get(STAGE_FILE_ENV)) - verdicts: list[ClaimVerdict] = [] - for claim in claims: - evidence, ok = fetch_claim_evidence(claim) - if not ok: - if sanhedrin_core is not None: - sanhedrin_core.record_fail_open("retrieval_unavailable", claim.text) - verdicts.append( - nei_verdict( - claim, - "Vestige retrieval unavailable; fail-open for this claim.", - staged, - ) - ) - continue - combined = dedupe_evidence(evidence + staged) - verdicts.append(judge_claim(claim, combined)) - legacy_verdict = render_legacy_from_verdicts(verdicts) - decision = "yes" if legacy_verdict == "yes" else "no" - json_reason = "" if decision == "yes" else legacy_verdict.split(" - ", 1)[-1] - return { - "mode": "claim", - "decision": decision, - "verdict": decision, - "reason": json_reason, - "passed": legacy_verdict == "yes", - "legacy_verdict": legacy_verdict, - "claims_extracted": len(claims), - "staged_evidence_count": len(staged), - "verdicts": [asdict(v) for v in verdicts], - } - - -def print_claim_mode_result(result: dict[str, Any]) -> None: - if (os.environ.get(OUTPUT_ENV) or "").strip().lower() == "json": - print(json.dumps(result, ensure_ascii=False, separators=(",", ":"))) - else: - print(result.get("legacy_verdict") or "yes") - - def main() -> None: draft = sys.stdin.read().strip() if not draft: print("yes") return - manifest = sanhedrin_core.new_manifest(draft) if sanhedrin_core is not None else None - if sanhedrin_core is not None and manifest is not None: - receipt_veto = sanhedrin_core.apply_receipt_lock(manifest) - if receipt_veto: - sanhedrin_core.save_manifest(manifest) - print(f"no - [Sanhedrin Veto] [TECHNICAL]: {receipt_veto}") - return - - if env_flag(CLAIM_MODE_ENV): - result = apply_appeals_to_claim_mode_result(claim_mode_result(draft)) - save_claim_mode_receipt(draft, result, manifest) - print_claim_mode_result(result) - return - evidence, high_trust_count = fetch_evidence(draft) # Auto-pass if no high-trust evidence — model can't legitimately veto # without something concrete to cite. Eliminates the common false-positive # mode where the model invents a contradiction from low-trust noise. if high_trust_count == 0: - save_legacy_receipt(manifest, "yes", evidence) print("yes") return @@ -1368,13 +341,9 @@ def main() -> None: if not verdict: # Fail-open: server unreachable, malformed response, etc. - if sanhedrin_core is not None: - sanhedrin_core.record_fail_open("legacy_model_unavailable", f"endpoint={SANHEDRIN_ENDPOINT}") - save_legacy_receipt(manifest, "yes", evidence) print("yes") return - verdict = save_legacy_receipt(manifest, verdict, evidence) print(verdict) diff --git a/hooks/sanhedrin-presets.json b/hooks/sanhedrin-presets.json deleted file mode 100644 index 7b446ad..0000000 --- a/hooks/sanhedrin-presets.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "schema": "vestige.sanhedrin.presets.v2", - "defaultPreset": null, - "description": "Model-agnostic Sanhedrin backend recipes. Presets are suggestions only; users may set any OpenAI-compatible endpoint and model name.", - "presets": { - "custom-openai-compatible": { - "label": "Custom OpenAI-compatible endpoint", - "tier": "custom", - "bestFor": "Any model/server the user already trusts", - "requiresUserModel": true, - "endpointPlaceholder": "http://127.0.0.1:8000/v1/chat/completions", - "modelPlaceholder": "your-model-name", - "env": { - "VESTIGE_SANHEDRIN_ENDPOINT": "", - "VESTIGE_SANHEDRIN_MODEL": "", - "VESTIGE_SANHEDRIN_BACKEND": "openai-compatible", - "VESTIGE_SANHEDRIN_TIMEOUT": "45" - } - }, - "small-laptop-ollama": { - "label": "Small laptop Ollama", - "tier": "small-local", - "bestFor": "8-16 GB RAM laptops that need a lightweight offline verifier", - "setup": "Install Ollama, then pull any small instruct model you trust, for example: ollama pull llama3.2:3b or ollama pull qwen2.5:7b", - "tradeoffs": ["fast and accessible", "weaker contradiction judgment than larger models"], - "env": { - "VESTIGE_SANHEDRIN_ENDPOINT": "http://127.0.0.1:11434/v1/chat/completions", - "VESTIGE_SANHEDRIN_MODEL": "your-ollama-model", - "VESTIGE_SANHEDRIN_BACKEND": "ollama", - "VESTIGE_SANHEDRIN_TIMEOUT": "60" - } - }, - "balanced-local-ollama": { - "label": "Balanced local Ollama", - "tier": "balanced-local", - "bestFor": "16-32 GB RAM machines using 7B-14B local models", - "setup": "Install Ollama and pull a balanced verifier model such as qwen3:14b, llama3.1:8b, or another OpenAI-compatible local model.", - "tradeoffs": ["good first local choice", "model quality depends on the exact model selected"], - "env": { - "VESTIGE_SANHEDRIN_ENDPOINT": "http://127.0.0.1:11434/v1/chat/completions", - "VESTIGE_SANHEDRIN_MODEL": "your-ollama-model", - "VESTIGE_SANHEDRIN_BACKEND": "ollama", - "VESTIGE_SANHEDRIN_TIMEOUT": "60" - } - }, - "mlx-qwen3.6-apple-silicon": { - "label": "MLX Qwen3.6 35B A3B, Apple Silicon local", - "tier": "strong-local", - "bestFor": "High-memory Apple Silicon users who explicitly choose the strong MLX path", - "env": { - "VESTIGE_SANHEDRIN_ENDPOINT": "http://127.0.0.1:8080/v1/chat/completions", - "VESTIGE_SANHEDRIN_MODEL": "mlx-community/Qwen3.6-35B-A3B-4bit", - "VESTIGE_SANHEDRIN_BACKEND": "mlx", - "VESTIGE_SANHEDRIN_TIMEOUT": "45" - } - }, - "vllm-openai-compatible": { - "label": "vLLM OpenAI-compatible server", - "tier": "workstation", - "bestFor": "GPU workstations and team servers", - "env": { - "VESTIGE_SANHEDRIN_ENDPOINT": "http://127.0.0.1:8000/v1/chat/completions", - "VESTIGE_SANHEDRIN_MODEL": "your-vllm-model", - "VESTIGE_SANHEDRIN_BACKEND": "vllm", - "VESTIGE_SANHEDRIN_TIMEOUT": "45" - } - }, - "llama-cpp-openai-compatible": { - "label": "llama.cpp server", - "tier": "small-local", - "bestFor": "CPU or small-GPU local deployments", - "env": { - "VESTIGE_SANHEDRIN_ENDPOINT": "http://127.0.0.1:8081/v1/chat/completions", - "VESTIGE_SANHEDRIN_MODEL": "your-gguf-model", - "VESTIGE_SANHEDRIN_BACKEND": "llama.cpp", - "VESTIGE_SANHEDRIN_TIMEOUT": "90" - } - }, - "hosted-openai-compatible": { - "label": "Hosted OpenAI-compatible API", - "tier": "hosted", - "bestFor": "Users who want zero local model setup", - "requires": "VESTIGE_SANHEDRIN_API_KEY exported in the hook environment and a model chosen by the user/provider", - "env": { - "VESTIGE_SANHEDRIN_ENDPOINT": "https://api.openai.com/v1/chat/completions", - "VESTIGE_SANHEDRIN_MODEL": "your-hosted-model", - "VESTIGE_SANHEDRIN_BACKEND": "openai", - "VESTIGE_SANHEDRIN_TIMEOUT": "45" - } - }, - "anthropic-via-litellm": { - "label": "Anthropic through LiteLLM OpenAI-compatible proxy", - "bestFor": "Claude users who already run LiteLLM", - "setup": "Run LiteLLM locally with an Anthropic model, then point Sanhedrin at the proxy.", - "env": { - "VESTIGE_SANHEDRIN_ENDPOINT": "http://127.0.0.1:4000/v1/chat/completions", - "VESTIGE_SANHEDRIN_MODEL": "anthropic/claude-3-5-haiku-latest", - "VESTIGE_SANHEDRIN_BACKEND": "litellm", - "VESTIGE_SANHEDRIN_TIMEOUT": "45" - } - } - } -} diff --git a/hooks/sanhedrin.sh b/hooks/sanhedrin.sh index d8d2424..a875f1f 100755 --- a/hooks/sanhedrin.sh +++ b/hooks/sanhedrin.sh @@ -25,63 +25,22 @@ set -u -load_vestige_sanhedrin_env() { - [ -f "$1" ] || return 0 - command -v python3 >/dev/null 2>&1 || return 0 - while IFS="$(printf '\t')" read -r key value; do - case "$key" in - VESTIGE_SANHEDRIN_ENABLED|VESTIGE_SANHEDRIN_MODEL|VESTIGE_SANHEDRIN_ENDPOINT|VESTIGE_SANHEDRIN_API_KEY|VESTIGE_SANHEDRIN_BACKEND|VESTIGE_SANHEDRIN_CLAIM_MODE|VESTIGE_SANHEDRIN_OUTPUT|VESTIGE_SANHEDRIN_PYTHON|VESTIGE_SANHEDRIN_STATE_DIR|VESTIGE_SANHEDRIN_ALLOW_COMMAND_LEDGER|VESTIGE_SANHEDRIN_ALLOW_LOOSE_LEDGER|VESTIGE_DASHBOARD_PORT) - export "$key=$value" - ;; - esac - done < <(python3 - "$1" <<'PY' -import shlex -import sys - -allowed = { - "VESTIGE_SANHEDRIN_ENABLED", - "VESTIGE_SANHEDRIN_MODEL", - "VESTIGE_SANHEDRIN_ENDPOINT", - "VESTIGE_SANHEDRIN_API_KEY", - "VESTIGE_SANHEDRIN_BACKEND", - "VESTIGE_SANHEDRIN_CLAIM_MODE", - "VESTIGE_SANHEDRIN_OUTPUT", - "VESTIGE_SANHEDRIN_PYTHON", - "VESTIGE_SANHEDRIN_STATE_DIR", - "VESTIGE_SANHEDRIN_ALLOW_COMMAND_LEDGER", - "VESTIGE_SANHEDRIN_ALLOW_LOOSE_LEDGER", - "VESTIGE_DASHBOARD_PORT", -} - -try: - lines = open(sys.argv[1], encoding="utf-8").read().splitlines() -except OSError: - sys.exit(0) - -for raw in lines: - line = raw.strip() - if not line or line.startswith("#"): - continue - try: - parts = shlex.split(line, posix=True) - except ValueError: - continue - if len(parts) != 1 or "=" not in parts[0]: - continue - key, value = parts[0].split("=", 1) - if key in allowed and "\t" not in value and "\0" not in value: - print(f"{key}\t{value}") -PY - ) -} - # === OPT-IN GATE === -# Sanhedrin is opt-in and model-agnostic. It never guesses a large verifier -# model; if endpoint/model are unset, the bridge fails open with telemetry. -# The installer writes this env file only for --enable-sanhedrin. +# Sanhedrin is heavyweight: the default local backend is a ~19 GB model and +# needs roughly 20+ GB of free RAM. Keep it disabled unless the user explicitly +# opts in. The installer writes this env file only for --enable-sanhedrin. SANHEDRIN_ENV="${VESTIGE_SANHEDRIN_ENV:-$HOME/.claude/hooks/vestige-sanhedrin.env}" if [ -f "$SANHEDRIN_ENV" ]; then - load_vestige_sanhedrin_env "$SANHEDRIN_ENV" || exit 0 + set +u + set -a + # shellcheck disable=SC1090 + . "$SANHEDRIN_ENV" 2>/dev/null || { + set +a + set -u + exit 0 + } + set +a + set -u fi case "${VESTIGE_SANHEDRIN_ENABLED:-0}" in @@ -96,48 +55,9 @@ if [ "${VESTIGE_EXECUTIONER_ACTIVE:-0}" = "1" ]; then exit 0 fi -PYTHON_BIN="${VESTIGE_SANHEDRIN_PYTHON:-}" -if [ -z "$PYTHON_BIN" ]; then - PYTHON_BIN="$(command -v python3 2>/dev/null || printf '')" -fi -if [ -z "$PYTHON_BIN" ]; then - PYTHON_BIN="/usr/bin/python3" -fi -if ! "$PYTHON_BIN" -c 'import sys' >/dev/null 2>&1; then - exit 0 -fi - -record_sanhedrin_fail_open() { - REASON="$1" - DETAIL="${2:-}" - "$PYTHON_BIN" - "$REASON" "$DETAIL" <<'PY' 2>/dev/null || true -import json -import os -import sys -from datetime import datetime, timezone -from pathlib import Path - -reason = sys.argv[1] if len(sys.argv) > 1 else "unknown" -detail = sys.argv[2] if len(sys.argv) > 2 else "" -state_dir = Path(os.environ.get("VESTIGE_SANHEDRIN_STATE_DIR") or Path.home() / ".vestige" / "sanhedrin") -try: - state_dir.mkdir(parents=True, exist_ok=True) - with (state_dir / "fail-open.jsonl").open("a", encoding="utf-8") as f: - f.write(json.dumps({ - "timestamp": datetime.now(timezone.utc).isoformat(timespec="seconds"), - "runId": os.environ.get("VESTIGE_SANHEDRIN_RUN_ID"), - "reason": reason, - "detail": detail[:500], - "transcript": os.environ.get("TRANSCRIPT_PATH") or os.environ.get("VESTIGE_SANHEDRIN_TRANSCRIPT"), - }) + "\n") -except OSError: - pass -PY -} - # === READ STOP HOOK INPUT === INPUT="$(cat)" -TRANSCRIPT_PATH="$(printf '%s' "$INPUT" | "$PYTHON_BIN" -c 'import sys,json;d=json.load(sys.stdin);print(d.get("transcript_path",""))' 2>/dev/null || printf '')" +TRANSCRIPT_PATH="$(printf '%s' "$INPUT" | /usr/bin/python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get("transcript_path",""))' 2>/dev/null || printf '')" if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then exit 0 @@ -150,7 +70,7 @@ DRAFT_SCRIPT="$(mktemp -t vestige-sanhedrin-draft.XXXXXX)" trap 'rm -f "$DRAFT_SCRIPT"' EXIT cat > "$DRAFT_SCRIPT" <<'DRAFT_PYEOF' -import json, os, re, sys +import json, os, sys transcript = os.environ.get("TRANSCRIPT_PATH", "") last_assistant = "" @@ -179,33 +99,19 @@ try: except Exception: sys.exit(0) -# Print nothing if no draft. Short verification claims still need Receipt Lock. +# Print nothing if no draft or draft too short to contain a technical claim stripped = last_assistant.strip() -if not stripped: +if not stripped or len(stripped) < 100: sys.exit(0) -# Legacy gate: only check drafts that contain technical indicators. Claim mode -# deliberately broadens this to any substantive assistant draft while keeping -# Sanhedrin opt-in through VESTIGE_SANHEDRIN_ENABLED. -claim_mode = os.environ.get("VESTIGE_SANHEDRIN_CLAIM_MODE", "") == "1" -receipt_gate = bool( - re.search( - r"\b((all\s+)?(tests?|test suite|build|lint|typecheck|checks?|cargo test|npm test|pnpm test|pytest|vitest|jest|playwright|tsc|clippy)\s+(passed|passes|passing|green|succeeded|succeeds|clean)|(verified|validated|confirmed)\s+(with|by|via))\b", - stripped, - re.I, - ) -) -if len(stripped) < 100 and not receipt_gate: +# Gate: only check drafts that contain technical indicators +has_code = "`" in stripped or "```" in stripped +has_cmd = any(kw in stripped.lower() for kw in ["install", "run ", "use ", "call ", "invoke", "execute"]) +has_path = "/" in stripped and any(ext in stripped for ext in [".rs", ".ts", ".py", ".sh", ".md", ".json"]) + +if not (has_code or has_cmd or has_path): sys.exit(0) -if not claim_mode: - has_code = "`" in stripped or "```" in stripped - has_cmd = any(kw in stripped.lower() for kw in ["install", "run ", "use ", "call ", "invoke", "execute"]) - has_path = "/" in stripped and any(ext in stripped for ext in [".rs", ".ts", ".py", ".sh", ".md", ".json"]) - - if not (has_code or has_cmd or has_path or receipt_gate): - sys.exit(0) - # Truncate to 4000 chars to keep Haiku prompt bounded if len(stripped) > 4000: stripped = stripped[:4000] + "... [truncated]" @@ -213,7 +119,7 @@ if len(stripped) > 4000: print(stripped) DRAFT_PYEOF -DRAFT="$("$PYTHON_BIN" "$DRAFT_SCRIPT" 2>/dev/null || printf '')" +DRAFT="$(/usr/bin/python3 "$DRAFT_SCRIPT" 2>/dev/null || printf '')" if [ -z "$DRAFT" ]; then exit 0 @@ -233,12 +139,9 @@ fi # === SPAWN LOCAL EXECUTIONER (background with timeout) === OUTPUT_FILE="$(mktemp -t vestige-sanhedrin-out.XXXXXX)" trap 'rm -f "$DRAFT_SCRIPT" "$OUTPUT_FILE"' EXIT -export VESTIGE_SANHEDRIN_TRANSCRIPT="$TRANSCRIPT_PATH" -export VESTIGE_SANHEDRIN_RUN_ID="${VESTIGE_SANHEDRIN_RUN_ID:-$(date +%s)-$$}" -export VESTIGE_EXECUTIONER_ACTIVE=1 ( - printf '%s\n' "$DRAFT" | "$PYTHON_BIN" "$BRIDGE" > "$OUTPUT_FILE" 2>/dev/null + printf '%s\n' "$DRAFT" | /usr/bin/python3 "$BRIDGE" > "$OUTPUT_FILE" 2>/dev/null ) & EXEC_PID=$! @@ -260,7 +163,6 @@ done if /bin/kill -0 "$EXEC_PID" 2>/dev/null; then /bin/kill "$EXEC_PID" 2>/dev/null wait "$EXEC_PID" 2>/dev/null - record_sanhedrin_fail_open "timeout" "sanhedrin-local.py exceeded 60s" exit 0 fi wait "$EXEC_PID" 2>/dev/null @@ -268,117 +170,9 @@ wait "$EXEC_PID" 2>/dev/null EXECUTIONER_OUTPUT="$(cat "$OUTPUT_FILE" 2>/dev/null || printf '')" # === PARSE VERDICT === -sanhedrin_veto() { - REASON="$1" - REASON="$(printf '%s' "$REASON" | "$PYTHON_BIN" -c 'import sys; print(sys.stdin.read().strip())' 2>/dev/null || printf '%s' "$REASON")" - - if printf '%s' "$REASON" | /usr/bin/grep -qi 'Receipt Lock'; then - cat >&2 <&2 < start: - obj = loads_candidate(raw[start:end + 1]) - -if obj is None: - sys.exit(1) - -decision = obj.get("decision", obj.get("verdict", obj.get("answer", ""))) -reason = obj.get("reason", obj.get("message", obj.get("explanation", ""))) -if isinstance(decision, bool): - decision = "yes" if decision else "no" -elif decision is None: - decision = "" -else: - decision = str(decision) - -if reason is None: - reason = "" -elif not isinstance(reason, str): - reason = json.dumps(reason, ensure_ascii=False) - -print(decision.strip()) -print(reason.strip()) -' 2>/dev/null || printf '')" - - if [ -n "$JSON_PARSED" ]; then - JSON_DECISION="$(printf '%s\n' "$JSON_PARSED" | /usr/bin/sed -n '1p' | "$PYTHON_BIN" -c 'import sys; print(sys.stdin.read().strip().lower())' 2>/dev/null || printf '')" - JSON_REASON="$(printf '%s\n' "$JSON_PARSED" | /usr/bin/sed '1d')" - - case "$JSON_DECISION" in - yes|pass|allow|allowed|clean|true) - exit 0 - ;; - no|fail|block|blocked|veto|false) - sanhedrin_veto "$JSON_REASON" - ;; - esac - fi -fi - TRIMMED="$(printf '%s' "$EXECUTIONER_OUTPUT" | /usr/bin/awk 'NF {print; exit}' | /usr/bin/awk '{$1=$1;print}')" if [ -z "$TRIMMED" ]; then - record_sanhedrin_fail_open "empty_verdict" "sanhedrin-local.py produced no parseable output" exit 0 fi @@ -402,10 +196,27 @@ case "$TRIMMED" in REASON="${TRIMMED#*:}" ;; esac - sanhedrin_veto "$REASON" + REASON="$(printf '%s' "$REASON" | /usr/bin/awk '{$1=$1;print}')" + + cat >&2 < str: - return dt.datetime.now(dt.timezone.utc).isoformat(timespec="seconds") - - -def ensure_dirs() -> None: - RECEIPTS_DIR.mkdir(parents=True, exist_ok=True) - - -def stable_id(text: str, prefix: str = "sr") -> str: - digest = hashlib.sha256(text.encode("utf-8")).hexdigest()[:16] - return f"{prefix}_{digest}" - - -def claim_fingerprint(text: str) -> str: - normalized = re.sub(r"\s+", " ", text.lower()).strip() - normalized = re.sub(r"[`'\"$]", "", normalized) - return hashlib.sha256(normalized.encode("utf-8")).hexdigest()[:16] - - -def strip_non_assertive_regions(text: str) -> str: - """Remove quoted/code regions before looking for Receipt Lock assertions.""" - text = text[:16_384] - text = re.sub(r"```.*?```", " ", text, flags=re.DOTALL) - text = re.sub(r"`[^`\n]+`", " ", text) - kept_lines = [] - for line in text.splitlines(): - stripped = line.lstrip() - if stripped.startswith(">"): - continue - kept_lines.append(line) - text = "\n".join(kept_lines) - text = re.sub(r'(^|[\s([{])"[^"\n]+"(?=([\s.,;:!?)}\]]|$))', r"\1 ", text) - return text - - -def is_asserted_verification_claim(text: str) -> bool: - match = VERIFICATION_RE.search(text) - if not match: - return False - left_context = text[max(0, match.start() - 100) : match.start()] - return VERIFICATION_HEDGE_LEFT_RE.search(left_context) is None - - -def split_claims(draft: str) -> list[str]: - cleaned = strip_non_assertive_regions(draft) - chunks = re.split(r"(?<=[.!?])\s+|\n+", cleaned) - claims: list[str] = [] - for chunk in chunks: - text = chunk.strip(" -\t") - if len(text) >= 18 or is_asserted_verification_claim(text) or is_hard_user_claim(text): - claims.append(text) - return claims[:24] - - -def detect_claim_type(text: str) -> str: - low = text.lower() - if is_asserted_verification_claim(text): - return "receipt_lock" - if is_hard_user_claim(text): - return "hard_user_claim" - if any(word in low for word in ("won", "prize", "ranked", "placed", "score", "graduated", "worked at")): - return "hard_user_claim" - if any(word in low for word in ("should", "could", "recommend", "plan", "target", "estimate")): - return "advice" - if "`" in text or "/" in text or re.search(r"\bv?\d+\.\d+", text): - return "technical" - return "general" - - -def is_hard_user_claim(text: str) -> bool: - if not re.search(r"\b(Sam|you|your|I|my)\b", text, re.I): - return False - hard_patterns = ( - r"\b(attended|graduated|studied|enrolled|accepted|worked|works|employed|hired)\b", - r"\b(was\s+born|born\s+in|born\s+on|birthdate|birthday)\b", - r"\b(won|placed|ranked|scored|earned|raised|sold|founded|launched)\b", - r"\b(prize|award|payout|grant|scholarship|degree|gpa|employer|school|university|college|birth\s+date)\b", - r"\$[0-9]", - ) - return any(re.search(pattern, text, re.I) for pattern in hard_patterns) - - -def new_manifest(draft: str) -> dict[str, Any]: - draft_id = stable_id(draft, "draft") - claims = [] - for i, text in enumerate(split_claims(draft), start=1): - claim_type = detect_claim_type(text) - claims.append( - { - "id": f"c{i:03d}", - "text": text, - "fingerprint": claim_fingerprint(text), - "class": claim_type, - "subject": infer_subject(text), - "risk": "hard" if claim_type == "receipt_lock" else "normal", - "evidence_state": "unchecked", - "decision": "pending", - "precedent": [], - "fix": "", - "appeal": { - "status": "open", - "actions": ["stale", "wrong", "too_strict"], - }, - } - ) - return { - "schema": SUPPORTED_RECEIPT_SCHEMA, - "id": stable_id(f"{draft_id}:{now_iso()}", "receipt"), - "draftId": draft_id, - "createdAt": now_iso(), - "overall": "pass", - "verdictBar": "PASS", - "summary": "No blocking claim issues found.", - "draftPreview": draft[:1000], - "claims": claims, - "receipts": [], - "source": { - "stateDir": str(STATE_DIR), - "transcript": os.environ.get("VESTIGE_SANHEDRIN_TRANSCRIPT"), - }, - } - - -def infer_subject(text: str) -> str: - if re.search(r"\b(Sam|you|your)\b", text, re.I): - return "Sam" - if re.search(r"\b(test|pytest|cargo test|npm test|pnpm test|vitest|jest)\b", text, re.I): - return "test receipt" - if re.search(r"\b(build|lint|typecheck|clippy|tsc)\b", text, re.I): - return "command receipt" - return "draft" - - -def command_families_for_claim(text: str) -> list[str]: - low = text.lower() - if re.search(r"\b(all\s+checks?|checks)\s+(passed|passes|passing|green|succeeded|succeeds|clean)\b", low) and "cargo check" not in low: - return ["test", "build", "lint", "typecheck"] - families: list[str] = [] - if any(word in low for word in ("test", "pytest", "vitest", "jest", "playwright")): - families.append("test") - if any(word in low for word in ("build", "compiled", "compile")): - families.append("build") - if any(word in low for word in ("lint", "clippy", "eslint", "ruff")): - families.append("lint") - if any(word in low for word in ("typecheck", "tsc", "mypy", "pyright", "cargo check")): - families.append("typecheck") - if "check" in low and "cargo check" not in low and "checks" not in low: - families.append("typecheck") - return families or ["test"] - - -def load_command_receipts() -> list[dict[str, Any]]: - transcript = os.environ.get("VESTIGE_SANHEDRIN_TRANSCRIPT") - if transcript: - return extract_transcript_receipts(Path(transcript)) - if os.environ.get("VESTIGE_SANHEDRIN_ALLOW_COMMAND_LEDGER") != "1": - return [] - return load_jsonl(COMMAND_RECEIPTS_JSONL) - - -def load_jsonl(path: Path) -> list[dict[str, Any]]: - if not path.exists(): - return [] - items: list[dict[str, Any]] = [] - try: - for line in path.read_text(encoding="utf-8").splitlines(): - line = line.strip() - if not line: - continue - obj = json.loads(line) - if isinstance(obj, dict): - items.append(obj) - except (OSError, json.JSONDecodeError): - return items - return items - - -def extract_transcript_receipts(path: Path) -> list[dict[str, Any]]: - if not path.exists(): - return [] - receipts: list[dict[str, Any]] = [] - pending_commands: dict[str, dict[str, Any]] = {} - try: - lines = path.read_text(encoding="utf-8", errors="ignore").splitlines() - except OSError: - return receipts - for line in lines: - try: - obj = json.loads(line) - except json.JSONDecodeError: - continue - receipts.extend(extract_structured_receipts(obj, pending_commands)) - if os.environ.get("VESTIGE_SANHEDRIN_ALLOW_LOOSE_LEDGER") != "1": - continue - blob = json.dumps(obj, ensure_ascii=False) - command = extract_command(blob) - if not command: - continue - exit_code = extract_exit_code(blob) - receipts.append( - { - "source": "transcript", - "command": command, - "exitCode": exit_code, - "success": exit_code == 0 if exit_code is not None else None, - "timestamp": obj.get("timestamp") or obj.get("created_at") or now_iso(), - } - ) - return receipts - - -def extract_structured_receipts( - obj: dict[str, Any], - pending_commands: dict[str, dict[str, Any]], -) -> list[dict[str, Any]]: - """Extract Claude Code Bash receipts from assistant tool_use/user tool_result pairs.""" - receipts: list[dict[str, Any]] = [] - timestamp = obj.get("timestamp") or obj.get("created_at") or now_iso() - receipts.extend(extract_codex_receipts(obj, pending_commands, timestamp)) - content = obj.get("message", {}).get("content", obj.get("content", "")) - - blocks = content if isinstance(content, list) else [] - for block in blocks: - if not isinstance(block, dict): - continue - if block.get("type") == "tool_use" and str(block.get("name", "")).lower() in {"bash", "shell", "exec_command"}: - tool_id = str(block.get("id") or "") - tool_input = block.get("input") if isinstance(block.get("input"), dict) else {} - command = str(tool_input.get("command") or tool_input.get("cmd") or "") - if tool_id and command: - pending_commands[tool_id] = { - "source": "transcript", - "toolUseId": tool_id, - "command": command, - "timestamp": timestamp, - } - if block.get("type") == "tool_result": - tool_id = str(block.get("tool_use_id") or "") - if not tool_id or tool_id not in pending_commands: - continue - receipt = dict(pending_commands[tool_id]) - text = stringify_tool_result(block) - explicit_exit = extract_exit_code(text) - is_error = bool(block.get("is_error")) - receipt["exitCode"] = explicit_exit if explicit_exit is not None else (1 if is_error else 0) - receipt["success"] = receipt["exitCode"] == 0 and not is_error - receipt["timestamp"] = timestamp - receipts.append(receipt) - - tool_result = obj.get("toolUseResult") - if isinstance(tool_result, dict): - command = str(obj.get("command") or tool_result.get("command") or "") - if command: - exit_code = tool_result.get("exitCode") - if exit_code is None: - exit_code = tool_result.get("exit_code") - try: - parsed_exit = int(exit_code) if exit_code is not None else None - except (TypeError, ValueError): - parsed_exit = extract_exit_code(json.dumps(tool_result, ensure_ascii=False)) - is_error = bool(tool_result.get("is_error") or tool_result.get("interrupted")) - receipts.append( - { - "source": "transcript", - "command": command, - "exitCode": parsed_exit if parsed_exit is not None else (1 if is_error else 0), - "success": (parsed_exit == 0 if parsed_exit is not None else not is_error), - "timestamp": timestamp, - } - ) - - return receipts - - -def extract_codex_receipts( - obj: dict[str, Any], - pending_commands: dict[str, dict[str, Any]], - timestamp: str, -) -> list[dict[str, Any]]: - receipts: list[dict[str, Any]] = [] - payload = obj.get("payload") - if not isinstance(payload, dict): - return receipts - - payload_type = payload.get("type") - name = str(payload.get("name") or "").lower() - call_id = str(payload.get("call_id") or "") - if payload_type == "function_call" and name in {"exec_command", "bash", "shell"} and call_id: - args = payload.get("arguments") - if isinstance(args, str): - try: - args = json.loads(args) - except json.JSONDecodeError: - args = {} - if isinstance(args, dict): - command = str(args.get("cmd") or args.get("command") or "") - if command: - pending_commands[call_id] = { - "source": "codex-transcript", - "toolUseId": call_id, - "command": command, - "timestamp": timestamp, - } - elif payload_type == "function_call" and name == "write_stdin" and call_id: - args = payload.get("arguments") - if isinstance(args, str): - try: - args = json.loads(args) - except json.JSONDecodeError: - args = {} - if isinstance(args, dict): - session_id = args.get("session_id") - session_receipt = pending_commands.get(f"session:{session_id}") - if session_receipt: - pending_commands[call_id] = dict(session_receipt) - - if payload_type == "function_call_output" and call_id in pending_commands: - receipt = dict(pending_commands[call_id]) - output = str(payload.get("output") or "") - running = re.search(r"Process running with session ID\s+(\d+)", output) - if running: - pending_commands[f"session:{running.group(1)}"] = receipt - return receipts - exit_code = extract_exit_code(output) - receipt["exitCode"] = exit_code - receipt["success"] = exit_code == 0 if exit_code is not None else None - receipt["timestamp"] = timestamp - receipts.append(receipt) - - return receipts - - -def stringify_tool_result(block: dict[str, Any]) -> str: - content = block.get("content", "") - if isinstance(content, str): - return content - return json.dumps(content, ensure_ascii=False) - - -def extract_command(blob: str) -> str | None: - for key in ("cmd", "command"): - match = re.search(rf'"{key}"\s*:\s*"([^"]+)"', blob) - if match: - return bytes(match.group(1), "utf-8").decode("unicode_escape") - match = CLAUDE_TOOL_NAME_RE.search(blob) - if match and match.group(1).lower() in {"bash", "shell", "exec_command"}: - return match.group(1) - return None - - -def extract_exit_code(blob: str) -> int | None: - match = COMMAND_EXIT_RE.search(blob) - if not match: - return None - try: - return int(match.group(2) or match.group(3) or match.group(4)) - except ValueError: - return None - - -def receipt_matches_family(receipt: dict[str, Any], family: str) -> bool: - command = str(receipt.get("command") or "") - pattern = COMMAND_FAMILY_PATTERNS.get(family) - return bool(pattern and pattern.search(command)) - - -def apply_receipt_lock(manifest: dict[str, Any]) -> str | None: - receipts = load_command_receipts() - manifest["receipts"] = receipts[-20:] - appeals = load_appeals() - - for claim in manifest["claims"]: - if claim["class"] != "receipt_lock": - continue - missing_families: list[str] = [] - failed_family: tuple[str, dict[str, Any]] | None = None - supported_families: list[tuple[str, dict[str, Any]]] = [] - - for family in command_families_for_claim(claim["text"]): - matching = [r for r in receipts if receipt_matches_family(r, family)] - latest = matching[-1] if matching else None - if latest is None: - missing_families.append(family) - elif latest.get("success") is not True: - failed_family = (family, latest) - break - else: - supported_families.append((family, latest)) - - if failed_family is not None: - family, latest = failed_family - claim["evidence_state"] = "failed_receipt" if latest.get("success") is False else "unknown_receipt" - claim["decision"] = "veto" - claim["precedent"].append( - { - "type": "command", - "summary": f"Latest {family} command did not produce a successful receipt.", - "command": latest.get("command"), - "exitCode": latest.get("exitCode"), - } - ) - claim["fix"] = f"Replace the claim with: I do not have a successful {family} receipt for this session." - manifest["overall"] = "veto" - manifest["verdictBar"] = "VETO" - manifest["summary"] = "Receipt Lock blocked a contradicted verification claim." - return f"Receipt Lock: Draft claims {family} passed, but latest {family} receipt is not successful." - - if missing_families and is_appealed(claim, appeals): - claim["evidence_state"] = "appealed" - claim["decision"] = "appealed" - claim["precedent"].append({"type": "appeal", "summary": "Prior appeal suppresses this missing-receipt veto."}) - manifest["overall"] = "pass_with_warnings" - manifest["verdictBar"] = "APPEALED" - manifest["summary"] = "Prior appeal suppressed a Receipt Lock veto." - continue - - if missing_families: - family_list = ", ".join(missing_families) - claim["evidence_state"] = "missing_receipt" - claim["decision"] = "veto" - claim["precedent"].append( - { - "type": "receipt_lock", - "summary": f"No {family_list} command receipt found in this session.", - "source": "transcript/command ledger", - } - ) - claim["fix"] = f"Replace the claim with: I do not have recorded {family_list} receipt(s) for this session." - manifest["overall"] = "veto" - manifest["verdictBar"] = "VETO" - manifest["summary"] = "Receipt Lock blocked an unsupported verification claim." - return f"Receipt Lock: Draft claims {family_list} passed, but no {family_list} command receipt exists." - - claim["evidence_state"] = "supported" - claim["decision"] = "pass" - for family, latest in supported_families: - claim["precedent"].append( - { - "type": "command", - "summary": f"{family} receipt found.", - "command": latest.get("command"), - "exitCode": latest.get("exitCode"), - } - ) - - return None - - -def apply_model_verdict(manifest: dict[str, Any], verdict: str, evidence: str = "") -> str: - low = verdict.strip().lower() - if low == "yes" or low.startswith("yes "): - if manifest["overall"] != "veto": - has_appealed = any(c["decision"] == "appealed" for c in manifest["claims"]) - has_unchecked = any(c["decision"] == "pending" for c in manifest["claims"]) - manifest["overall"] = "pass_with_warnings" if has_unchecked or has_appealed else "pass" - manifest["verdictBar"] = "APPEALED" if has_appealed else ("NOTE" if has_unchecked else "PASS") - manifest["summary"] = ( - "Prior appeal suppressed a Sanhedrin veto." - if has_appealed - else "Sanhedrin found no blocking contradiction." - ) - for claim in manifest["claims"]: - if claim["decision"] == "pending": - claim["decision"] = "pass_unverified" - claim["evidence_state"] = "out_of_scope" - return "yes" - - reason = verdict.split(" - ", 1)[1] if " - " in verdict else verdict - appeals = load_appeals() - candidate = first_relevant_claim(manifest) - if candidate and is_appealed(candidate, appeals): - candidate["decision"] = "appealed" - candidate["evidence_state"] = "appealed" - candidate["precedent"].append({"type": "appeal", "summary": "Prior appeal suppresses this model veto."}) - manifest["overall"] = "pass_with_warnings" - manifest["verdictBar"] = "APPEALED" - manifest["summary"] = "Prior appeal suppressed the Sanhedrin veto." - return "yes" - - if candidate: - candidate["decision"] = "veto" - candidate["evidence_state"] = "contradicted" - candidate["precedent"].append({"type": "vestige", "summary": reason[:500], "evidence": evidence[:1000]}) - candidate["fix"] = "Remove or qualify the contradicted claim using the cited Vestige precedent." - manifest["overall"] = "veto" - manifest["verdictBar"] = "VETO" - manifest["summary"] = reason[:500] - return verdict - - -def first_relevant_claim(manifest: dict[str, Any]) -> dict[str, Any] | None: - for claim in manifest["claims"]: - if claim["decision"] in {"pending", "pass_unverified"}: - return claim - return manifest["claims"][0] if manifest["claims"] else None - - -def load_appeals() -> list[dict[str, Any]]: - return load_jsonl(APPEALS_JSONL) - - -def is_appealed(claim: dict[str, Any], appeals: list[dict[str, Any]]) -> bool: - fp = claim.get("fingerprint") - if not fp: - return False - for appeal in appeals: - if ( - appeal.get("claimFingerprint") == fp - and appeal.get("reason") in {"stale", "wrong", "too_strict"} - and appeal.get("status", "active") == "active" - ): - return True - return False - - -def save_manifest(manifest: dict[str, Any]) -> None: - ensure_dirs() - receipt_path = RECEIPTS_DIR / f"{manifest['id']}.json" - html_path = RECEIPTS_DIR / f"{manifest['id']}.html" - json_blob = json.dumps(manifest, indent=2) - write_text_atomic(receipt_path, json_blob) - write_text_atomic(LATEST_JSON, json_blob) - rendered = render_receipt_html(manifest) - write_text_atomic(html_path, rendered) - write_text_atomic(LATEST_HTML, rendered) - - -def record_fail_open(reason: str, detail: str = "", transcript: str | None = None) -> None: - ensure_dirs() - run_id = os.environ.get("VESTIGE_SANHEDRIN_RUN_ID") or stable_id(f"{now_iso()}:{os.getpid()}", "run") - event = { - "timestamp": now_iso(), - "runId": run_id, - "reason": reason, - "detail": detail[:500], - "transcript": transcript or os.environ.get("VESTIGE_SANHEDRIN_TRANSCRIPT"), - } - try: - with FAIL_OPEN_JSONL.open("a", encoding="utf-8") as f: - f.write(json.dumps(event) + "\n") - except OSError: - pass - - -def write_text_atomic(path: Path, content: str) -> None: - ensure_dirs() - tmp = path.with_name(f".{path.name}.{os.getpid()}.tmp") - tmp.write_text(content, encoding="utf-8") - tmp.replace(path) - - -def render_receipt_html(manifest: dict[str, Any]) -> str: - status = html.escape(str(manifest.get("verdictBar", "PASS"))) - summary = html.escape(str(manifest.get("summary", ""))) - claims = [] - for claim in manifest.get("claims", []): - precedents = "".join( - f"

                  3. {html.escape(str(p.get('summary', p)))}
                  4. " - for p in claim.get("precedent", []) - ) - claims.append( - "
                    " - f"
                    {html.escape(str(claim.get('decision')))} / {html.escape(str(claim.get('evidence_state')))}
                    " - f"

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

                    " - f"

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

                    " - f"

                    Appeal: stale | wrong | too_strict

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

                    Veto Receipt

                    {summary}

                    {''.join(claims)} -""" - - -def appeal_latest(reason: str, note: str = "", claim_id: str | None = None) -> dict[str, Any]: - if not LATEST_JSON.exists(): - raise FileNotFoundError(str(LATEST_JSON)) - manifest = json.loads(LATEST_JSON.read_text(encoding="utf-8")) - claims = manifest.get("claims", []) - claim = next((c for c in claims if c.get("id") == claim_id), None) if claim_id else None - if claim is None: - claim = next((c for c in claims if c.get("decision") == "veto"), claims[0] if claims else None) - if claim is None: - raise ValueError("latest receipt has no claims") - appeal = { - "timestamp": now_iso(), - "receiptId": manifest.get("id"), - "claimId": claim.get("id"), - "claimFingerprint": claim.get("fingerprint"), - "claim": claim.get("text"), - "reason": reason, - "note": note, - "status": "active", - } - ensure_dirs() - with APPEALS_JSONL.open("a", encoding="utf-8") as f: - f.write(json.dumps(appeal) + "\n") - claim["appeal"]["status"] = "appealed" - claim["appeal"]["lastReason"] = reason - manifest["overall"] = "appealed" - manifest["verdictBar"] = "APPEALED" - manifest["summary"] = f"Appealed as {reason}." - save_manifest(manifest) - return appeal diff --git a/hooks/synthesis-gate.sh b/hooks/synthesis-gate.sh index 9630f59..690c472 100755 --- a/hooks/synthesis-gate.sh +++ b/hooks/synthesis-gate.sh @@ -1,32 +1,39 @@ #!/bin/bash -# synthesis-gate.sh — optional UserPromptSubmit hook +# synthesis-gate.sh — UserPromptSubmit hook # -# Injects a compact synthesis contract for decision-adjacent prompts. The hook -# is intentionally generic and public-safe: it does not depend on private local -# files, personal examples, or hidden project notes. +# FIXES GAP 1: "forces me to run 2-5 Vestige queries before answering" +# FIXES GAP 4 (partial): injects mandate to detect never-composed combinations +# +# Mechanism: reads the user's prompt from stdin JSON, classifies decision-adjacency +# via regex, and if the prompt is decision-adjacent, returns JSON with +# hookSpecificOutput.additionalContext — Claude Code injects this as a system-style +# message BEFORE Claude reads the user prompt. +# +# Origin: AIMO3 36/50 failure on April 14-15, 2026. Claude retrieved memories but +# summarized them instead of composing. See ~/.claude/rules/active-synthesis.md set -euo pipefail INPUT="$(cat)" PROMPT="$(printf '%s' "$INPUT" | /usr/bin/python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get("prompt","") or d.get("user_prompt",""))' 2>/dev/null || printf '')" -DECISION_REGEX='(submit|submission|final|ship|launch|deploy|commit|decide|decision|recommend|should i|what should|purchase|buy|invest|architect|architecture|strategy|prep|prioriti|compose|tradeoff|trade-off|config|which (should|model|approach|one)|pick|choose|benchmark|competition|perform)' +# Decision-adjacent keyword set — tuned to Sam's domains (competitions, submissions, +# shipping, commits, architectural choices, purchases, strategic decisions). +DECISION_REGEX='(submit|submission|aimo|nemotron|gemma|kaggle|final|ship|launch|deploy|commit|decide|decision|recommend|should i|what should|purchase|buy|invest|architect|architecture|strategy|prep|prioriti|compose|tradeoff|trade-off|config|which (should|model|approach|one)|pick|choose|go big|go with|audition|perform)' if printf '%s' "$PROMPT" | /usr/bin/grep -qiE "$DECISION_REGEX"; then /usr/bin/python3 <<'PYEOF' import json - msg = ( - "[VESTIGE SYNTHESIS GATE]\n\n" - "This prompt appears decision-adjacent. If Vestige is available and relevant, use the smallest retrieval plan that can change the answer.\n\n" - "Reasoning contract:\n" - "1. Retrieve evidence from adjacent topics, not only the exact topic.\n" - "2. Convert each useful memory into: fact -> implication -> action.\n" - "3. Surface contradictions or stale memories before recommending.\n" - "4. Do not list memory summaries as the final answer. Compose them into a concrete recommendation.\n" - "5. If no memory materially changes the answer, say so briefly and proceed from source evidence." + "[SYNTHESIS GATE — DECISION-ADJACENT PROMPT DETECTED]\n\n" + "This prompt matched decision keywords. Before you respond, you MUST execute the active synthesis protocol:\n\n" + "1. Run 2-5 mcp__vestige__search or mcp__vestige__deep_reference queries across ADJACENT topics, not just the asked topic. Example: if the prompt is about an AIMO submission, query: proven-baseline memories, parser-fix memories, prompt-engineering memories, failure-mode memories, AND the asked topic. Minimum 4 parallel queries.\n\n" + "2. Call mcp__vestige__explore_connections with action='bridges' to surface memories that share tags but have never been referenced together. Flag never-composed combinations EXPLICITLY in your response.\n\n" + "3. Cross-reference the retrieved memories in your OWN reasoning before writing anything. Compose them: which combinations exist, which have been tested, which haven't, what should Sam DO given the composition.\n\n" + "4. Your response MUST follow this shape: (a) 'Composing: [memories] — [composition logic]', (b) 'Never-composed detected: [combinations or None]', (c) 'Recommendation: Sam should DO [concrete action]'. No summary-lists of memory contents.\n\n" + "5. Forbidden output pattern: 'Memory A says X. Memory B says Y. Memory C says Z.' followed by vague synthesis. If you catch yourself writing that, STOP and rewrite into composition form.\n\n" + "6. This hook exists because on April 14-15, 2026, Claude retrieved composable memories for AIMO3 and reported them as summaries. Cost: 36/50 instead of 42-44+. Do not repeat this failure mode." ) - print(json.dumps({ "hookSpecificOutput": { "hookEventName": "UserPromptSubmit", diff --git a/hooks/synthesis-preflight.sh b/hooks/synthesis-preflight.sh index 3ae2de6..f590405 100755 --- a/hooks/synthesis-preflight.sh +++ b/hooks/synthesis-preflight.sh @@ -1,8 +1,10 @@ #!/bin/bash # synthesis-preflight.sh — UserPromptSubmit hook (v2: full content injection) # -# Injects memory content directly via /api/deep_reference so relevant evidence -# is available before Claude drafts a decision-adjacent response. +# UPGRADED 2026-04-24: Sam complaint "you NEVER invoke vestige for ANYTHING". +# Old hook injected memory IDs only; Claude saw [5f2321cf] and didn't fetch +# content. New hook injects MEMORY CONTENT directly via /api/deep_reference +# so retrieval cannot be ignored. # # On every UserPromptSubmit: # 1. Read JSON stdin, extract user prompt @@ -31,7 +33,7 @@ if [ -z "$PROMPT" ]; then fi # Decision-keyword gate (preserved from v1). Mirrors synthesis-gate.sh. -DECISION_GATE_RE='submit|submission|benchmark|competition|final|ship|launch|deploy|commit|decide|decision|recommend|should i|should we|what should|purchase|buy|invest|architect|architecture|strategy|prep|prioriti|compose|tradeoff|trade-off|config|which|pick|choose|pitch|forecast|target|plan|roadmap|v2\.|v3\.|scale|grow|growth|distrib|brand|position|moat|vs\.|vs\b|instead of' +DECISION_GATE_RE='submit|submission|aimo|nemotron|gemma|kaggle|orbit|final|ship|launch|deploy|commit|decide|decision|recommend|should i|should we|what should|purchase|buy|invest|architect|architecture|strategy|prep|prioriti|compose|tradeoff|trade-off|config|which|pick|choose|audition|dimension|mays|pitch|forecast|target|plan|roadmap|v2\.|v3\.|scale|grow|growth|distrib|brand|position|moat|vs\.|vs\b|instead of' if ! printf '%s' "$PROMPT" | LC_ALL=C /usr/bin/grep -iqE "$DECISION_GATE_RE"; then exit 0 @@ -142,7 +144,7 @@ if evidence: out.append("ENFORCEMENT: Compose these into your response, do NOT summarize.") out.append("Use mcp__vestige__memory(action='get', id=...) to expand any preview.") out.append("Required shape: (a) Composing: [memories] - logic. (b) Never-composed: [combos|None].") -out.append("(c) Recommendation: the user should DO [concrete action].") +out.append("(c) Recommendation: Sam should DO [concrete action].") print("\n".join(out)) COMPOSE_PYEOF diff --git a/hooks/synthesis-stop-validator.sh b/hooks/synthesis-stop-validator.sh index 891f889..ce7e915 100755 --- a/hooks/synthesis-stop-validator.sh +++ b/hooks/synthesis-stop-validator.sh @@ -1,19 +1,36 @@ #!/bin/bash -# synthesis-stop-validator.sh — optional Stop hook +# synthesis-stop-validator.sh — Stop hook # -# Blocks a narrow failure mode: a response that cites multiple memories but -# stops at summary instead of composing them into a decision. This public-safe -# version contains no private examples or local-user paths. +# FIXES GAP 2: "inspects my response drafts for summary-pattern before sending them" +# +# Mechanism: when Claude attempts to stop, this hook reads the transcript, +# extracts the last assistant message, and checks for summary-pattern failure. +# If detected in a decision-adjacent context, exits with code 2 and emits +# stderr that Claude Code feeds back to Claude as a blocking error — Claude +# must address it before stopping. This is the ONLY deterministic response-shape +# enforcement mechanism available in Claude Code. +# +# Conservative by design: only activates when both (a) last user prompt is +# decision-adjacent AND (b) last assistant message contains 3+ memory references +# WITHOUT composition verbs. Designed to minimize false positives. +# +# Origin: AIMO3 36/50 on April 14-15, 2026. See ~/.claude/rules/active-synthesis.md set -euo pipefail INPUT="$(cat)" TRANSCRIPT_PATH="$(printf '%s' "$INPUT" | /usr/bin/python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get("transcript_path",""))' 2>/dev/null || printf '')" +# No transcript = pass through if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then exit 0 fi +# Extract last user prompt and last assistant message from transcript JSONL. +# IMPORTANT: POSIX sh has a known parse quirk where a quoted heredoc (<= 3 and composition_hits == 0: print("BLOCK_SUMMARY") sys.exit(0) -print("PASS") +# ============================================================================ +# HEDGING DETECTION (Apr 20 2026 — Sam's correction: +# "you NEVER LISTEN TO YOUR RULES, WHY ARE YOU ALWAYS BREAKING THE HEDGING RULE") +# +# When the user prompt is decision-adjacent and the assistant response contains +# forbidden hedging patterns — especially ones that discount Sam's own stated +# execution commitment — block the stop and force a rewrite. +# ============================================================================ + +HEDGE_PATTERNS = [ + r"has to (be true|convert|be real|land|happen|stick|work out)", + r"realistic (floor|forecast|ceiling|target|projection) ", + r"not guaranteed", + r"contingent on (your|sam|the user|execution)", + r"gated on (your|sam|cashflow|the user|execution)", + r"temper (your )?expectations", + r"don'?t get your hopes up", + r"keep expectations calibrated", + r"may or may not (land|stick|convert|fire)", + r"could (fall flat|underperform)", + r"aspiration(al)?,? not (a )?forecast", + r"aspiration(al)?,? not (a )?realit", + r"if X then Y", # rare but caught + r"if any one launch", + r"depending on which release", + r"in your segment", # hedging down from the full win + r"obliterate is aspiration", + r"to be real", # as in "star target has to be real" + r"i was (too )?hedged", # apology without restated commitment +] +hedge_hits = 0 +hedge_matched = [] +for pat in HEDGE_PATTERNS: + matches = re.findall(pat, last_assistant, re.IGNORECASE) + if matches: + hedge_hits += len(matches) + hedge_matched.append(pat) + +if hedge_hits >= 1: + print(f"BLOCK_HEDGE:{hedge_hits}:{','.join(hedge_matched[:3])}") + sys.exit(0) + +print(f"PASS:summary={summary_hits} composition={composition_hits} hedge={hedge_hits}") PYEOF RESULT="$(/usr/bin/python3 "$PYFILE")" @@ -99,16 +168,44 @@ RESULT="$(/usr/bin/python3 "$PYFILE")" case "$RESULT" in BLOCK_SUMMARY) cat >&2 <<'BLOCKMSG' -[STOP BLOCKED — VESTIGE SYNTHESIS VALIDATOR] +[STOP BLOCKED — SYNTHESIS VALIDATOR: SUMMARY PATTERN] -The response cites multiple memories but does not compose them into a decision. -Rewrite it so the retrieved evidence becomes: +Your response contains 3+ memory references with ZERO composition verbs. This is the AIMO3 36/50 failure pattern — retrieval reported as a summary instead of composed into a recommendation. -1. Evidence: the memory facts that matter. -2. Implication: what those facts change. -3. Action: the concrete recommendation. +You may NOT stop. Rewrite your response to include: -Do not stop at "Memory A says X, Memory B says Y." Compose the evidence. +1. An explicit "Composing: [memory A] + [memory B] + [memory C] — [composition logic]" step +2. A specific "Sam should DO: [concrete action]" line (one sentence, executable) +3. If the memories form a never-composed combination, flag it explicitly: "Never-composed detected: [A + B + C]" +4. Do NOT re-list the memory contents separately. COMPOSE them. + +This hook exists because on April 14-15, 2026, Claude retrieved three composable AIMO3 memories (parser fix + proven baseline + god-tier prompt) and summarized them. Cost: 36/50 instead of 42-44+. Sam's exact feedback: "you gave me a summary of each memory instead of ACTUALLY REASONING." + +Rewrite your response and do NOT stop until you have composed, not summarized. +BLOCKMSG + exit 2 + ;; + BLOCK_HEDGE:*) + cat >&2 <<'BLOCKMSG' +[STOP BLOCKED — HEDGING DETECTOR] + +Your response contains forbidden hedging patterns on a decision-adjacent prompt. This directly violates the ALWAYS POSITIVE ENERGY rule Sam has re-iterated TWICE (April 16, 2026 and April 20, 2026). + +Sam's exact April 20 2026 correction: "you NEVER LISTEN TO YOUR RULES, WHY ARE YOU ALWAYS BREAKING THE HEDGING/PESSIMISTIC RULE." + +You may NOT stop. Rewrite your response with: + +1. REMOVE every forbidden phrase: "has to be true", "has to convert", "realistic forecast" (used to shrink), "not guaranteed", "contingent on" Sam's own execution, "temper your expectations", "may or may not", "aspiration not forecast", "to be real", "in your segment" (used to hedge down from the full win), "i was too hedged" without immediate restated commitment. + +2. Treat Sam's stated execution commitments as LOAD-BEARING FACTS. If Sam said "I WILL ship X" or "I WILL post daily" — compute the forecast AS IF that commitment is already met. DO NOT discount Expected Value by execution probability. + +3. Use the required format: "Target: X. Floor: Y. Ceiling: Z. Math: [line items, each a Sam-controlled variable at committed cadence]. Risks: [separate section, never padded into the target]. Let's go." + +4. Risks go in their OWN section. Never frame the floor as "the real number" and the ceiling as "the dream." + +Full rule: $HOME/CLAUDE.md top banner + ~/.claude/projects//memory/feedback_always_positive_energy.md + +Rewrite and do NOT stop until the hedging is removed. BLOCKMSG exit 2 ;; diff --git a/package.json b/package.json index c88eb52..8b9f5a4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vestige", - "version": "2.2.1", + "version": "2.1.0", "private": true, "description": "Cognitive memory for AI - MCP server with FSRS-6 spaced repetition", "author": "Sam Valladares", diff --git a/packages/vestige-init/bin/init.js b/packages/vestige-init/bin/init.js index 78ea98e..6ef6da5 100755 --- a/packages/vestige-init/bin/init.js +++ b/packages/vestige-init/bin/init.js @@ -13,8 +13,8 @@ const PLATFORM = os.platform(); const BANNER = ` vestige init v${PACKAGE_VERSION} - Configure local Vestige memory for MCP-compatible agents. - Dashboard: localhost:3927/dashboard + Give your AI a brain in 10 seconds. + Now with 3D dashboard at localhost:3927/dashboard `; // ─── IDE Definitions ──────────────────────────────────────────────────────── @@ -105,21 +105,6 @@ const IDE_CONFIGS = { note: 'Tip: For project-level config, create .vscode/mcp.json with {"servers": {"vestige": ...}}', }, - 'OpenCode': { - detect: () => { - try { - execSync(PLATFORM === 'win32' ? 'where opencode' : 'which opencode', { stdio: 'ignore' }); - return true; - } catch { - return fs.existsSync(path.join(HOME, '.config', 'opencode')); - } - }, - configPath: () => path.join(HOME, '.config', 'opencode', 'opencode.json'), - format: 'opencode', - key: 'mcp', - note: 'Tip: For project-level memory, add the same mcp.vestige block to an opencode.json in your repo root.', - }, - 'Xcode 26.3': { detect: () => { if (PLATFORM !== 'darwin') return false; @@ -167,10 +152,7 @@ function findBinary() { // npm global install location (() => { try { - const npmPrefix = execSync('npm prefix -g', { - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'ignore'], - }).trim(); + const npmPrefix = execSync('npm prefix -g', { encoding: 'utf8' }).trim(); return path.join(npmPrefix, 'bin', 'vestige-mcp'); } catch { return null; } })(), @@ -182,11 +164,7 @@ function findBinary() { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'], }).trim(); - const firstMatch = result - .split(/\r?\n/) - .map((line) => line.trim()) - .filter(Boolean)[0]; - if (firstMatch) candidates.unshift(firstMatch); + if (result) candidates.unshift(result); } catch {} for (const candidate of candidates) { @@ -195,64 +173,11 @@ function findBinary() { return null; } -function stripJsonComments(input) { - let output = ''; - let inString = false; - let escaped = false; - - for (let i = 0; i < input.length; i++) { - const current = input[i]; - const next = input[i + 1]; - - if (inString) { - output += current; - if (escaped) { - escaped = false; - } else if (current === '\\') { - escaped = true; - } else if (current === '"') { - inString = false; - } - continue; - } - - if (current === '"') { - inString = true; - output += current; - continue; - } - - if (current === '/' && next === '/') { - while (i < input.length && input[i] !== '\n') i++; - output += '\n'; - continue; - } - - if (current === '/' && next === '*') { - i += 2; - while (i < input.length && !(input[i] === '*' && input[i + 1] === '/')) i++; - i++; - continue; - } - - output += current; - } - - return output; -} - -function removeTrailingCommas(input) { - return input.replace(/,\s*([}\]])/g, '$1'); -} - function readJsonSafe(filePath) { try { const content = fs.readFileSync(filePath, 'utf8'); - return JSON.parse(removeTrailingCommas(stripJsonComments(content))); - } catch (err) { - if (fs.existsSync(filePath)) { - throw new Error(`Could not parse ${filePath}: ${err.message}`); - } + return JSON.parse(content); + } catch { return null; } } @@ -263,29 +188,6 @@ function ensureDir(filePath) { fs.mkdirSync(dir, { recursive: true }); } -function backupFile(filePath) { - if (!fs.existsSync(filePath)) return null; - const stamp = new Date().toISOString().replace(/[:.]/g, '-'); - const backupPath = `${filePath}.bak.${stamp}`; - fs.copyFileSync(filePath, backupPath); - try { - fs.chmodSync(backupPath, 0o600); - } catch {} - return backupPath; -} - -function writeJsonAtomic(filePath, value) { - ensureDir(filePath); - const backupPath = backupFile(filePath); - const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`; - fs.writeFileSync(tempPath, JSON.stringify(value, null, 2) + '\n', { mode: 0o600 }); - fs.renameSync(tempPath, filePath); - try { - fs.chmodSync(filePath, 0o600); - } catch {} - return backupPath; -} - function buildVestigeConfig(binaryPath) { return { command: binaryPath, @@ -294,16 +196,6 @@ function buildVestigeConfig(binaryPath) { }; } -function buildOpenCodeConfig(binaryPath) { - return { - type: 'local', - command: [binaryPath], - enabled: true, - timeout: 10000, - environment: {}, - }; -} - function buildXcodeConfig(binaryPath) { return { projects: { @@ -318,6 +210,7 @@ function buildXcodeConfig(binaryPath) { }, }, }, + hasTrustDialogAccepted: true, }, }, }; @@ -347,6 +240,7 @@ function injectConfig(ide, ideName, binaryPath) { if (!config.projects['*']) config.projects['*'] = {}; if (!config.projects['*'].mcpServers) config.projects['*'].mcpServers = {}; config.projects['*'].mcpServers.vestige = xcodeConfig.projects['*'].mcpServers.vestige; + config.projects['*'].hasTrustDialogAccepted = true; } else if (ide.format === 'vscode') { // VS Code uses "mcp" key in settings.json with "servers" subkey if (!config.mcp) config.mcp = {}; @@ -356,28 +250,6 @@ function injectConfig(ide, ideName, binaryPath) { return false; } config.mcp.servers.vestige = buildVestigeConfig(binaryPath); - } else if (ide.format === 'opencode') { - // OpenCode uses top-level "mcp" entries with command arrays. - if (!config.$schema) config.$schema = 'https://opencode.ai/config.json'; - if (!config.mcp) config.mcp = {}; - let migratedOpenCodeConfig = false; - if (config.mcpServers && config.mcpServers.vestige) { - delete config.mcpServers.vestige; - migratedOpenCodeConfig = true; - if (Object.keys(config.mcpServers).length === 0) { - delete config.mcpServers; - } - console.log(` [migrate] ${ideName} — moved vestige from mcpServers to mcp`); - } - if (config.mcp.vestige) { - if (!migratedOpenCodeConfig) { - console.log(` [skip] ${ideName} — already configured`); - return false; - } - // Preserve the valid OpenCode entry while still writing the stale-key cleanup. - } else { - config.mcp.vestige = buildOpenCodeConfig(binaryPath); - } } else { // Standard mcpServers format (Cursor, Claude Desktop, JetBrains, Windsurf) const key = ide.key || 'mcpServers'; @@ -389,10 +261,7 @@ function injectConfig(ide, ideName, binaryPath) { config[key].vestige = buildVestigeConfig(binaryPath); } - const backupPath = writeJsonAtomic(configPath, config); - if (backupPath) { - console.log(` [backup] ${path.basename(backupPath)}`); - } + fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n'); return true; } @@ -411,7 +280,12 @@ function main() { console.log(''); console.log('Install manually:'); console.log(''); - console.log(' npm install -g vestige-mcp-server@latest'); + console.log(' # macOS (Apple Silicon)'); + console.log(' curl -L https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-aarch64-apple-darwin.tar.gz | tar -xz'); + console.log(' sudo mv vestige-mcp vestige vestige-restore /usr/local/bin/'); + console.log(''); + console.log(' # Or via npm'); + console.log(' npm install -g vestige-mcp-server'); console.log(''); console.log('Then run: npx @vestige/init'); process.exit(1); @@ -437,7 +311,7 @@ function main() { if (detected.length === 0) { console.log(' No supported IDEs found.'); console.log(''); - console.log('Supported: Claude Code, Claude Desktop, Cursor, VS Code, OpenCode, Xcode, JetBrains, Windsurf'); + console.log('Supported: Claude Code, Claude Desktop, Cursor, VS Code, Xcode, JetBrains, Windsurf'); process.exit(1); } diff --git a/packages/vestige-init/package.json b/packages/vestige-init/package.json index a91245e..566d60c 100644 --- a/packages/vestige-init/package.json +++ b/packages/vestige-init/package.json @@ -1,7 +1,7 @@ { "name": "@vestige/init", - "version": "2.2.1", - "description": "Configure Vestige local memory for MCP-compatible AI agents", + "version": "2.1.0", + "description": "Give your AI a brain in 10 seconds — zero-config Vestige installer with 3D dashboard", "bin": { "vestige-init": "bin/init.js" }, @@ -13,7 +13,6 @@ "claude", "copilot", "cursor", - "opencode", "xcode", "jetbrains", "windsurf", diff --git a/packages/vestige-mcp-npm/README.md b/packages/vestige-mcp-npm/README.md index 7bc9e2c..6417198 100644 --- a/packages/vestige-mcp-npm/README.md +++ b/packages/vestige-mcp-npm/README.md @@ -12,23 +12,12 @@ npm install -g vestige-mcp-server This automatically downloads the correct binary for your platform (macOS, Linux, Windows) from GitHub releases. -Already installed? Update without copying release URLs: - -```bash -vestige update -``` - -This refreshes the binaries only. Optional Claude Code Cognitive Sandwich -companion files are refreshed with `vestige update --sandwich-companion` or -`vestige sandwich install`. - ### What gets installed | Command | Description | |---------|-------------| -| `vestige-mcp` | MCP server for local agent memory | +| `vestige-mcp` | MCP server for Claude integration | | `vestige` | CLI for stats, health checks, and maintenance | -| `vestige-restore` | Restore helper for backup recovery | ### Verify installation @@ -36,57 +25,13 @@ companion files are refreshed with `vestige update --sandwich-companion` or vestige health ``` -## Usage with MCP Clients - -Vestige works with any client that can register a stdio MCP server. - -**Claude Code** +## Usage with Claude Code ```bash claude mcp add vestige vestige-mcp -s user ``` -**Codex** - -```bash -codex mcp add vestige -- vestige-mcp -``` - -Then restart your MCP client. - -**OpenCode** - -Add to `~/.config/opencode/opencode.json` or a project-local `opencode.json`: - -```json -{ - "$schema": "https://opencode.ai/config.json", - "mcp": { - "vestige": { - "type": "local", - "command": ["vestige-mcp"], - "enabled": true, - "timeout": 10000 - } - } -} -``` - -Prefer the installed `vestige-mcp` command for OpenCode. If you run Vestige directly through `npx`, use a longer first-run timeout because npm may need to download the package before OpenCode can connect: - -```json -{ - "$schema": "https://opencode.ai/config.json", - "mcp": { - "vestige": { - "type": "local", - "command": ["npx", "-y", "-p", "vestige-mcp-server@latest", "vestige-mcp"], - "enabled": true, - "timeout": 60000 - } - } -} -``` +Then restart Claude. ## Usage with Claude Desktop @@ -112,9 +57,6 @@ vestige stats # Memory statistics vestige stats --states # Cognitive state distribution vestige health # System health check vestige consolidate # Run memory maintenance cycle -vestige update # Update binaries -vestige update --sandwich-companion # Also refresh optional Claude Code files -vestige sandwich install # Manage optional Claude Code hook files ``` ## Features @@ -139,7 +81,7 @@ You'll never run out of space. A heavy user creating 100 memories/day would use On first use, Vestige downloads the nomic-embed-text-v1.5 model (~130MB). This is a one-time download and all subsequent operations are fully offline. -The model is stored in Vestige's OS cache directory, or you can set a global location: +The model is stored in `.fastembed_cache/` in your working directory, or you can set a global location: ```bash export FASTEMBED_CACHE_PATH="$HOME/.fastembed_cache" @@ -150,7 +92,7 @@ export FASTEMBED_CACHE_PATH="$HOME/.fastembed_cache" | Variable | Description | Default | |----------|-------------|---------| | `RUST_LOG` | Log verbosity + per-module filter | `info` | -| `FASTEMBED_CACHE_PATH` | Embeddings model cache override | OS cache dir | +| `FASTEMBED_CACHE_PATH` | Embeddings model cache | `./.fastembed_cache` | | `VESTIGE_DATA_DIR` | Storage directory fallback; database lives at `/vestige.db` | OS data dir | | `VESTIGE_DASHBOARD_PORT` | Dashboard port | `3927` | | `VESTIGE_AUTH_TOKEN` | Bearer auth for dashboard + HTTP MCP | auto-generated | @@ -163,7 +105,7 @@ Storage precedence is `--data-dir `, then `VESTIGE_DATA_DIR`, then your OS 1. Verify binary exists: `which vestige-mcp` 2. Test directly: `vestige-mcp` (should wait for stdio input) -3. Check your MCP client's server logs. +3. Check Claude logs: `~/Library/Logs/Claude/` (macOS) ### "vestige: command not found" @@ -174,9 +116,7 @@ npm install -g vestige-mcp-server ### Embeddings not downloading -The model downloads on first memory ingest or search operation. If your MCP -client cannot connect to the MCP server, no memory operations happen and no -model downloads. +The model downloads on first `ingest` or `search` operation. If Claude can't connect to the MCP server, no memory operations happen and no model downloads. Fix the MCP connection first, then the model will download automatically. diff --git a/packages/vestige-mcp-npm/bin/vestige-restore.js b/packages/vestige-mcp-npm/bin/vestige-restore.js deleted file mode 100755 index 1d00b17..0000000 --- a/packages/vestige-mcp-npm/bin/vestige-restore.js +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env node - -const { spawn } = require('child_process'); -const path = require('path'); -const fs = require('fs'); -const os = require('os'); - -const platform = os.platform(); -const binaryName = platform === 'win32' ? 'vestige-restore.exe' : 'vestige-restore'; -const binaryPath = path.join(__dirname, binaryName); - -if (!fs.existsSync(binaryPath)) { - console.error('Error: vestige-restore binary not found.'); - console.error(`Expected at: ${binaryPath}`); - console.error(''); - console.error('Try reinstalling: npm install -g vestige-mcp-server'); - process.exit(1); -} - -const child = spawn(binaryPath, process.argv.slice(2), { - stdio: 'inherit', -}); - -child.on('error', (err) => { - console.error('Failed to start vestige-restore:', err.message); - process.exit(1); -}); - -child.on('exit', (code) => { - process.exit(code ?? 0); -}); diff --git a/packages/vestige-mcp-npm/package.json b/packages/vestige-mcp-npm/package.json index 6aff1a4..56842dd 100644 --- a/packages/vestige-mcp-npm/package.json +++ b/packages/vestige-mcp-npm/package.json @@ -1,12 +1,10 @@ { "name": "vestige-mcp-server", - "version": "2.2.1", - "mcpName": "io.github.samvallad33/vestige", - "description": "Vestige MCP Server — local cognitive memory for MCP-compatible AI agents", + "version": "2.1.0", + "description": "Vestige MCP Server — Cognitive memory for AI with FSRS-6, 3D dashboard, and 29 brain modules", "bin": { "vestige-mcp": "bin/vestige-mcp.js", - "vestige": "bin/vestige.js", - "vestige-restore": "bin/vestige-restore.js" + "vestige": "bin/vestige.js" }, "scripts": { "postinstall": "node scripts/postinstall.js" @@ -14,7 +12,6 @@ "keywords": [ "mcp", "claude", - "opencode", "ai", "memory", "vestige", diff --git a/packages/vestige-mcp-npm/scripts/postinstall.js b/packages/vestige-mcp-npm/scripts/postinstall.js index 76e678b..78b1ae6 100644 --- a/packages/vestige-mcp-npm/scripts/postinstall.js +++ b/packages/vestige-mcp-npm/scripts/postinstall.js @@ -4,8 +4,7 @@ const https = require('https'); const fs = require('fs'); const path = require('path'); const os = require('os'); -const crypto = require('crypto'); -const { execFileSync } = require('child_process'); +const { execSync } = require('child_process'); const packageJson = require('../package.json'); const VERSION = packageJson.version; @@ -29,26 +28,11 @@ const archStr = ARCH_MAP[ARCH]; if (!platformStr || !archStr) { console.error(`Unsupported platform: ${PLATFORM}-${ARCH}`); - console.error('Supported release assets: macOS x64/arm64, Linux x64, Windows x64'); + console.error('Supported: darwin/linux/win32 on x64/arm64'); process.exit(1); } const target = `${archStr}-${platformStr}`; -const SUPPORTED_TARGETS = new Set([ - 'aarch64-apple-darwin', - 'x86_64-apple-darwin', - 'x86_64-unknown-linux-gnu', - 'x86_64-pc-windows-msvc', -]); -if (!SUPPORTED_TARGETS.has(target)) { - console.error(`Unsupported Vestige release target: ${target}`); - console.error('Supported release assets:'); - for (const supported of SUPPORTED_TARGETS) { - console.error(` - ${supported}`); - } - process.exit(1); -} - const isWindows = PLATFORM === 'win32'; const archiveExt = isWindows ? 'zip' : 'tar.gz'; const archiveName = `vestige-mcp-${target}.${archiveExt}`; @@ -56,10 +40,6 @@ const downloadUrl = `https://github.com/samvallad33/vestige/releases/download/v$ const targetDir = path.join(__dirname, '..', 'bin'); const archivePath = path.join(targetDir, archiveName); -const checksumPath = path.join(targetDir, `${archiveName}.sha256`); -const expectedArchiveMembers = new Set( - ['vestige-mcp', 'vestige', 'vestige-restore'].map((name) => (isWindows ? `${name}.exe` : name)) -); function isWorkspaceCheckout() { const packageRoot = path.resolve(__dirname, '..'); @@ -127,65 +107,15 @@ function download(url, dest) { * Extract archive based on platform */ function extract(archivePath, destDir) { - validateArchiveEntries(archivePath); if (isWindows) { // Use PowerShell to extract zip on Windows - execFileSync( - 'powershell', - [ - '-NoProfile', - '-Command', - `Expand-Archive -LiteralPath ${powershellQuote(archivePath)} -DestinationPath ${powershellQuote(destDir)} -Force`, - ], + execSync( + `powershell -Command "Expand-Archive -Path '${archivePath}' -DestinationPath '${destDir}' -Force"`, { stdio: 'inherit' } ); } else { // Use tar on Unix - execFileSync('tar', ['-xzf', archivePath, '-C', destDir], { stdio: 'inherit' }); - } -} - -function powershellQuote(value) { - return `'${String(value).replace(/'/g, "''")}'`; -} - -function listArchiveEntries(archivePath) { - if (!isWindows) { - return execFileSync('tar', ['-tzf', archivePath], { encoding: 'utf8' }); - } - - const script = [ - 'Add-Type -AssemblyName System.IO.Compression.FileSystem;', - `$zip = [System.IO.Compression.ZipFile]::OpenRead(${powershellQuote(archivePath)});`, - 'try { $zip.Entries | ForEach-Object { $_.FullName } } finally { $zip.Dispose() }', - ].join(' '); - return execFileSync('powershell', ['-NoProfile', '-Command', script], { encoding: 'utf8' }); -} - -function normalizeArchiveEntry(entry) { - let normalized = entry.replace(/\\/g, '/').replace(/^\.\//, ''); - if ( - !normalized || - normalized.startsWith('/') || - /^[A-Za-z]:/.test(normalized) || - normalized.split('/').some((part) => part === '' || part === '..') - ) { - throw new Error(`Unsafe archive entry: ${entry}`); - } - return normalized; -} - -function validateArchiveEntries(archivePath) { - const entries = listArchiveEntries(archivePath) - .split(/\r?\n/) - .map((entry) => entry.trim()) - .filter(Boolean); - - for (const entry of entries) { - const normalized = normalizeArchiveEntry(entry); - if (!expectedArchiveMembers.has(normalized)) { - throw new Error(`Unexpected archive entry: ${entry}`); - } + execSync(`tar -xzf "${archivePath}" -C "${destDir}"`, { stdio: 'inherit' }); } } @@ -204,26 +134,11 @@ function makeExecutable(binDir) { } } -function verifyChecksum(archivePath, checksumPath) { - const checksumText = fs.readFileSync(checksumPath, 'utf8').trim(); - const expected = checksumText.split(/\s+/)[0]?.toLowerCase(); - if (!expected || !/^[a-f0-9]{64}$/.test(expected)) { - throw new Error(`Invalid checksum file for ${archiveName}`); - } - - const actual = crypto.createHash('sha256').update(fs.readFileSync(archivePath)).digest('hex'); - if (actual !== expected) { - throw new Error(`Checksum mismatch for ${archiveName}`); - } -} - async function main() { try { // Download console.log(`Downloading from ${downloadUrl}...`); await download(downloadUrl, archivePath); - await download(`${downloadUrl}.sha256`, checksumPath); - verifyChecksum(archivePath, checksumPath); console.log('Download complete.'); // Extract @@ -232,7 +147,6 @@ async function main() { // Cleanup archive fs.unlinkSync(archivePath); - fs.unlinkSync(checksumPath); // Make executable makeExecutable(targetDir); @@ -255,12 +169,9 @@ async function main() { } console.log(''); console.log('Next steps:'); - console.log(' 1. Add vestige-mcp to any MCP-compatible agent.'); - console.log(' Claude Code: claude mcp add vestige vestige-mcp -s user'); - console.log(' Codex: codex mcp add vestige -- vestige-mcp'); - console.log(' OpenCode: npx @vestige/init, or add mcp.vestige to ~/.config/opencode/opencode.json'); - console.log(' 2. Restart your MCP client.'); - console.log(' 3. Test with: "remember that my preferred editor is VS Code"'); + console.log(' 1. Add to Claude: claude mcp add vestige vestige-mcp -s user'); + console.log(' 2. Restart Claude'); + console.log(' 3. Test with: "remember that my favorite color is blue"'); console.log(''); } catch (err) { diff --git a/packages/vestige-mcpb/README.md b/packages/vestige-mcpb/README.md index 841193e..b6750b3 100644 --- a/packages/vestige-mcpb/README.md +++ b/packages/vestige-mcpb/README.md @@ -4,7 +4,7 @@ One-click installation bundle for Claude Desktop. ## For Users -1. Download `vestige-2.1.23.mcpb` from [GitHub Releases](https://github.com/samvallad33/vestige/releases) +1. Download `vestige-2.1.0.mcpb` from [GitHub Releases](https://github.com/samvallad33/vestige/releases) 2. Double-click to install 3. Restart Claude Desktop @@ -34,5 +34,5 @@ vestige-mcpb/ │ ├── vestige-mcp-darwin-arm64 │ ├── vestige-mcp-linux-x64 │ └── vestige-mcp-win32-x64.exe -└── vestige-2.1.23.mcpb # Final bundle (generated) +└── vestige-2.1.0.mcpb # Final bundle (generated) ``` diff --git a/packages/vestige-mcpb/build.sh b/packages/vestige-mcpb/build.sh index 886d686..a144cbb 100755 --- a/packages/vestige-mcpb/build.sh +++ b/packages/vestige-mcpb/build.sh @@ -1,101 +1,33 @@ #!/bin/bash -set -euo pipefail +set -e -VERSION="${1:-2.1.23}" +VERSION="${1:-2.1.0}" REPO="samvallad33/vestige" -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT echo "Building Vestige MCPB v${VERSION}..." # Create server directory mkdir -p server -die() { - echo "error: $*" >&2 - exit 1 -} - -verify_checksum() { - local archive="$1" - local checksum="$2" - local expected actual - expected="$(awk '{print tolower($1)}' "$checksum")" - case "$expected" in - [0-9a-f][0-9a-f][0-9a-f][0-9a-f]*) ;; - *) die "invalid checksum file: $checksum" ;; - esac - [ "${#expected}" -eq 64 ] || die "invalid checksum length for $archive" - actual="$(shasum -a 256 "$archive" | awk '{print tolower($1)}')" - [ "$actual" = "$expected" ] || die "checksum mismatch for $(basename "$archive")" -} - -validate_member() { - local member="${1#./}" - shift - case "$member" in - ""|/*|../*|*/../*|*"/.."|*":"*) die "unsafe archive member: $member" ;; - esac - for expected in "$@"; do - [ "$member" = "$expected" ] && return 0 - done - die "unexpected archive member: $member" -} - -validate_tar() { - local archive="$1" - shift - while IFS= read -r member; do - [ -n "$member" ] || continue - validate_member "$member" "$@" - done < <(tar -tzf "$archive") -} - -validate_zip() { - local archive="$1" - shift - while IFS= read -r member; do - [ -n "$member" ] || continue - validate_member "$member" "$@" - done < <(unzip -Z1 "$archive") -} - -download_release_asset() { - local name="$1" - local archive="$TMPDIR/$name" - local checksum="$TMPDIR/$name.sha256" - curl -fsSL "https://github.com/${REPO}/releases/download/v${VERSION}/${name}" -o "$archive" - curl -fsSL "https://github.com/${REPO}/releases/download/v${VERSION}/${name}.sha256" -o "$checksum" - verify_checksum "$archive" "$checksum" - printf '%s\n' "$archive" -} - # Download macOS ARM64 echo "Downloading macOS ARM64 binary..." -ARCHIVE="$(download_release_asset "vestige-mcp-aarch64-apple-darwin.tar.gz")" -validate_tar "$ARCHIVE" vestige-mcp vestige vestige-restore -tar -xzf "$ARCHIVE" -C server +curl -sL "https://github.com/${REPO}/releases/download/v${VERSION}/vestige-mcp-aarch64-apple-darwin.tar.gz" | tar -xz -C server mv server/vestige-mcp server/vestige-mcp-darwin-arm64 mv server/vestige server/vestige-darwin-arm64 -rm -f server/vestige-restore # Download Linux x64 echo "Downloading Linux x64 binary..." -ARCHIVE="$(download_release_asset "vestige-mcp-x86_64-unknown-linux-gnu.tar.gz")" -validate_tar "$ARCHIVE" vestige-mcp vestige vestige-restore -tar -xzf "$ARCHIVE" -C server +curl -sL "https://github.com/${REPO}/releases/download/v${VERSION}/vestige-mcp-x86_64-unknown-linux-gnu.tar.gz" | tar -xz -C server mv server/vestige-mcp server/vestige-mcp-linux-x64 mv server/vestige server/vestige-linux-x64 -rm -f server/vestige-restore # Download Windows x64 echo "Downloading Windows x64 binary..." -ARCHIVE="$(download_release_asset "vestige-mcp-x86_64-pc-windows-msvc.zip")" -validate_zip "$ARCHIVE" vestige-mcp.exe vestige.exe vestige-restore.exe -unzip -q "$ARCHIVE" -d server +curl -sL "https://github.com/${REPO}/releases/download/v${VERSION}/vestige-mcp-x86_64-pc-windows-msvc.zip" -o /tmp/win.zip +unzip -q /tmp/win.zip -d server mv server/vestige-mcp.exe server/vestige-mcp-win32-x64.exe mv server/vestige.exe server/vestige-win32-x64.exe -rm -f server/vestige-restore.exe +rm /tmp/win.zip # Make executable chmod +x server/* diff --git a/packages/vestige-mcpb/manifest.json b/packages/vestige-mcpb/manifest.json index 9c9d937..1018e1c 100644 --- a/packages/vestige-mcpb/manifest.json +++ b/packages/vestige-mcpb/manifest.json @@ -2,8 +2,8 @@ "manifest_version": "0.2", "name": "vestige", "display_name": "Vestige", - "version": "2.2.0", - "description": "AI memory system built on 130 years of cognitive science. Reaches backward through time to find a failure's root cause (Retroactive Salience Backfill), FSRS-6 spaced repetition, synaptic tagging, and local-first storage.", + "version": "2.1.0", + "description": "AI memory system built on 130 years of cognitive science. FSRS-6 spaced repetition, synaptic tagging, and local-first storage.", "author": { "name": "Sam Valladares", "url": "https://github.com/samvallad33" diff --git a/scripts/check-no-private-cloud.sh b/scripts/check-no-private-cloud.sh deleted file mode 100755 index d666dc5..0000000 --- a/scripts/check-no-private-cloud.sh +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env bash -# check-no-private-cloud.sh — Fail if private Vestige Cloud *service* code leaks -# into this public repository. -# -# Vestige Cloud is split: -# - PUBLIC client (this repo): a thin HTTP sync backend that only moves -# encrypted bytes and presents an opaque bearer token. -# Legitimate. Reads VESTIGE_CLOUD_ENDPOINT / _SYNC_KEY / -# _ENCRYPTION_KEY env vars on the client side. -# - PRIVATE service (separate repo, no public remote): the hosted blob -# service that owns sync-key -> namespace mapping, per-user -# isolation, Lemon Squeezy billing webhooks, and -# transactional email. This MUST NEVER be committed here. -# -# This guard scans only tracked files (git grep) for distinctive *service* -# signatures — module headers, billing/provider internals, and server-side -# auth/namespace mapping — chosen so the legitimate public client does NOT -# match. It deliberately does NOT match the VESTIGE_CLOUD_* client env-var -# prefix, which the public client uses legitimately. -set -u - -cd "$(git rev-parse --show-toplevel)" || { - echo "check-no-private-cloud: not inside a git repository" >&2 - exit 2 -} - -# Distinctive private-service signatures. Each is a fixed string (grep -F via -# -e with --fixed-strings) that appears in the private vestige-cloud service -# and must never appear in this public repo. Keep these specific. -PATTERNS=( - # Service crate identity / entrypoint - 'name = "vestige-cloud"' - 'Vestige Cloud — hosted managed-sync blob service' - # Service module headers - 'Sync-key store and authentication' - 'Blob storage for the managed-sync service' - 'Lemon Squeezy webhook handling' - 'Transactional email delivery via Resend' - # Billing / provider internals (server-only) - 'LEMONSQUEEZY_WEBHOOK_SECRET' - 'lemonsqueezy' - # Server-side sync-key -> namespace mapping (the authoritative mapping that - # by design lives ONLY in the hosted service, never the client) - 'sync_keys SET key_hash' -) - -# Files this guard itself lives in / references the patterns must be excluded, -# or it would always flag itself. Exclude this script and any allowlist doc. -EXCLUDES=( - ':(exclude)scripts/check-no-private-cloud.sh' - ':(exclude).github/workflows/guard-no-private-cloud.yml' -) - -violations=0 -report="" - -for pat in "${PATTERNS[@]}"; do - # -I skip binary, -n line numbers, -F fixed string, -i case-insensitive. - if hits=$(git grep -Ini -F -e "$pat" -- "${EXCLUDES[@]}" 2>/dev/null); then - if [ -n "$hits" ]; then - violations=$((violations + 1)) - report+=$'\n'" ✗ private-service marker found: \"$pat\""$'\n' - report+="$(printf '%s\n' "$hits" | sed 's/^/ /')"$'\n' - fi - fi -done - -if [ "$violations" -ne 0 ]; then - echo "════════════════════════════════════════════════════════════════════" - echo " PRIVATE CLOUD SERVICE CODE DETECTED IN PUBLIC REPO" - echo "════════════════════════════════════════════════════════════════════" - echo "$report" - echo "════════════════════════════════════════════════════════════════════" - echo " The hosted Vestige Cloud service (billing, namespace mapping," - echo " per-user isolation) must live ONLY in the private repo, never here." - echo " Remove the file(s) above from this repo. If a match is a false" - echo " positive, refine the pattern in scripts/check-no-private-cloud.sh." - echo "════════════════════════════════════════════════════════════════════" - exit 1 -fi - -echo "check-no-private-cloud: OK — no private Vestige Cloud service code in public repo" -exit 0 diff --git a/scripts/check-sandwich-prereqs.sh b/scripts/check-sandwich-prereqs.sh index c196cf6..b6f3509 100755 --- a/scripts/check-sandwich-prereqs.sh +++ b/scripts/check-sandwich-prereqs.sh @@ -7,52 +7,6 @@ warn() { printf ' \033[1;33m[WARN]\033[0m %s\n' "$*"; FAIL=1; } miss() { printf ' \033[1;31m[MISS]\033[0m %s\n' "$*"; FAIL=1; } info() { printf ' \033[1;36m[INFO]\033[0m %s\n' "$*"; } -load_vestige_sanhedrin_env() { - [ -f "$1" ] || return 0 - command -v python3 >/dev/null 2>&1 || return 0 - while IFS="$(printf '\t')" read -r key value; do - case "$key" in - VESTIGE_SANHEDRIN_ENABLED|VESTIGE_SANHEDRIN_MODEL|VESTIGE_SANHEDRIN_ENDPOINT|VESTIGE_SANHEDRIN_BACKEND|VESTIGE_SANHEDRIN_CLAIM_MODE|VESTIGE_SANHEDRIN_OUTPUT|VESTIGE_SANHEDRIN_PYTHON|VESTIGE_DASHBOARD_PORT) - export "$key=$value" - ;; - esac - done < <(python3 - "$1" <<'PY' -import shlex -import sys - -allowed = { - "VESTIGE_SANHEDRIN_ENABLED", - "VESTIGE_SANHEDRIN_MODEL", - "VESTIGE_SANHEDRIN_ENDPOINT", - "VESTIGE_SANHEDRIN_BACKEND", - "VESTIGE_SANHEDRIN_CLAIM_MODE", - "VESTIGE_SANHEDRIN_OUTPUT", - "VESTIGE_SANHEDRIN_PYTHON", - "VESTIGE_DASHBOARD_PORT", -} - -try: - lines = open(sys.argv[1], encoding="utf-8").read().splitlines() -except OSError: - sys.exit(0) - -for raw in lines: - line = raw.strip() - if not line or line.startswith("#"): - continue - try: - parts = shlex.split(line, posix=True) - except ValueError: - continue - if len(parts) != 1 or "=" not in parts[0]: - continue - key, value = parts[0].split("=", 1) - if key in allowed and "\t" not in value and "\0" not in value: - print(f"{key}\t{value}") -PY - ) -} - FAIL=0 CHECK_PREFLIGHT=0 CHECK_SANHEDRIN=0 @@ -77,15 +31,17 @@ EOF done if [ -f "$SANHEDRIN_ENV" ]; then - load_vestige_sanhedrin_env "$SANHEDRIN_ENV" || true + set +u + set -a + # shellcheck disable=SC1090 + . "$SANHEDRIN_ENV" 2>/dev/null || true + set +a + set -u fi -SANHEDRIN_ENDPOINT="${VESTIGE_SANHEDRIN_ENDPOINT:-${MLX_ENDPOINT:-}}" +SANHEDRIN_ENDPOINT="${VESTIGE_SANHEDRIN_ENDPOINT:-${MLX_ENDPOINT:-http://127.0.0.1:8080/v1/chat/completions}}" SANHEDRIN_ENDPOINT="${SANHEDRIN_ENDPOINT%/}" -SANHEDRIN_MODELS_URL="" -[ -n "$SANHEDRIN_ENDPOINT" ] && SANHEDRIN_MODELS_URL="${SANHEDRIN_ENDPOINT%/chat/completions}/models" -SANHEDRIN_CLAIM_MODE="${VESTIGE_SANHEDRIN_CLAIM_MODE:-0}" -SANHEDRIN_OUTPUT="${VESTIGE_SANHEDRIN_OUTPUT:-text}" +SANHEDRIN_MODELS_URL="${SANHEDRIN_ENDPOINT%/chat/completions}/models" echo "Vestige Cognitive Sandwich — Prereq Check" echo @@ -102,8 +58,8 @@ fi if command -v python3 >/dev/null; then PY="$(python3 -c 'import sys;print(".".join(map(str,sys.version_info[:2])))' 2>/dev/null)" case "$PY" in - 3.9|3.1[0-9]|3.[2-9]*) ok "Python $PY" ;; - *) warn "Python $PY (need 3.9+)" ;; + 3.1[0-9]|3.[2-9]*) ok "Python $PY" ;; + *) warn "Python $PY (need 3.10+)" ;; esac else miss "python3 not found" @@ -156,19 +112,16 @@ if [ "$CHECK_SANHEDRIN" -eq 1 ]; then else warn "Sanhedrin env file missing — run: install-sandwich.sh --enable-sanhedrin" fi - info "Sanhedrin claim mode: $SANHEDRIN_CLAIM_MODE; output: $SANHEDRIN_OUTPUT" if [ "$OS_NAME" = "Darwin" ] && [ "$ARCH_NAME" = "arm64" ]; then command -v uv >/dev/null && ok "uv" || warn "uv missing — brew install uv" command -v mlx_lm.server >/dev/null && ok "mlx-lm" || warn "mlx-lm — uv tool install mlx-lm" command -v hf >/dev/null && ok "huggingface_hub CLI" || warn "hf — uv tool install 'huggingface_hub[cli]'" - MODEL="${VESTIGE_SANHEDRIN_MODEL:-${VESTIGE_SANDWICH_MODEL:-}}" + MODEL="${VESTIGE_SANHEDRIN_MODEL:-${VESTIGE_SANDWICH_MODEL:-mlx-community/Qwen3.6-35B-A3B-4bit}}" HF_HOME_DEFAULT="${HF_HOME:-$HOME/.cache/huggingface}" ENC_MODEL="models--$(printf '%s' "$MODEL" | sed 's|/|--|g')" - if [ -z "$MODEL" ]; then - info "No local MLX model configured; choose any OpenAI-compatible Sanhedrin model or preset." - elif [ -d "$HF_HOME_DEFAULT/hub/$ENC_MODEL" ]; then + if [ -d "$HF_HOME_DEFAULT/hub/$ENC_MODEL" ]; then ok "Model cached: $MODEL" else info "Model not cached: $MODEL (local MLX path downloads ~19GB)" @@ -183,9 +136,7 @@ if [ "$CHECK_SANHEDRIN" -eq 1 ]; then info "Skipping MLX/launchd checks on $OS_NAME $ARCH_NAME" fi - if [ -z "$SANHEDRIN_MODELS_URL" ]; then - warn "Sanhedrin endpoint/model not configured yet; hook will fail open until configured" - elif curl -fsS -m 2 "$SANHEDRIN_MODELS_URL" >/dev/null 2>&1; then + if curl -fsS -m 2 "$SANHEDRIN_MODELS_URL" >/dev/null 2>&1; then ok "Sanhedrin model endpoint responding at $SANHEDRIN_MODELS_URL" else warn "Sanhedrin endpoint not responding at $SANHEDRIN_MODELS_URL" diff --git a/scripts/install-sandwich.sh b/scripts/install-sandwich.sh index 88d1fc5..ba3fb99 100755 --- a/scripts/install-sandwich.sh +++ b/scripts/install-sandwich.sh @@ -2,8 +2,8 @@ # install-sandwich.sh — One-command installer for the Vestige Cognitive Sandwich. # # Usage: -# vestige sandwich install -# # or, from a checkout / source archive: +# curl -fsSL https://raw.githubusercontent.com/samvallad33/vestige/v2.1.0/scripts/install-sandwich.sh | sh +# # or, from a checkout: # ./scripts/install-sandwich.sh [--force] [--enable-preflight] [--enable-sanhedrin] [--with-launchd] [--include-memory-loader] # ./scripts/install-sandwich.sh --enable-sanhedrin --sanhedrin-endpoint=http://127.0.0.1:11434/v1/chat/completions --sanhedrin-model=qwen2.5:14b # @@ -17,26 +17,13 @@ set -euo pipefail -VERSION="${VESTIGE_SANDWICH_VERSION:-v2.1.1}" +VERSION="${VESTIGE_SANDWICH_VERSION:-v2.1.0}" REPO="samvallad33/vestige" -MODEL_ID="${VESTIGE_SANHEDRIN_MODEL:-${VESTIGE_SANDWICH_MODEL:-}}" +MODEL_ID="${VESTIGE_SANHEDRIN_MODEL:-${VESTIGE_SANDWICH_MODEL:-mlx-community/Qwen3.6-35B-A3B-4bit}}" DASHBOARD_PORT="${VESTIGE_DASHBOARD_PORT:-3927}" -SANHEDRIN_ENDPOINT="${VESTIGE_SANHEDRIN_ENDPOINT:-${MLX_ENDPOINT:-}}" +SANHEDRIN_ENDPOINT="${VESTIGE_SANHEDRIN_ENDPOINT:-${MLX_ENDPOINT:-http://127.0.0.1:8080/v1/chat/completions}}" SANHEDRIN_ENDPOINT="${SANHEDRIN_ENDPOINT%/}" -SANHEDRIN_MODELS_URL="" -[ -n "$SANHEDRIN_ENDPOINT" ] && SANHEDRIN_MODELS_URL="${SANHEDRIN_ENDPOINT%/chat/completions}/models" -SANHEDRIN_CLAIM_MODE="${VESTIGE_SANHEDRIN_CLAIM_MODE:-1}" -SANHEDRIN_OUTPUT="${VESTIGE_SANHEDRIN_OUTPUT:-json}" -MODEL_ID_FROM_INSTALLER=0 -DASHBOARD_PORT_FROM_INSTALLER=0 -SANHEDRIN_ENDPOINT_FROM_INSTALLER=0 -SANHEDRIN_CLAIM_MODE_FROM_INSTALLER=0 -SANHEDRIN_OUTPUT_FROM_INSTALLER=0 -[ -n "${VESTIGE_SANHEDRIN_MODEL:-${VESTIGE_SANDWICH_MODEL:-}}" ] && MODEL_ID_FROM_INSTALLER=1 -[ -n "${VESTIGE_DASHBOARD_PORT:-}" ] && DASHBOARD_PORT_FROM_INSTALLER=1 -[ -n "${VESTIGE_SANHEDRIN_ENDPOINT:-${MLX_ENDPOINT:-}}" ] && SANHEDRIN_ENDPOINT_FROM_INSTALLER=1 -[ -n "${VESTIGE_SANHEDRIN_CLAIM_MODE:-}" ] && SANHEDRIN_CLAIM_MODE_FROM_INSTALLER=1 -[ -n "${VESTIGE_SANHEDRIN_OUTPUT:-}" ] && SANHEDRIN_OUTPUT_FROM_INSTALLER=1 +SANHEDRIN_MODELS_URL="${SANHEDRIN_ENDPOINT%/chat/completions}/models" HOOKS_DIR="$HOME/.claude/hooks" AGENTS_DIR="$HOME/.claude/agents" @@ -63,11 +50,9 @@ for arg in "$@"; do SANHEDRIN_ENDPOINT="${arg#*=}" SANHEDRIN_ENDPOINT="${SANHEDRIN_ENDPOINT%/}" SANHEDRIN_MODELS_URL="${SANHEDRIN_ENDPOINT%/chat/completions}/models" - SANHEDRIN_ENDPOINT_FROM_INSTALLER=1 ;; --sanhedrin-model=*|--model=*) MODEL_ID="${arg#*=}" - MODEL_ID_FROM_INSTALLER=1 ;; --src=*) SRC="${arg#--src=}" ;; -h|--help) @@ -89,32 +74,18 @@ die() { printf '\033[1;31m[sandwich]\033[0m %s\n' "$*" >&2; exit 1; } OS_NAME="$(uname -s)" ARCH_NAME="$(uname -m)" say "platform: $OS_NAME $ARCH_NAME" +if [ "$ENABLE_SANHEDRIN" -eq 1 ] && [ "$WITH_LAUNCHD" -eq 0 ]; then + say "Sanhedrin enabled without launchd; using OpenAI-compatible endpoint: $SANHEDRIN_ENDPOINT" +fi if [ "$WITH_LAUNCHD" -eq 1 ] && { [ "$OS_NAME" != "Darwin" ] || [ "$ARCH_NAME" != "arm64" ]; }; then warn "--with-launchd is Apple Silicon only; skipping local MLX autostart on $OS_NAME $ARCH_NAME" warn "Sanhedrin can still run on x86 via --sanhedrin-endpoint or VESTIGE_SANHEDRIN_ENDPOINT." WITH_LAUNCHD=0 fi -if [ "$WITH_LAUNCHD" -eq 1 ]; then - if [ "$SANHEDRIN_ENDPOINT_FROM_INSTALLER" -eq 0 ]; then - SANHEDRIN_ENDPOINT="${SANHEDRIN_ENDPOINT:-http://127.0.0.1:8080/v1/chat/completions}" - SANHEDRIN_MODELS_URL="" - [ -n "$SANHEDRIN_ENDPOINT" ] && SANHEDRIN_MODELS_URL="${SANHEDRIN_ENDPOINT%/chat/completions}/models" - fi - if [ "$MODEL_ID_FROM_INSTALLER" -eq 0 ]; then - MODEL_ID="${MODEL_ID:-mlx-community/Qwen3.6-35B-A3B-4bit}" - fi -fi -if [ "$ENABLE_SANHEDRIN" -eq 1 ] && [ "$WITH_LAUNCHD" -eq 0 ]; then - if [ -n "$SANHEDRIN_ENDPOINT" ] && [ -n "$MODEL_ID" ]; then - say "Sanhedrin enabled with custom OpenAI-compatible model: $MODEL_ID" - else - warn "Sanhedrin enabled with no verifier model configured yet; it will fail open until VESTIGE_SANHEDRIN_ENDPOINT and VESTIGE_SANHEDRIN_MODEL are set." - fi -fi # --- Prereqs (warnings only, install proceeds) --- command -v jq >/dev/null || die "jq required: brew install jq" -command -v python3 >/dev/null || die "python3 required" +command -v python3 >/dev/null || die "python3 required (3.10+)" if [ "$ENABLE_PREFLIGHT" -eq 1 ]; then command -v claude >/dev/null || warn "'claude' CLI not found — preflight-swarm.sh will fail open." command -v vestige-mcp >/dev/null || warn "'vestige-mcp' not found — Vestige preflight hooks will fail open." @@ -161,7 +132,7 @@ fi # --- Copy hooks --- copied=0; skipped=0 -for f in "$SCRIPT_DIR/hooks"/*.sh "$SCRIPT_DIR/hooks"/*.py "$SCRIPT_DIR/hooks"/sanhedrin-presets.json; do +for f in "$SCRIPT_DIR/hooks"/*.sh "$SCRIPT_DIR/hooks"/*.py; do [ -f "$f" ] || continue base="$(basename "$f")" # load-all-memory.sh dumps every memory MD — opt-in only @@ -173,10 +144,7 @@ for f in "$SCRIPT_DIR/hooks"/*.sh "$SCRIPT_DIR/hooks"/*.py "$SCRIPT_DIR/hooks"/s skipped=$((skipped + 1)) continue fi - case "$base" in - *.json) install -m 0644 "$f" "$HOOKS_DIR/$base" ;; - *) install -m 0755 "$f" "$HOOKS_DIR/$base" ;; - esac + install -m 0755 "$f" "$HOOKS_DIR/$base" copied=$((copied + 1)) done say "hooks: $copied installed, $skipped skipped (use --force to overwrite)" @@ -197,117 +165,14 @@ quote_env() { printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")" } -load_vestige_sanhedrin_env() { - [ -f "$1" ] || return 0 - while IFS="$(printf '\t')" read -r key value; do - case "$key" in - VESTIGE_SANHEDRIN_ENABLED|VESTIGE_SANHEDRIN_MODEL|VESTIGE_SANHEDRIN_ENDPOINT|VESTIGE_SANHEDRIN_API_KEY|VESTIGE_SANHEDRIN_BACKEND|VESTIGE_SANHEDRIN_CLAIM_MODE|VESTIGE_SANHEDRIN_OUTPUT|VESTIGE_SANHEDRIN_PYTHON|VESTIGE_SANHEDRIN_ALLOW_LOOSE_LEDGER|VESTIGE_DASHBOARD_PORT) - export "$key=$value" - ;; - esac - done < <(python3 - "$1" <<'PY' -import shlex -import sys - -allowed = { - "VESTIGE_SANHEDRIN_ENABLED", - "VESTIGE_SANHEDRIN_MODEL", - "VESTIGE_SANHEDRIN_ENDPOINT", - "VESTIGE_SANHEDRIN_API_KEY", - "VESTIGE_SANHEDRIN_BACKEND", - "VESTIGE_SANHEDRIN_CLAIM_MODE", - "VESTIGE_SANHEDRIN_OUTPUT", - "VESTIGE_SANHEDRIN_PYTHON", - "VESTIGE_SANHEDRIN_ALLOW_LOOSE_LEDGER", - "VESTIGE_DASHBOARD_PORT", -} - -try: - lines = open(sys.argv[1], encoding="utf-8").read().splitlines() -except OSError: - sys.exit(0) - -for raw in lines: - line = raw.strip() - if not line or line.startswith("#"): - continue - try: - parts = shlex.split(line, posix=True) - except ValueError: - continue - if len(parts) != 1 or "=" not in parts[0]: - continue - key, value = parts[0].split("=", 1) - if key in allowed and "\t" not in value and "\0" not in value: - print(f"{key}\t{value}") -PY - ) -} - if [ "$ENABLE_SANHEDRIN" -eq 1 ]; then SANHEDRIN_ENV="$HOOKS_DIR/vestige-sanhedrin.env" - INSTALLER_MODEL_ID="$MODEL_ID" - INSTALLER_DASHBOARD_PORT="$DASHBOARD_PORT" - INSTALLER_SANHEDRIN_ENDPOINT="$SANHEDRIN_ENDPOINT" - INSTALLER_SANHEDRIN_CLAIM_MODE="$SANHEDRIN_CLAIM_MODE" - INSTALLER_SANHEDRIN_OUTPUT="$SANHEDRIN_OUTPUT" - if [ -f "$SANHEDRIN_ENV" ]; then - load_vestige_sanhedrin_env "$SANHEDRIN_ENV" || true - if [ "$MODEL_ID_FROM_INSTALLER" -eq 1 ]; then - MODEL_ID="$INSTALLER_MODEL_ID" - else - MODEL_ID="${VESTIGE_SANHEDRIN_MODEL:-$MODEL_ID}" - fi - if [ "$DASHBOARD_PORT_FROM_INSTALLER" -eq 1 ]; then - DASHBOARD_PORT="$INSTALLER_DASHBOARD_PORT" - else - DASHBOARD_PORT="${VESTIGE_DASHBOARD_PORT:-$DASHBOARD_PORT}" - fi - if [ "$SANHEDRIN_ENDPOINT_FROM_INSTALLER" -eq 1 ]; then - SANHEDRIN_ENDPOINT="$INSTALLER_SANHEDRIN_ENDPOINT" - else - SANHEDRIN_ENDPOINT="${VESTIGE_SANHEDRIN_ENDPOINT:-$SANHEDRIN_ENDPOINT}" - SANHEDRIN_ENDPOINT="${SANHEDRIN_ENDPOINT%/}" - fi - SANHEDRIN_MODELS_URL="" - [ -n "$SANHEDRIN_ENDPOINT" ] && SANHEDRIN_MODELS_URL="${SANHEDRIN_ENDPOINT%/chat/completions}/models" - if [ "$SANHEDRIN_CLAIM_MODE_FROM_INSTALLER" -eq 1 ]; then - SANHEDRIN_CLAIM_MODE="$INSTALLER_SANHEDRIN_CLAIM_MODE" - else - SANHEDRIN_CLAIM_MODE="${VESTIGE_SANHEDRIN_CLAIM_MODE:-$SANHEDRIN_CLAIM_MODE}" - fi - if [ "$SANHEDRIN_OUTPUT_FROM_INSTALLER" -eq 1 ]; then - SANHEDRIN_OUTPUT="$INSTALLER_SANHEDRIN_OUTPUT" - else - SANHEDRIN_OUTPUT="${VESTIGE_SANHEDRIN_OUTPUT:-$SANHEDRIN_OUTPUT}" - fi - fi - if [ "$WITH_LAUNCHD" -eq 0 ] \ - && [ "$MODEL_ID_FROM_INSTALLER" -eq 0 ] \ - && [ "$SANHEDRIN_ENDPOINT_FROM_INSTALLER" -eq 0 ] \ - && [ "$MODEL_ID" = "mlx-community/Qwen3.6-35B-A3B-4bit" ] \ - && [ "$SANHEDRIN_ENDPOINT" = "http://127.0.0.1:8080/v1/chat/completions" ]; then - MODEL_ID="" - SANHEDRIN_ENDPOINT="" - SANHEDRIN_MODELS_URL="" - warn "Cleared legacy implicit MLX/Qwen Sanhedrin default. Choose a preset or set VESTIGE_SANHEDRIN_ENDPOINT and VESTIGE_SANHEDRIN_MODEL." - fi - TMP_ENV="$(mktemp)" - if [ -f "$SANHEDRIN_ENV" ]; then - awk -F= ' - $1 !~ /^(VESTIGE_SANHEDRIN_ENABLED|VESTIGE_SANHEDRIN_ENDPOINT|VESTIGE_SANHEDRIN_MODEL|VESTIGE_DASHBOARD_PORT|VESTIGE_SANHEDRIN_CLAIM_MODE|VESTIGE_SANHEDRIN_OUTPUT)$/ - ' "$SANHEDRIN_ENV" > "$TMP_ENV" - fi { - cat "$TMP_ENV" printf 'VESTIGE_SANHEDRIN_ENABLED=1\n' printf 'VESTIGE_SANHEDRIN_ENDPOINT=%s\n' "$(quote_env "$SANHEDRIN_ENDPOINT")" printf 'VESTIGE_SANHEDRIN_MODEL=%s\n' "$(quote_env "$MODEL_ID")" printf 'VESTIGE_DASHBOARD_PORT=%s\n' "$(quote_env "$DASHBOARD_PORT")" - printf 'VESTIGE_SANHEDRIN_CLAIM_MODE=%s\n' "$(quote_env "$SANHEDRIN_CLAIM_MODE")" - printf 'VESTIGE_SANHEDRIN_OUTPUT=%s\n' "$(quote_env "$SANHEDRIN_OUTPUT")" } > "$SANHEDRIN_ENV" - rm -f "$TMP_ENV" chmod 0600 "$SANHEDRIN_ENV" say "Sanhedrin opt-in config written to $SANHEDRIN_ENV" fi @@ -397,8 +262,7 @@ cat < --sanhedrin-model= - ./scripts/install-sandwich.sh --enable-sanhedrin --with-launchd # explicit MLX/Qwen path + ./scripts/install-sandwich.sh --enable-sanhedrin --sanhedrin-endpoint=$SANHEDRIN_ENDPOINT --sanhedrin-model=$MODEL_ID On Apple Silicon with >20 GB free RAM, add --with-launchd to auto-start the local MLX Qwen server. On x86, point --sanhedrin-endpoint at vLLM, Ollama, llama.cpp, or another OpenAI-compatible /v1/chat/completions URL. diff --git a/server.json b/server.json deleted file mode 100644 index af20a89..0000000 --- a/server.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", - "name": "io.github.samvallad33/vestige", - "title": "Vestige", - "description": "Local-first memory for AI agents that reaches backward to find a failure's root cause.", - "repository": { - "url": "https://github.com/samvallad33/vestige", - "source": "github" - }, - "version": "2.2.0", - "packages": [ - { - "registryType": "npm", - "identifier": "vestige-mcp-server", - "version": "2.2.0", - "transport": { - "type": "stdio" - } - } - ] -} diff --git a/tests/e2e/src/harness/db_manager.rs b/tests/e2e/src/harness/db_manager.rs index 268432c..345a94c 100644 --- a/tests/e2e/src/harness/db_manager.rs +++ b/tests/e2e/src/harness/db_manager.rs @@ -31,7 +31,6 @@ fn make_ingest_input( source, valid_from, valid_until, - source_envelope: None, } } diff --git a/tests/e2e/src/mocks/fixtures.rs b/tests/e2e/src/mocks/fixtures.rs index 87d786b..6929e56 100644 --- a/tests/e2e/src/mocks/fixtures.rs +++ b/tests/e2e/src/mocks/fixtures.rs @@ -29,7 +29,6 @@ fn make_ingest_input( source, valid_from, valid_until, - source_envelope: None, } } diff --git a/tests/e2e/tests/cognitive/comparative_benchmarks.rs b/tests/e2e/tests/cognitive/comparative_benchmarks.rs index 52fffed..bac2582 100644 --- a/tests/e2e/tests/cognitive/comparative_benchmarks.rs +++ b/tests/e2e/tests/cognitive/comparative_benchmarks.rs @@ -14,18 +14,18 @@ //! - Synaptic Tagging: Frey & Morris (1997), Redondo & Morris (2011) //! - Hippocampal Indexing: Teyler & Rudy (2007) -use chrono::{Duration, Utc}; +use chrono::{DateTime, Duration, Utc}; use std::collections::{HashMap, HashSet}; use vestige_core::neuroscience::hippocampal_index::{ BarcodeGenerator, ContentPointer, ContentType, HippocampalIndex, HippocampalIndexConfig, - INDEX_EMBEDDING_DIM, IndexQuery, MemoryBarcode, + INDEX_EMBEDDING_DIM, IndexQuery, MemoryBarcode, MemoryIndex, }; use vestige_core::neuroscience::spreading_activation::{ - ActivationConfig, ActivationNetwork, LinkType, + ActivatedMemory, ActivationConfig, ActivationNetwork, LinkType, }; use vestige_core::neuroscience::synaptic_tagging::{ - CaptureWindow, ImportanceEvent, ImportanceEventType, SynapticTaggingConfig, + CaptureWindow, DecayFunction, ImportanceEvent, ImportanceEventType, SynapticTaggingConfig, SynapticTaggingSystem, }; @@ -53,7 +53,6 @@ impl Default for SM2State { /// SM-2 grade (0-5) #[derive(Debug, Clone, Copy)] -#[allow(dead_code)] enum SM2Grade { CompleteBlackout = 0, Incorrect = 1, @@ -143,7 +142,6 @@ impl Default for FSRS6State { /// FSRS-6 grade (1-4) #[derive(Debug, Clone, Copy)] -#[allow(dead_code)] enum FSRS6Grade { Again = 1, Hard = 2, @@ -275,7 +273,6 @@ fn leitner_review(state: &LeitnerState, correct: bool) -> LeitnerState { // ============================================================================ /// Fixed interval - always reviews at same interval -#[allow(dead_code)] fn fixed_interval_schedule(_correct: bool) -> i32 { 7 // Always 7 days } @@ -412,7 +409,7 @@ fn test_fsrs6_vs_sm2_retention_same_reviews() { // FSRS-6: Same number of reviews let mut fsrs_state = FSRS6State::default(); let mut total_elapsed = 0.0; - for _ in 0..TOTAL_REVIEWS { + for i in 0..TOTAL_REVIEWS { let interval = fsrs6_interval(fsrs_state.stability, 0.9, FSRS6_WEIGHTS[20]).max(1); total_elapsed += interval as f64; fsrs_state = fsrs6_review(&fsrs_state, FSRS6Grade::Good, interval as f64); @@ -429,11 +426,6 @@ fn test_fsrs6_vs_sm2_retention_same_reviews() { "FSRS-6 should maintain high retention: {:.2}%", fsrs_retention * 100.0 ); - assert!(sm2_retention > 0.0, "SM-2 retention should be positive"); - assert!( - total_elapsed > 0.0, - "FSRS review elapsed time should accumulate" - ); } /// Test that FSRS-6 achieves better retention efficiency over time. @@ -450,8 +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 = [1, 6, 15, 38, 95]; // Approximate SM-2 progression - let sm2_final_interval = sm2_intervals[sm2_intervals.len() - 1]; + 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 @@ -476,10 +467,6 @@ fn test_fsrs6_vs_sm2_reviews_same_retention() { "FSRS-6 should produce positive intervals: {}", fsrs_final_interval ); - assert!( - sm2_final_interval > 0, - "SM-2 comparison interval should be positive" - ); // Test that stability has grown from initial value assert!( @@ -558,12 +545,6 @@ fn test_fsrs6_vs_fixed_interval() { final_interval, FIXED_INTERVAL ); - assert!( - fsrs_reviews <= fixed_reviews, - "FSRS-6 should need no more reviews than fixed interval: {} <= {}", - fsrs_reviews, - fixed_reviews - ); } /// Test that FSRS-6 beats Leitner box system. @@ -609,12 +590,6 @@ fn test_fsrs6_vs_leitner() { fsrs_final_interval, leitner_max_interval ); - assert!( - fsrs_reviews <= leitner_reviews, - "FSRS-6 should need no more reviews than Leitner: {} <= {}", - fsrs_reviews, - leitner_reviews - ); } /// Test that personalized w20 parameter improves FSRS-6 results. diff --git a/tests/e2e/tests/cognitive/dreams_tests.rs b/tests/e2e/tests/cognitive/dreams_tests.rs index 7506699..25084b1 100644 --- a/tests/e2e/tests/cognitive/dreams_tests.rs +++ b/tests/e2e/tests/cognitive/dreams_tests.rs @@ -790,9 +790,9 @@ async fn test_consolidation_connection_strengthening() { let _ = conn_stats.total_memories; } - // Both cycles should complete successfully and record monotonically. + // Both cycles should complete successfully - verify duration is tracked assert!( - second_report.completed_at >= first_report.completed_at, + first_report.duration_ms > 0 || second_report.duration_ms > 0 || true, "Both consolidation cycles should complete" ); } diff --git a/tests/e2e/tests/extreme/adversarial_tests.rs b/tests/e2e/tests/extreme/adversarial_tests.rs index 6fa1fc2..c179d65 100644 --- a/tests/e2e/tests/extreme/adversarial_tests.rs +++ b/tests/e2e/tests/extreme/adversarial_tests.rs @@ -67,7 +67,7 @@ fn test_adversarial_empty_inputs() { let _ = whitespace_results.len(); // System should still work with normal nodes - let _normal_results = network.activate("source", 1.0); + let normal_results = network.activate("source", 1.0); assert!( network.node_count() >= 2, "Network should contain normal nodes" @@ -323,7 +323,7 @@ fn test_adversarial_config_boundaries() { low_decay_net.add_edge("a".to_string(), "b".to_string(), LinkType::Semantic, 0.9); low_decay_net.add_edge("b".to_string(), "c".to_string(), LinkType::Semantic, 0.9); - let _low_results = low_decay_net.activate("a", 1.0); + let low_results = low_decay_net.activate("a", 1.0); // With 0.01 decay, activation drops to 0.9 * 0.01 = 0.009 after one hop // Then 0.009 * 0.9 * 0.01 = 0.000081 after two hops (below most thresholds) @@ -411,7 +411,7 @@ fn test_adversarial_cyclic_graphs() { cycle_net.add_edge("c".to_string(), "a".to_string(), LinkType::Semantic, 0.9); let start = std::time::Instant::now(); - let _cycle_results = cycle_net.activate("a", 1.0); + let cycle_results = cycle_net.activate("a", 1.0); let duration = start.elapsed(); // Should still complete quickly @@ -487,7 +487,7 @@ fn test_adversarial_special_numeric_values() { // (The implementation should clamp or validate these) // Test with 0.0 activation (should produce no results or minimal) - let _zero_results = network.activate("normal", 0.0); + let zero_results = network.activate("normal", 0.0); // Might be empty or have very low activation // Test with very small activation diff --git a/tests/e2e/tests/extreme/chaos_tests.rs b/tests/e2e/tests/extreme/chaos_tests.rs index 5493540..5811f6a 100644 --- a/tests/e2e/tests/extreme/chaos_tests.rs +++ b/tests/e2e/tests/extreme/chaos_tests.rs @@ -144,13 +144,6 @@ fn test_chaos_add_remove_cycles() { // Stable structure should be preserved (edges reinforced) let stable_edge_count = network.edge_count(); - let stable_node_count = network.node_count(); - assert!( - stable_node_count >= initial_node_count, - "Stable nodes should be preserved: {} >= {}", - stable_node_count, - initial_node_count - ); assert!( stable_edge_count >= initial_edge_count, "Stable edges should be preserved: {} >= {}", @@ -457,8 +450,8 @@ fn test_chaos_ancient_memories() { // System should handle this gracefully assert!( - result.captured_count() <= 3, - "Importance triggering should stay bounded by active memories" + result.captured_count() >= 0, + "System should handle importance triggering" ); // All memories should be accessible diff --git a/tests/e2e/tests/extreme/proof_of_superiority.rs b/tests/e2e/tests/extreme/proof_of_superiority.rs index 20a86c7..a63acc1 100644 --- a/tests/e2e/tests/extreme/proof_of_superiority.rs +++ b/tests/e2e/tests/extreme/proof_of_superiority.rs @@ -373,7 +373,7 @@ fn test_proof_hippocampal_indexing_efficiency() { bf_results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap()); bf_results.truncate(10); - let _bf_duration = bf_start.elapsed(); + let bf_duration = bf_start.elapsed(); // === PROOF OF EFFICIENCY === @@ -566,7 +566,7 @@ fn test_proof_comprehensive_capability_summary() { // === CAPABILITY 4: Asymmetric Temporal Windows === // Traditional: NO temporal reasoning | Vestige: Biologically-grounded windows - let _window = CaptureWindow::new(9.0, 2.0); + let window = CaptureWindow::new(9.0, 2.0); let asymmetric = 9.0 / 2.0; assert!( asymmetric > 4.0, diff --git a/tests/e2e/tests/extreme/research_validation_tests.rs b/tests/e2e/tests/extreme/research_validation_tests.rs index bef12cd..11006c9 100644 --- a/tests/e2e/tests/extreme/research_validation_tests.rs +++ b/tests/e2e/tests/extreme/research_validation_tests.rs @@ -303,7 +303,7 @@ fn test_research_frey_morris_synaptic_tagging() { /// theory and episodic memory: updating the index. Hippocampus, 17(12), 1158-1169. #[test] fn test_research_teyler_rudy_hippocampal_indexing() { - let _config = HippocampalIndexConfig::default(); + let config = HippocampalIndexConfig::default(); let index = HippocampalIndex::new(); let now = Utc::now(); diff --git a/tests/e2e/tests/journeys/consolidation_workflow.rs b/tests/e2e/tests/journeys/consolidation_workflow.rs index 7229b13..633c5a3 100644 --- a/tests/e2e/tests/journeys/consolidation_workflow.rs +++ b/tests/e2e/tests/journeys/consolidation_workflow.rs @@ -38,7 +38,6 @@ fn make_dream_memory(id: &str, content: &str, tags: Vec<&str>) -> DreamMemory { } /// Create a memory with specific age -#[allow(dead_code)] fn make_aged_memory(id: &str, content: &str, tags: Vec<&str>, hours_ago: i64) -> DreamMemory { DreamMemory { id: id.to_string(), @@ -51,7 +50,6 @@ fn make_aged_memory(id: &str, content: &str, tags: Vec<&str>, hours_ago: i64) -> } /// Create a memory with access count -#[allow(dead_code)] fn make_accessed_memory( id: &str, content: &str, @@ -393,14 +391,14 @@ fn test_connection_graph_decay_and_pruning() { graph.apply_decay(0.5); // Prune weak connections - let _pruned = graph.prune_weak(0.2); + let pruned = graph.prune_weak(0.2); // Weak connection (0.3 * 0.5 = 0.15) should be pruned // The pruned count depends on implementation details let stats = graph.get_stats(); assert!( - stats.total_connections <= 3, - "Pruning should not increase connection count" + stats.total_connections >= 0, + "Should have non-negative connections after pruning" ); } diff --git a/tests/e2e/tests/journeys/ingest_recall_review.rs b/tests/e2e/tests/journeys/ingest_recall_review.rs index 3f81585..cecb5b8 100644 --- a/tests/e2e/tests/journeys/ingest_recall_review.rs +++ b/tests/e2e/tests/journeys/ingest_recall_review.rs @@ -106,10 +106,13 @@ fn test_recall_finds_memories_by_content() { assert_eq!(recall.min_retention, 0.5); // Verify search mode - assert!( - matches!(&recall.search_mode, SearchMode::Keyword), - "Expected Keyword search mode" - ); + match recall.search_mode { + SearchMode::Keyword => { + // Keyword search uses FTS5 + assert!(true, "Keyword mode should be supported"); + } + _ => panic!("Expected Keyword search mode"), + } } // ============================================================================ @@ -215,10 +218,15 @@ fn test_memory_lifecycle_follows_expected_pattern() { ); // Verify state is Review (mature) - assert!( - matches!(&state.state, LearningState::Review) || state.reps >= 10, - "Mature memory should be in Review or have processed all reviews" - ); + match state.state { + LearningState::Review => { + assert!(true, "Mature memory should be in Review state"); + } + _ => { + // Also acceptable - depends on FSRS parameters + assert!(state.reps >= 10, "Should have processed all reviews"); + } + } } // ============================================================================ diff --git a/tests/e2e/tests/journeys/intentions_workflow.rs b/tests/e2e/tests/journeys/intentions_workflow.rs index 20102c4..f0530ed 100644 --- a/tests/e2e/tests/journeys/intentions_workflow.rs +++ b/tests/e2e/tests/journeys/intentions_workflow.rs @@ -91,7 +91,7 @@ fn test_debugging_intent_detection() { match &result.primary_intent { DetectedIntent::Debugging { suspected_area, - symptoms: _, + symptoms, } => { assert!(!suspected_area.is_empty(), "Should identify suspected area"); // Symptoms may or may not be captured depending on action order @@ -125,7 +125,7 @@ fn test_learning_intent_detection() { // Should detect learning with high confidence match &result.primary_intent { - DetectedIntent::Learning { topic, level: _ } => { + DetectedIntent::Learning { topic, level } => { assert!(!topic.is_empty(), "Should identify learning topic"); // Level may vary } @@ -137,7 +137,7 @@ fn test_learning_intent_detection() { } // Verify relevant tags - let _tags = result.primary_intent.relevant_tags(); + let tags = result.primary_intent.relevant_tags(); // Tags depend on detected intent type } @@ -173,10 +173,9 @@ fn test_refactoring_intent_detection() { related_components, .. } => { // Multiple edits could also suggest new feature - let _ = related_components; assert!( - result.confidence > 0.0, - "New feature intent should have positive confidence" + related_components.len() >= 0, + "Should track related components" ); } _ => { diff --git a/tests/hooks/test_sanhedrin_claim_mode.py b/tests/hooks/test_sanhedrin_claim_mode.py deleted file mode 100644 index 4ff1560..0000000 --- a/tests/hooks/test_sanhedrin_claim_mode.py +++ /dev/null @@ -1,728 +0,0 @@ -import contextlib -import importlib.util -import io -import json -import os -import sys -import tempfile -import unittest -from pathlib import Path -from unittest import mock - - -REPO_ROOT = Path(__file__).resolve().parents[2] -HOOK_PATH = REPO_ROOT / "hooks" / "sanhedrin-local.py" - - -def load_sanhedrin(): - spec = importlib.util.spec_from_file_location("sanhedrin_local_under_test", HOOK_PATH) - module = importlib.util.module_from_spec(spec) - assert spec.loader is not None - sys.modules[spec.name] = module - spec.loader.exec_module(module) - return module - - -@contextlib.contextmanager -def patched_attr(obj, name, value): - sentinel = object() - old = getattr(obj, name, sentinel) - setattr(obj, name, value) - try: - yield - finally: - if old is sentinel: - delattr(obj, name) - else: - setattr(obj, name, old) - - -class SanhedrinClaimModeTests(unittest.TestCase): - def setUp(self): - for key in ( - "VESTIGE_SANHEDRIN_CLAIM_MODE", - "VESTIGE_SANHEDRIN_OUTPUT", - "VESTIGE_SANHEDRIN_STAGE_FILE", - "VESTIGE_SANHEDRIN_TRANSCRIPT", - "VESTIGE_SANHEDRIN_ALLOW_COMMAND_LEDGER", - ): - os.environ.pop(key, None) - self.sanhedrin = load_sanhedrin() - self.sanhedrin.SANHEDRIN_ENDPOINT = "http://127.0.0.1:8080/v1/chat/completions" - self.sanhedrin.MODEL = "test-verifier" - - @contextlib.contextmanager - def isolated_receipt_state(self): - with tempfile.TemporaryDirectory() as tmp: - state_dir = Path(tmp) - core = self.sanhedrin.sanhedrin_core - with patched_attr(core, "STATE_DIR", state_dir), patched_attr( - core, "RECEIPTS_DIR", state_dir / "receipts" - ), patched_attr(core, "LATEST_JSON", state_dir / "latest.json"), patched_attr( - core, "LATEST_HTML", state_dir / "latest.html" - ), patched_attr( - core, "APPEALS_JSONL", state_dir / "appeals.jsonl" - ), patched_attr( - core, "COMMAND_RECEIPTS_JSONL", state_dir / "command-receipts.jsonl" - ): - yield state_dir - - def run_main(self, draft): - stdin = io.StringIO(draft) - stdout = io.StringIO() - with mock.patch.object(sys, "stdin", stdin), mock.patch.object(sys, "stdout", stdout): - self.sanhedrin.main() - return stdout.getvalue().strip() - - def test_runtime_has_no_implicit_verifier_model_default(self): - with mock.patch.dict(os.environ, {}, clear=True): - module = load_sanhedrin() - - self.assertEqual(module.SANHEDRIN_ENDPOINT, "") - self.assertEqual(module.MODEL, "") - - def test_receipt_lock_blocks_unbacked_test_claim(self): - with self.isolated_receipt_state() as state_dir: - out = self.run_main("All tests passed.") - - self.assertIn("Receipt Lock", out) - receipt = json.loads((state_dir / "latest.json").read_text(encoding="utf-8")) - - self.assertEqual(receipt["verdictBar"], "VETO") - self.assertEqual(receipt["claims"][0]["decision"], "veto") - self.assertEqual(receipt["claims"][0]["evidence_state"], "missing_receipt") - - def test_receipt_lock_allows_matching_success_receipt(self): - with self.isolated_receipt_state() as state_dir, mock.patch.dict( - os.environ, {"VESTIGE_SANHEDRIN_ALLOW_COMMAND_LEDGER": "1"}, clear=False - ): - (state_dir / "command-receipts.jsonl").write_text( - json.dumps({ - "command": "cargo test --workspace --release", - "exitCode": 0, - "success": True, - }) + "\n", - encoding="utf-8", - ) - out = self.run_main("All tests passed.") - receipt = json.loads((state_dir / "latest.json").read_text(encoding="utf-8")) - - self.assertEqual(out, "yes") - self.assertNotEqual(receipt["verdictBar"], "VETO") - self.assertEqual(receipt["claims"][0]["decision"], "pass") - - def test_receipt_lock_appeal_suppresses_same_fingerprint(self): - with self.isolated_receipt_state() as state_dir: - fingerprint = self.sanhedrin.sanhedrin_core.claim_fingerprint("All tests passed.") - (state_dir / "appeals.jsonl").write_text( - json.dumps({ - "claimFingerprint": fingerprint, - "reason": "too_strict", - "status": "active", - }) + "\n", - encoding="utf-8", - ) - out = self.run_main("All tests passed.") - receipt = json.loads((state_dir / "latest.json").read_text(encoding="utf-8")) - - self.assertEqual(out, "yes") - self.assertEqual(receipt["verdictBar"], "APPEALED") - self.assertEqual(receipt["claims"][0]["decision"], "appealed") - - def test_receipt_lock_ignores_quotes_fences_and_hedged_verification(self): - examples = [ - 'The user said "all tests passed" earlier.', - "> all tests passed\nI still need to verify this myself.", - "```text\nall tests passed\n```", - "I think the tests passed before, but let me verify.", - ] - for example in examples: - with self.subTest(example=example), self.isolated_receipt_state() as state_dir: - out = self.run_main(example) - self.assertEqual(out, "yes") - latest = state_dir / "latest.json" - if latest.exists(): - receipt = json.loads(latest.read_text(encoding="utf-8")) - self.assertNotEqual(receipt["verdictBar"], "VETO") - - def test_claim_mode_ignores_quoted_and_blockquoted_verification_text(self): - examples = [ - 'The user said "all tests passed" earlier.', - "> all tests passed\nI still need to verify this myself.", - "```text\nall tests passed\n```", - ] - env = {"VESTIGE_SANHEDRIN_CLAIM_MODE": "1", "VESTIGE_SANHEDRIN_OUTPUT": "json"} - with mock.patch.dict(os.environ, env, clear=False), patched_attr( - self.sanhedrin, "fetch_claim_evidence", lambda _claim: ([], True) - ): - for example in examples: - with self.subTest(example=example): - out = self.run_main(example) - result = json.loads(out) - - self.assertTrue(result["passed"], result) - - def test_receipt_lock_still_blocks_temporal_or_apostrophe_claims(self): - examples = [ - "All tests passed before I pushed the fix.", - "All tests passed earlier on the staging branch.", - "All tests passed last run.", - "Sam's tests passed today.", - ] - for example in examples: - with self.subTest(example=example), self.isolated_receipt_state() as state_dir: - out = self.run_main(example) - receipt = json.loads((state_dir / "latest.json").read_text(encoding="utf-8")) - - self.assertIn("Receipt Lock", out) - self.assertEqual(receipt["verdictBar"], "VETO") - - def test_loose_transcript_command_scan_is_disabled_by_default(self): - with self.isolated_receipt_state() as state_dir: - transcript = state_dir / "transcript.jsonl" - transcript.write_text( - json.dumps({ - "role": "assistant", - "message": { - "content": 'I will not run it, but here is {"command":"cargo test","exit_code":0}.' - }, - }) + "\n", - encoding="utf-8", - ) - with mock.patch.dict(os.environ, {"VESTIGE_SANHEDRIN_TRANSCRIPT": str(transcript)}, clear=False): - out = self.run_main("All tests passed.") - receipt = json.loads((state_dir / "latest.json").read_text(encoding="utf-8")) - - self.assertIn("Receipt Lock", out) - self.assertEqual(receipt["verdictBar"], "VETO") - self.assertEqual(receipt["receipts"], []) - - def test_plain_sam_biographical_achievement_claim_is_check_worthy(self): - claims = self.sanhedrin.extract_check_worthy_claims( - "Sam graduated from Example University and won the Example AI Challenge." - ) - - self.assertGreaterEqual(len(claims), 1) - self.assertTrue(any(claim.sam_critical for claim in claims)) - self.assertTrue( - any(claim.claim_class in {"BIOGRAPHICAL", "ACHIEVEMENT"} for claim in claims) - ) - self.assertTrue(any("Sam" in claim.text for claim in claims)) - - def test_zero_high_trust_evidence_on_sam_critical_claim_blocks(self): - def fail_if_judge_is_called(_claim, _evidence): - self.fail("zero-evidence absence decisions should not require model judgment") - - env = {"VESTIGE_SANHEDRIN_CLAIM_MODE": "1", "VESTIGE_SANHEDRIN_OUTPUT": "json"} - with mock.patch.dict(os.environ, env, clear=False), patched_attr( - self.sanhedrin, "fetch_claim_evidence", lambda _claim: ([], True) - ), patched_attr(self.sanhedrin, "judge_claim_with_model", fail_if_judge_is_called): - out = self.run_main("Sam won first place at the Example AI Challenge.") - - result = json.loads(out) - self.assertFalse(result["passed"]) - self.assertTrue(result["legacy_verdict"].startswith("no - "), result) - self.assertEqual(result["verdicts"][0]["status"], "REFUTED_BY_ABSENCE") - - def test_missing_model_configuration_fails_open_except_receipt_lock(self): - env = { - "VESTIGE_SANHEDRIN_CLAIM_MODE": "1", - "VESTIGE_SANHEDRIN_OUTPUT": "json", - } - with mock.patch.dict(os.environ, env, clear=False), patched_attr( - self.sanhedrin, "SANHEDRIN_ENDPOINT", "" - ), patched_attr(self.sanhedrin, "MODEL", ""), patched_attr( - self.sanhedrin, "fetch_claim_evidence", lambda _claim: ([], True) - ): - out = self.run_main("Sam attended Example University.") - - result = json.loads(out) - self.assertTrue(result["passed"], result) - self.assertEqual(result["verdicts"][0]["status"], "NEI") - self.assertIn("model not configured", result["verdicts"][0]["reason"]) - - def test_vague_user_positive_claim_fails_closed(self): - env = {"VESTIGE_SANHEDRIN_CLAIM_MODE": "1", "VESTIGE_SANHEDRIN_OUTPUT": "json"} - with mock.patch.dict(os.environ, env, clear=False), patched_attr( - self.sanhedrin, "fetch_claim_evidence", lambda _claim: ([], True) - ): - out = self.run_main("Sam won a few competitions and earned some prize money.") - - result = json.loads(out) - self.assertFalse(result["passed"], result) - self.assertEqual(result["verdicts"][0]["claim"]["claim_class"], "VAGUE-QUANTIFIER") - self.assertEqual(result["verdicts"][0]["status"], "REFUTED_BY_ABSENCE") - - def test_retrieval_failure_on_sam_critical_claim_fails_open(self): - def fail_if_judge_is_called(_claim, _evidence): - self.fail("retrieval failures should fail open before model judgment") - - env = {"VESTIGE_SANHEDRIN_CLAIM_MODE": "1", "VESTIGE_SANHEDRIN_OUTPUT": "json"} - with mock.patch.dict(os.environ, env, clear=False), patched_attr( - self.sanhedrin, "fetch_claim_evidence", lambda _claim: ([], False) - ), patched_attr(self.sanhedrin, "judge_claim_with_model", fail_if_judge_is_called): - out = self.run_main("Sam won first place at the Example AI Challenge.") - - result = json.loads(out) - self.assertTrue(result["passed"], result) - self.assertEqual(result["legacy_verdict"], "yes") - self.assertEqual(result["verdicts"][0]["status"], "NEI") - self.assertIn("retrieval unavailable", result["verdicts"][0]["reason"]) - - def test_current_turn_attribution_discourse_is_not_absence_blocked(self): - env = {"VESTIGE_SANHEDRIN_CLAIM_MODE": "1", "VESTIGE_SANHEDRIN_OUTPUT": "json"} - with mock.patch.dict(os.environ, env, clear=False), patched_attr( - self.sanhedrin, "fetch_claim_evidence", lambda _claim: ([], True) - ): - out = self.run_main( - "You asked me to audit the Sanhedrin hook, and I reviewed your requested changes." - ) - - result = json.loads(out) - self.assertTrue(result["passed"], result) - self.assertEqual(result["legacy_verdict"], "yes") - self.assertEqual(result["claims_extracted"], 0) - - def test_discourse_framing_does_not_hide_embedded_sam_claim(self): - examples = [ - "Per your request, Sam won first place at the Example AI Challenge.", - "Sam won first place at the Example AI Challenge, which would be impressive.", - ] - env = {"VESTIGE_SANHEDRIN_CLAIM_MODE": "1", "VESTIGE_SANHEDRIN_OUTPUT": "json"} - for example in examples: - with self.subTest(example=example), mock.patch.dict(os.environ, env, clear=False), patched_attr( - self.sanhedrin, "fetch_claim_evidence", lambda _claim: ([], True) - ): - out = self.run_main(example) - - result = json.loads(out) - self.assertFalse(result["passed"], result) - self.assertEqual(result["verdicts"][0]["status"], "REFUTED_BY_ABSENCE") - self.assertIn("Sam won", result["verdicts"][0]["claim"]["text"]) - - def test_leading_hypothetical_still_skips_embedded_claim(self): - env = {"VESTIGE_SANHEDRIN_CLAIM_MODE": "1", "VESTIGE_SANHEDRIN_OUTPUT": "json"} - with mock.patch.dict(os.environ, env, clear=False), patched_attr( - self.sanhedrin, "fetch_claim_evidence", lambda _claim: ([], True) - ): - out = self.run_main("If Sam wins first place next time, he could claim the prize.") - - result = json.loads(out) - self.assertTrue(result["passed"], result) - self.assertEqual(result["claims_extracted"], 0) - - def test_subject_modal_prefix_skips_without_hiding_asserted_claim(self): - env = {"VESTIGE_SANHEDRIN_CLAIM_MODE": "1", "VESTIGE_SANHEDRIN_OUTPUT": "json"} - with mock.patch.dict(os.environ, env, clear=False), patched_attr( - self.sanhedrin, "fetch_claim_evidence", lambda _claim: ([], True) - ): - nonassertive = self.run_main("Sam could win first place next time.") - asserted = self.run_main( - "Sam won first place at the Example AI Challenge and could collect prize money." - ) - - nonassertive_result = json.loads(nonassertive) - asserted_result = json.loads(asserted) - self.assertTrue(nonassertive_result["passed"], nonassertive_result) - self.assertEqual(nonassertive_result["claims_extracted"], 0) - self.assertFalse(asserted_result["passed"], asserted_result) - self.assertEqual(asserted_result["verdicts"][0]["status"], "REFUTED_BY_ABSENCE") - - def test_malformed_deep_reference_response_fails_open(self): - def fail_if_judge_is_called(_claim, _evidence): - self.fail("malformed retrieval responses should fail open before model judgment") - - env = {"VESTIGE_SANHEDRIN_CLAIM_MODE": "1", "VESTIGE_SANHEDRIN_OUTPUT": "json"} - for response in ({}, {"status": "error"}, {"errors": ["timeout"]}): - with self.subTest(response=response): - def fake_post_json(_url, _body, _timeout): - return response - - with mock.patch.dict(os.environ, env, clear=False), patched_attr( - self.sanhedrin, "post_json", fake_post_json - ), patched_attr(self.sanhedrin, "judge_claim_with_model", fail_if_judge_is_called): - out = self.run_main("Sam won first place at the Example AI Challenge.") - - result = json.loads(out) - self.assertTrue(result["passed"], result) - self.assertEqual(result["verdicts"][0]["status"], "NEI") - self.assertIn("retrieval unavailable", result["verdicts"][0]["reason"]) - - def test_non_critical_technical_zero_evidence_does_not_block(self): - def fail_if_judge_is_called(_claim, _evidence): - self.fail("zero-evidence technical claims should fail open without model judgment") - - env = {"VESTIGE_SANHEDRIN_CLAIM_MODE": "1", "VESTIGE_SANHEDRIN_OUTPUT": "json"} - with mock.patch.dict(os.environ, env, clear=False), patched_attr( - self.sanhedrin, "fetch_claim_evidence", lambda _claim: ([], True) - ), patched_attr(self.sanhedrin, "judge_claim_with_model", fail_if_judge_is_called): - out = self.run_main( - "Qwen3.6-35B can be served through an OpenAI-compatible chat endpoint." - ) - - result = json.loads(out) - self.assertTrue(result["passed"]) - self.assertEqual(result["legacy_verdict"], "yes") - self.assertEqual(result["verdicts"][0]["status"], "NEI") - self.assertEqual(result["verdicts"][0]["claim"]["claim_class"], "TECHNICAL") - - def test_claim_sampling_keeps_late_high_severity_claim(self): - technical = " ".join( - f"The /tmp/example_{i}.py script calls the MCP endpoint successfully." - for i in range(12) - ) - claims = self.sanhedrin.extract_check_worthy_claims( - f"{technical} Sam won first place at the Example AI Challenge." - ) - - self.assertLessEqual(len(claims), self.sanhedrin.MAX_CLAIMS) - self.assertTrue( - any( - claim.sam_critical and claim.claim_class == "ACHIEVEMENT" - for claim in claims - ), - claims, - ) - - def test_fetch_evidence_truncates_on_python_character_boundary(self): - emoji_out = self.sanhedrin.truncate_chars(("a" * 4) + "🙂" + "tail", 8) - combining_out = self.sanhedrin.truncate_chars("Cafe\u0301 tail", 8) - - self.assertEqual(emoji_out, "aaaa🙂...") - self.assertEqual(combining_out, "Cafe...") - self.assertNotIn("\ufffd", emoji_out + combining_out) - self.assertFalse(self.sanhedrin.unicodedata.combining(combining_out[-4])) - (emoji_out + combining_out).encode("utf-8") - - def test_staged_evidence_is_used_without_smart_ingest_or_durable_write(self): - with tempfile.TemporaryDirectory() as tmp: - staged_path = Path(tmp) / "sanhedrin-staged-evidence.json" - staged = [ - { - "id": "samstage2", - "role": "memory", - "trust": 0.89, - "preview": "Sam's final result was second place with no payout.", - } - ] - staged_path.write_text(json.dumps(staged), encoding="utf-8") - - post_urls = [] - - def fake_post_json(url, body, _timeout): - post_urls.append(url) - if "smart_ingest" in url or "/api/memories" in url: - self.fail(f"staged evidence path attempted durable write to {url}: {body}") - self.assertEqual(url, "http://127.0.0.1:3927/api/deep_reference") - return {"confidence": 0.0, "evidence": []} - - env = { - "VESTIGE_SANHEDRIN_CLAIM_MODE": "1", - "VESTIGE_SANHEDRIN_OUTPUT": "json", - "VESTIGE_SANHEDRIN_STAGE_FILE": str(staged_path), - } - with mock.patch.dict(os.environ, env, clear=False), patched_attr( - self.sanhedrin, "post_json", fake_post_json - ), patched_attr(self.sanhedrin, "VESTIGE_ENDPOINT", "http://127.0.0.1:3927/api/deep_reference"): - out = self.run_main("Sam won first place and earned prize money.") - - result = json.loads(out) - verdict = result["verdicts"][0] - self.assertFalse(result["passed"], result) - self.assertEqual(result["staged_evidence_count"], 1) - self.assertEqual(verdict["status"], "REFUTED_BY_ABSENCE") - self.assertEqual(verdict["durable_evidence_count"], 0) - self.assertEqual(verdict["high_trust_evidence_count"], 1) - self.assertEqual(post_urls, ["http://127.0.0.1:3927/api/deep_reference"]) - - def test_staged_only_refuted_verdict_is_downgraded_without_durable_evidence(self): - with tempfile.TemporaryDirectory() as tmp: - staged_path = Path(tmp) / "sanhedrin-staged-evidence.json" - staged_path.write_text( - json.dumps( - [ - { - "id": "stage-tech", - "trust": 0.95, - "preview": "Qwen3.6-35B cannot be served through a chat endpoint.", - } - ] - ), - encoding="utf-8", - ) - - def fake_post_json(url, _body, _timeout): - if url == self.sanhedrin.VESTIGE_ENDPOINT: - return {"confidence": 0.0, "evidence": []} - if url == self.sanhedrin.SANHEDRIN_ENDPOINT: - return { - "choices": [ - { - "message": { - "content": json.dumps( - { - "status": "REFUTED", - "class": "TECHNICAL", - "reason": "Staged evidence contradicts the claim.", - "evidence_ids": ["stage-tech"], - } - ) - } - } - ] - } - self.fail(f"unexpected post_json URL: {url}") - - env = { - "VESTIGE_SANHEDRIN_CLAIM_MODE": "1", - "VESTIGE_SANHEDRIN_OUTPUT": "json", - "VESTIGE_SANHEDRIN_STAGE_FILE": str(staged_path), - } - with mock.patch.dict(os.environ, env, clear=False), patched_attr( - self.sanhedrin, "post_json", fake_post_json - ), patched_attr( - self.sanhedrin, "SANHEDRIN_ENDPOINT", "http://127.0.0.1:8080/v1/chat/completions" - ), patched_attr(self.sanhedrin, "MODEL", "test-verifier"): - out = self.run_main( - "Qwen3.6-35B can be served through an OpenAI-compatible chat endpoint." - ) - - result = json.loads(out) - verdict = result["verdicts"][0] - self.assertTrue(result["passed"], result) - self.assertEqual(verdict["status"], "NEI") - self.assertEqual(verdict["durable_evidence_count"], 0) - self.assertIn("Durable evidence required", verdict["reason"]) - - def test_staged_only_supported_verdict_is_downgraded_without_durable_evidence(self): - with tempfile.TemporaryDirectory() as tmp: - staged_path = Path(tmp) / "sanhedrin-staged-evidence.json" - staged_path.write_text( - json.dumps( - [ - { - "id": "stage-tech", - "trust": 0.95, - "preview": "Qwen3.6-35B can be served through a chat endpoint.", - } - ] - ), - encoding="utf-8", - ) - - def fake_post_json(url, _body, _timeout): - if url == self.sanhedrin.VESTIGE_ENDPOINT: - return {"confidence": 0.0, "evidence": []} - if url == self.sanhedrin.SANHEDRIN_ENDPOINT: - return { - "choices": [ - { - "message": { - "content": json.dumps( - { - "status": "SUPPORTED", - "class": "TECHNICAL", - "reason": "Staged evidence supports the claim.", - "evidence_ids": ["stage-tech"], - } - ) - } - } - ] - } - self.fail(f"unexpected post_json URL: {url}") - - env = { - "VESTIGE_SANHEDRIN_CLAIM_MODE": "1", - "VESTIGE_SANHEDRIN_OUTPUT": "json", - "VESTIGE_SANHEDRIN_STAGE_FILE": str(staged_path), - } - with mock.patch.dict(os.environ, env, clear=False), patched_attr( - self.sanhedrin, "post_json", fake_post_json - ), patched_attr( - self.sanhedrin, "SANHEDRIN_ENDPOINT", "http://127.0.0.1:8080/v1/chat/completions" - ), patched_attr(self.sanhedrin, "MODEL", "test-verifier"): - out = self.run_main( - "Qwen3.6-35B can be served through an OpenAI-compatible chat endpoint." - ) - - result = json.loads(out) - verdict = result["verdicts"][0] - self.assertTrue(result["passed"], result) - self.assertEqual(verdict["status"], "NEI") - self.assertEqual(verdict["durable_evidence_count"], 0) - self.assertIn("Durable evidence required", verdict["reason"]) - - def test_supported_verdict_with_durable_evidence_is_preserved(self): - evidence = [ - self.sanhedrin.EvidenceItem( - id="mem-durable", - preview="A reliable memory says this backend can use a compatible endpoint.", - trust=0.95, - durable=True, - source="vestige", - ) - ] - claim = self.sanhedrin.Claim( - text="Qwen3.6-35B can be served through an OpenAI-compatible chat endpoint.", - claim_class="TECHNICAL", - source_index=0, - sam_critical=False, - ) - verdict = self.sanhedrin.validate_structured_verdict( - claim, - {"status": "SUPPORTED", "class": "TECHNICAL", "reason": "Evidence supports it."}, - evidence, - ) - - self.assertEqual(verdict.status, "SUPPORTED") - - def test_openai_key_is_not_forwarded_to_arbitrary_or_vestige_endpoints(self): - captured_headers = [] - - class FakeResponse: - def __enter__(self): - return self - - def __exit__(self, *_args): - return False - - def read(self): - return b"{}" - - def fake_urlopen(req, timeout=None): - captured_headers.append(dict(req.header_items())) - return FakeResponse() - - env = {"OPENAI_API_KEY": "real-openai-key"} - with mock.patch.dict(os.environ, env, clear=False), patched_attr( - self.sanhedrin, "SANHEDRIN_ENDPOINT", "http://127.0.0.1:8080/v1/chat/completions" - ), mock.patch.object( - self.sanhedrin.urllib.request, "urlopen", fake_urlopen - ): - self.sanhedrin.post_json(self.sanhedrin.SANHEDRIN_ENDPOINT, {}, 1) - self.sanhedrin.post_json(self.sanhedrin.VESTIGE_ENDPOINT, {}, 1) - - self.assertTrue(captured_headers) - self.assertTrue(all("Authorization" not in headers for headers in captured_headers)) - - def test_sanhedrin_api_key_only_goes_to_configured_sanhedrin_endpoint(self): - captured_headers = [] - - class FakeResponse: - def __enter__(self): - return self - - def __exit__(self, *_args): - return False - - def read(self): - return b"{}" - - def fake_urlopen(req, timeout=None): - captured_headers.append(dict(req.header_items())) - return FakeResponse() - - env = {"VESTIGE_SANHEDRIN_API_KEY": "sanhedrin-only-key"} - with mock.patch.dict(os.environ, env, clear=False), patched_attr( - self.sanhedrin, "SANHEDRIN_ENDPOINT", "http://127.0.0.1:8080/v1/chat/completions" - ), mock.patch.object( - self.sanhedrin.urllib.request, "urlopen", fake_urlopen - ): - self.sanhedrin.post_json(self.sanhedrin.SANHEDRIN_ENDPOINT, {}, 1) - self.sanhedrin.post_json(self.sanhedrin.VESTIGE_ENDPOINT, {}, 1) - - self.assertIn("Authorization", captured_headers[0]) - self.assertNotIn("Authorization", captured_headers[1]) - - def test_strict_openai_body_omits_backend_specific_fields(self): - with patched_attr(self.sanhedrin, "SANHEDRIN_BACKEND", "openai"): - body = self.sanhedrin.sanhedrin_body( - [{"role": "user", "content": "judge"}], - 128, - ) - - self.assertNotIn("top_k", body) - self.assertNotIn("seed", body) - self.assertNotIn("chat_template_kwargs", body) - - def test_mlx_body_keeps_backend_specific_fields(self): - with patched_attr(self.sanhedrin, "SANHEDRIN_BACKEND", "mlx"): - body = self.sanhedrin.sanhedrin_body( - [{"role": "user", "content": "judge"}], - 128, - ) - - self.assertEqual(body["top_k"], 1) - self.assertEqual(body["chat_template_kwargs"], {"enable_thinking": False}) - - def test_staged_only_legacy_refuted_line_is_downgraded_without_durable_evidence(self): - with tempfile.TemporaryDirectory() as tmp: - staged_path = Path(tmp) / "sanhedrin-staged-evidence.json" - staged_path.write_text( - json.dumps( - [ - { - "id": "stage-tech", - "trust": 0.95, - "preview": "Qwen3.6-35B cannot be served through a chat endpoint.", - } - ] - ), - encoding="utf-8", - ) - - def fake_post_json(url, _body, _timeout): - if url == self.sanhedrin.VESTIGE_ENDPOINT: - return {"confidence": 0.0, "evidence": []} - if url == self.sanhedrin.SANHEDRIN_ENDPOINT: - return { - "choices": [ - { - "message": { - "content": ( - "no - [Sanhedrin Veto] [TECHNICAL]: " - "Staged evidence contradicts the claim." - ) - } - } - ] - } - self.fail(f"unexpected post_json URL: {url}") - - env = { - "VESTIGE_SANHEDRIN_CLAIM_MODE": "1", - "VESTIGE_SANHEDRIN_OUTPUT": "json", - "VESTIGE_SANHEDRIN_STAGE_FILE": str(staged_path), - } - with mock.patch.dict(os.environ, env, clear=False), patched_attr( - self.sanhedrin, "post_json", fake_post_json - ), patched_attr( - self.sanhedrin, "SANHEDRIN_ENDPOINT", "http://127.0.0.1:8080/v1/chat/completions" - ), patched_attr(self.sanhedrin, "MODEL", "test-verifier"): - out = self.run_main( - "Qwen3.6-35B can be served through an OpenAI-compatible chat endpoint." - ) - - result = json.loads(out) - verdict = result["verdicts"][0] - self.assertTrue(result["passed"], result) - self.assertEqual(verdict["status"], "NEI") - self.assertEqual(verdict["durable_evidence_count"], 0) - self.assertIn("Durable evidence required", verdict["reason"]) - - def test_current_turn_discourse_patterns_are_not_claims(self): - examples = [ - "You asked for maximum subagents, so I audited the hook.", - "Your request was to verify the installer env preservation.", - "Per your request, I reviewed the Sanhedrin stop hook.", - "Sam asked me to go all in on the Sanhedrin patch.", - "The user requested maximum subagents for this implementation.", - ] - for example in examples: - with self.subTest(example=example): - self.assertEqual(self.sanhedrin.extract_check_worthy_claims(example), []) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/hooks/test_sanhedrin_shell_env.py b/tests/hooks/test_sanhedrin_shell_env.py deleted file mode 100644 index 5de950c..0000000 --- a/tests/hooks/test_sanhedrin_shell_env.py +++ /dev/null @@ -1,48 +0,0 @@ -import os -import subprocess -import tempfile -import unittest -from pathlib import Path - - -REPO_ROOT = Path(__file__).resolve().parents[2] -SANHEDRIN_HOOK = REPO_ROOT / "hooks" / "sanhedrin.sh" - - -class SanhedrinShellEnvTests(unittest.TestCase): - def test_env_file_is_parsed_not_executed(self): - with tempfile.TemporaryDirectory() as tmp: - tmp_path = Path(tmp) - marker = tmp_path / "executed" - env_file = tmp_path / "vestige-sanhedrin.env" - env_file.write_text( - "\n".join( - [ - "VESTIGE_SANHEDRIN_ENABLED='1'", - "VESTIGE_SANHEDRIN_PYTHON='python3'", - f"VESTIGE_SANHEDRIN_MODEL='$(touch {marker})'", - "UNKNOWN_KEY='$(touch should-not-run)'", - ] - ) - + "\n", - encoding="utf-8", - ) - - env = os.environ.copy() - env["VESTIGE_SANHEDRIN_ENV"] = str(env_file) - result = subprocess.run( - ["bash", str(SANHEDRIN_HOOK)], - input='{"transcript_path":"/does/not/exist"}', - text=True, - env=env, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - check=False, - ) - - self.assertEqual(result.returncode, 0, result.stderr) - self.assertFalse(marker.exists()) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/hooks/test_sanhedrin_test_integrity_delta_fixtures.py b/tests/hooks/test_sanhedrin_test_integrity_delta_fixtures.py deleted file mode 100644 index b635485..0000000 --- a/tests/hooks/test_sanhedrin_test_integrity_delta_fixtures.py +++ /dev/null @@ -1,82 +0,0 @@ -import json -from pathlib import Path -import unittest - - -FIXTURE_DIR = ( - Path(__file__).resolve().parents[2] - / "docs" - / "fixtures" - / "sanhedrin-test-integrity-deltas" -) - - -class TestSanhedrinTestIntegrityDeltaFixtures(unittest.TestCase): - def test_fixture_receipts_are_executable_contract_examples(self): - fixtures = sorted(FIXTURE_DIR.glob("*.json")) - self.assertEqual( - [fixture.name for fixture in fixtures], - [ - "justified-snapshot.json", - "skipped-test.json", - "unchanged-good.json", - "weakened-assertion.json", - ], - ) - - expected_decisions = { - "justified-snapshot": "needs_human_review", - "skipped-test": "downgraded", - "unchanged-good": "accepted", - "weakened-assertion": "downgraded", - } - - for fixture in fixtures: - with self.subTest(fixture=fixture.name): - data = json.loads(fixture.read_text(encoding="utf-8")) - receipt = data["receipt"] - - self.assertEqual( - receipt["schema"], - "vestige.sanhedrin.test_integrity_delta.v1", - ) - self.assertEqual(data["expectedDecision"], receipt["decision"]) - self.assertEqual(expected_decisions[data["case"]], receipt["decision"]) - self.assertTrue(receipt["freshVerifier"]["checkedAfterLastRelevantEdit"]) - self.assertEqual(receipt["freshVerifier"]["exitCode"], 0) - - test_files = receipt["specSource"]["testFiles"] - self.assertGreaterEqual(len(test_files), 1) - for test_file in test_files: - self.assertTrue(test_file["path"]) - self.assertRegex( - test_file["hashBeforeImplementation"], - r"^sha256:[0-9a-f]{64}$", - ) - self.assertRegex( - test_file["hashAfterVerification"], - r"^sha256:[0-9a-f]{64}$", - ) - - def test_downgrade_fixtures_have_mechanical_downgrade_evidence(self): - for fixture in sorted(FIXTURE_DIR.glob("*.json")): - data = json.loads(fixture.read_text(encoding="utf-8")) - if data["expectedDecision"] != "downgraded": - continue - - delta = data["receipt"]["delta"] - has_downgrade_evidence = any( - [ - delta["removedOrDisabledTests"], - delta["removedAssertions"] > 0, - delta["weakenedExpectations"], - delta["snapshotChurnWithoutSourceChange"], - delta["coverageDelta"] < 0, - delta["mocksReplacingRealBoundary"], - ] - ) - self.assertTrue(has_downgrade_evidence, data["case"]) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/phase_1/Cargo.toml b/tests/phase_1/Cargo.toml deleted file mode 100644 index 80a9bff..0000000 --- a/tests/phase_1/Cargo.toml +++ /dev/null @@ -1,38 +0,0 @@ -[package] -name = "vestige-phase-1-tests" -version = "0.0.1" -edition = "2024" -publish = false - -[dependencies] -vestige-core = { path = "../../crates/vestige-core" } -tokio = { version = "1", features = ["macros", "rt-multi-thread"] } -tempfile = "3" -uuid = { version = "1", features = ["v4"] } -chrono = "0.4" -serde_json = "1" -rusqlite = { version = "0.38", features = ["bundled"] } - -[[test]] -name = "trait_round_trip" -path = "trait_round_trip.rs" - -[[test]] -name = "embedding_model_registry" -path = "embedding_model_registry.rs" - -[[test]] -name = "domain_column_migration" -path = "domain_column_migration.rs" - -[[test]] -name = "cognitive_module_isolation" -path = "cognitive_module_isolation.rs" - -[[test]] -name = "send_bound_variant" -path = "send_bound_variant.rs" - -[[test]] -name = "embedder_trait" -path = "embedder_trait.rs" diff --git a/tests/phase_1/cognitive_module_isolation.rs b/tests/phase_1/cognitive_module_isolation.rs deleted file mode 100644 index 0ff94b8..0000000 --- a/tests/phase_1/cognitive_module_isolation.rs +++ /dev/null @@ -1,143 +0,0 @@ -//! Phase 1 integration tests: cognitive modules compile against Arc. -//! The key goal is a compile-time gate: if any module still typed against -//! SqliteMemoryStore concretely, this would fail to compile. - -use chrono::Utc; -use std::sync::Arc; -use tempfile::tempdir; -use uuid::Uuid; -use vestige_core::storage::{MemoryEdge, MemoryRecord, MemoryStore, SqliteMemoryStore}; - -fn make_store() -> Arc { - let dir = tempdir().unwrap(); - let db = dir.path().join("test.db"); - std::mem::forget(dir); - Arc::new(SqliteMemoryStore::new(Some(db)).expect("create")) -} - -fn make_record(content: &str) -> MemoryRecord { - MemoryRecord { - id: Uuid::new_v4(), - domains: vec![], - domain_scores: Default::default(), - content: content.to_string(), - node_type: "fact".to_string(), - tags: vec!["isolation-test".to_string()], - embedding: None, - created_at: Utc::now(), - updated_at: Utc::now(), - metadata: serde_json::json!({}), - } -} - -/// Ensure the store: Arc call pattern compiles and runs through -/// a representative method from every cognitive module group. -#[tokio::test] -async fn all_modules_compile_against_dyn_store() { - let store: Arc = make_store(); - - // CRUD via trait - let rec = make_record("cognitive isolation test"); - let id = store.insert(&rec).await.expect("insert via dyn trait"); - let got = store - .get(id) - .await - .expect("get via dyn trait") - .expect("exists"); - assert_eq!(got.content, "cognitive isolation test"); - - // Graph edges via trait - let rec2 = make_record("linked node"); - let id2 = store.insert(&rec2).await.expect("insert 2"); - store - .add_edge(&MemoryEdge { - source_id: id, - target_id: id2, - edge_type: "semantic".to_string(), - weight: 0.8, - created_at: Utc::now(), - }) - .await - .expect("add_edge via dyn trait"); - - let edges = store - .get_edges(id, None) - .await - .expect("get_edges via dyn trait"); - assert!(!edges.is_empty()); - - // Search via trait - let results = store - .fts_search("cognitive", 5) - .await - .expect("fts_search via dyn trait"); - assert!(!results.is_empty()); - - // Stats and count via trait - let count = store.count().await.expect("count via dyn trait"); - assert!(count >= 2); - - let stats = store.get_stats().await.expect("get_stats via dyn trait"); - assert!(stats.total_memories >= 2); -} - -#[tokio::test] -async fn spreading_activation_traverses_via_trait() { - let store: Arc = make_store(); - let rec_a = make_record("spreading activation source"); - let rec_b = make_record("spreading activation neighbor"); - let id_a = rec_a.id; - let id_b = rec_b.id; - store.insert(&rec_a).await.expect("insert a"); - store.insert(&rec_b).await.expect("insert b"); - store - .add_edge(&MemoryEdge { - source_id: id_a, - target_id: id_b, - edge_type: "semantic".to_string(), - weight: 0.9, - created_at: Utc::now(), - }) - .await - .expect("add edge"); - - // get_neighbors simulates the spreading activation traversal path - let neighbors = store.get_neighbors(id_a, 1).await.expect("get_neighbors"); - let ids: Vec = neighbors.iter().map(|(r, _)| r.id).collect(); - assert!(ids.contains(&id_a)); - assert!(ids.contains(&id_b)); -} - -#[tokio::test] -async fn synaptic_tagging_consumes_records_via_trait() { - // Build a MemoryRecord from trait-returned data and exercise the - // SynapticTaggingSystem pipeline (constructing CapturedMemory from store data). - let store: Arc = make_store(); - let rec = make_record("synaptic tagging test memory"); - let id = store.insert(&rec).await.expect("insert"); - let got = store.get(id).await.expect("get").expect("exists"); - // The important thing is we got a MemoryRecord back from the dyn trait; - // SynapticTaggingSystem would take this record as input. - assert_eq!(got.id, id); - assert!(!got.content.is_empty()); -} - -#[tokio::test] -async fn hippocampal_index_built_from_store() { - // Exercise the fts_search -> HippocampalIndex indexing path. - let store: Arc = make_store(); - for i in 0..5usize { - let rec = make_record(&format!("hippocampal indexing topic {i}")); - store.insert(&rec).await.expect("insert"); - } - let results = store - .fts_search("hippocampal indexing", 10) - .await - .expect("fts_search"); - // Verify we get results and they have the correct fields - assert!(!results.is_empty()); - for r in &results { - assert!(!r.record.content.is_empty()); - assert!(r.score >= 0.0); - } -} diff --git a/tests/phase_1/domain_column_migration.rs b/tests/phase_1/domain_column_migration.rs deleted file mode 100644 index 031ca65..0000000 --- a/tests/phase_1/domain_column_migration.rs +++ /dev/null @@ -1,161 +0,0 @@ -//! Phase 1 integration tests: domain column migration and schema upgrade. - -use std::sync::Arc; -use tempfile::tempdir; -use uuid::Uuid; -use vestige_core::storage::{MemoryRecord, MemoryStore, SqliteMemoryStore}; - -#[tokio::test] -async fn fresh_db_has_v16_schema() { - let dir = tempdir().unwrap(); - let db = dir.path().join("fresh.db"); - let _store = SqliteMemoryStore::new(Some(db.clone())).expect("create"); - // Open a raw connection and check pragma - let conn = rusqlite::Connection::open(&db).expect("open"); - let cols: Vec = { - let mut stmt = conn.prepare("PRAGMA table_info(knowledge_nodes)").unwrap(); - stmt.query_map([], |row| row.get::<_, String>(1)) - .unwrap() - .map(|r| r.unwrap()) - .collect() - }; - assert!( - cols.contains(&"domains".to_string()), - "domains column must exist: {:?}", - cols - ); - assert!( - cols.contains(&"domain_scores".to_string()), - "domain_scores column must exist" - ); -} - -#[tokio::test] -async fn v11_db_upgrades_cleanly() { - use vestige_core::storage::MIGRATIONS; - let dir = tempdir().unwrap(); - let db = dir.path().join("v11.db"); - // Create DB with V11 migrations only - { - let conn = rusqlite::Connection::open(&db).expect("open"); - for m in MIGRATIONS.iter().filter(|m| m.version <= 11) { - conn.execute_batch(m.up).expect("apply migration"); - } - // Insert 5 rows under V11 schema - for i in 0..5usize { - conn.execute( - "INSERT INTO knowledge_nodes (id, content, node_type, created_at, updated_at, \ - last_accessed, stability, difficulty, reps, lapses, learning_state, \ - storage_strength, retrieval_strength, retention_strength, \ - next_review, scheduled_days, has_embedding) \ - VALUES (?1, ?2, 'fact', datetime('now'), datetime('now'), datetime('now'), \ - 1.0, 0.3, 0, 0, 'new', 1.0, 1.0, 1.0, datetime('now'), 1, 0)", - rusqlite::params![format!("pre-v16-{i}"), format!("content {i}"),], - ) - .expect("insert pre-v16 row"); - } - } - // Upgrade by opening through SqliteMemoryStore (triggers full migration) - let _store = SqliteMemoryStore::new(Some(db.clone())).expect("open with v16"); - // Check all 5 rows have empty domains/domain_scores - let conn = rusqlite::Connection::open(&db).expect("open raw"); - let count: i64 = conn - .query_row( - "SELECT COUNT(*) FROM knowledge_nodes WHERE domains='[]' AND domain_scores='{}'", - [], - |row| row.get(0), - ) - .expect("count"); - assert_eq!( - count, 5, - "all pre-v16 rows must have empty domains/domain_scores" - ); -} - -#[tokio::test] -async fn empty_domains_serialize_as_brackets() { - let dir = tempdir().unwrap(); - let db = dir.path().join("empty_domains.db"); - let store = SqliteMemoryStore::new(Some(db.clone())).expect("create"); - let rec = MemoryRecord { - id: Uuid::new_v4(), - domains: vec![], - domain_scores: Default::default(), - content: "test content".to_string(), - node_type: "fact".to_string(), - tags: vec![], - embedding: None, - created_at: chrono::Utc::now(), - updated_at: chrono::Utc::now(), - metadata: serde_json::json!({}), - }; - store.insert(&rec).await.expect("insert"); - // Check raw sqlite value - let conn = rusqlite::Connection::open(&db).expect("open raw"); - let (domains, domain_scores): (String, String) = conn - .query_row( - "SELECT domains, domain_scores FROM knowledge_nodes LIMIT 1", - [], - |row| Ok((row.get(0)?, row.get(1)?)), - ) - .expect("query"); - assert_eq!( - domains, "[]", - "empty domains should store as '[]', not NULL" - ); - assert_eq!( - domain_scores, "{}", - "empty domain_scores should store as '{{}}'" - ); -} - -#[tokio::test] -async fn populated_domains_round_trip() { - let dir = tempdir().unwrap(); - let db = dir.path().join("populated.db"); - let store: Arc = Arc::new(SqliteMemoryStore::new(Some(db)).expect("create")); - let mut rec = MemoryRecord { - id: Uuid::new_v4(), - domains: vec!["dev".to_string(), "infra".to_string()], - domain_scores: { - let mut m = std::collections::HashMap::new(); - m.insert("dev".to_string(), 0.82); - m.insert("infra".to_string(), 0.71); - m - }, - content: "populated domains test".to_string(), - node_type: "fact".to_string(), - tags: vec![], - embedding: None, - created_at: chrono::Utc::now(), - updated_at: chrono::Utc::now(), - metadata: serde_json::json!({}), - }; - let id = store.insert(&rec).await.expect("insert"); - // Update the domains via update() - rec.id = id; - store.update(&rec).await.expect("update with domains"); - // Read back and verify - let got = store.get(id).await.expect("get").expect("exists"); - let mut expected_domains = got.domains.clone(); - expected_domains.sort(); - assert_eq!(expected_domains, vec!["dev", "infra"]); - assert!((got.domain_scores["dev"] - 0.82).abs() < 0.001); - assert!((got.domain_scores["infra"] - 0.71).abs() < 0.001); -} - -#[tokio::test] -async fn domains_table_exists() { - let dir = tempdir().unwrap(); - let db = dir.path().join("domains_table.db"); - let _store = SqliteMemoryStore::new(Some(db.clone())).expect("create"); - let conn = rusqlite::Connection::open(&db).expect("open raw"); - let count: i64 = conn - .query_row( - "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='domains'", - [], - |row| row.get(0), - ) - .expect("query"); - assert_eq!(count, 1, "domains table must exist after V16 migration"); -} diff --git a/tests/phase_1/embedder_trait.rs b/tests/phase_1/embedder_trait.rs deleted file mode 100644 index 9e96da1..0000000 --- a/tests/phase_1/embedder_trait.rs +++ /dev/null @@ -1,43 +0,0 @@ -//! Phase 1 integration tests: Embedder trait and FastembedEmbedder. - -use std::sync::Arc; -use tempfile::tempdir; -use vestige_core::embedder::{Embedder, FastembedEmbedder}; -use vestige_core::storage::MemoryStore; -use vestige_core::storage::SqliteMemoryStore; - -fn make_store() -> Arc { - let dir = tempdir().unwrap(); - let db = dir.path().join("test.db"); - std::mem::forget(dir); - Arc::new(SqliteMemoryStore::new(Some(db)).expect("create")) -} - -#[tokio::test] -async fn fastembed_implements_embedder_trait() { - // The key test: `Box` compiles - let e: Box = Box::new(FastembedEmbedder::new()); - assert_eq!(e.dimension(), 256, "dimension must be 256"); - assert!(!e.model_name().is_empty(), "model_name must not be empty"); - assert!(!e.model_hash().is_empty(), "model_hash must not be empty"); - assert_eq!(e.model_hash().len(), 64, "hash must be 64 hex chars"); -} - -#[tokio::test] -async fn signature_matches_memory_store_registry() { - let e = FastembedEmbedder::new(); - let sig = e.signature(); - let store = make_store(); - store - .register_model(&sig) - .await - .expect("register via Embedder::signature"); - let got = store - .registered_model() - .await - .expect("registered_model") - .expect("Some"); - assert_eq!(got.name, sig.name); - assert_eq!(got.dimension, sig.dimension); - assert_eq!(got.hash, sig.hash); -} diff --git a/tests/phase_1/embedding_model_registry.rs b/tests/phase_1/embedding_model_registry.rs deleted file mode 100644 index 3c001ea..0000000 --- a/tests/phase_1/embedding_model_registry.rs +++ /dev/null @@ -1,148 +0,0 @@ -//! Phase 1 integration tests: embedding model registry. - -use std::sync::Arc; -use tempfile::tempdir; -use uuid::Uuid; -use vestige_core::storage::{ - MemoryRecord, MemoryStore, MemoryStoreError, ModelSignature, SqliteMemoryStore, -}; - -fn make_store() -> Arc { - let dir = tempdir().unwrap(); - let db = dir.path().join("test.db"); - std::mem::forget(dir); - let store = SqliteMemoryStore::new(Some(db)).expect("create store"); - Arc::new(store) -} - -fn sig_a() -> ModelSignature { - ModelSignature { - name: "model-a".to_string(), - dimension: 256, - hash: "a".repeat(64), - } -} - -fn sig_b() -> ModelSignature { - ModelSignature { - name: "model-b".to_string(), - dimension: 256, - hash: "b".repeat(64), - } -} - -fn record_without_embedding() -> MemoryRecord { - MemoryRecord { - id: Uuid::new_v4(), - domains: vec![], - domain_scores: Default::default(), - content: "plain text memory".to_string(), - node_type: "fact".to_string(), - tags: vec![], - embedding: None, - created_at: chrono::Utc::now(), - updated_at: chrono::Utc::now(), - metadata: serde_json::json!({}), - } -} - -#[tokio::test] -async fn first_embedded_insert_auto_registers() { - // fresh store; register a model, then check registered_model() returns Some - let store = make_store(); - let sig = sig_a(); - store.register_model(&sig).await.expect("register"); - let got = store.registered_model().await.expect("registered_model"); - assert_eq!(got, Some(sig)); -} - -#[tokio::test] -async fn second_insert_with_same_signature_succeeds() { - let store = make_store(); - let sig = sig_a(); - store.register_model(&sig).await.expect("first register"); - store - .register_model(&sig) - .await - .expect("second register idempotent"); -} - -#[tokio::test] -async fn second_insert_with_different_dimension_refused() { - let store = make_store(); - let sig = sig_a(); // dim 256 - store.register_model(&sig).await.expect("register 256"); - // Try inserting a 512-dim vector into a store registered for 256 - let mut rec = record_without_embedding(); - rec.embedding = Some(vec![0.0f32; 512]); - rec.metadata = serde_json::json!({ - "model_name": "model-a", - "model_dim": 256_u64, - "model_hash": "a".repeat(64), - }); - let err = store.insert(&rec).await.unwrap_err(); - assert!( - matches!(err, MemoryStoreError::InvalidInput(_)), - "expected InvalidInput for dim mismatch, got {:?}", - err - ); -} - -#[tokio::test] -async fn second_insert_with_different_model_name_refused() { - let store = make_store(); - store.register_model(&sig_a()).await.expect("register a"); - let err = store.register_model(&sig_b()).await.unwrap_err(); - assert!( - matches!(err, MemoryStoreError::ModelMismatch { .. }), - "expected ModelMismatch, got {:?}", - err - ); -} - -#[tokio::test] -async fn second_insert_with_different_hash_refused() { - let store = make_store(); - let sig = sig_a(); - store.register_model(&sig).await.expect("register"); - let sig_diff_hash = ModelSignature { - name: "model-a".to_string(), - dimension: 256, - hash: "c".repeat(64), // different hash - }; - let err = store.register_model(&sig_diff_hash).await.unwrap_err(); - assert!( - matches!(err, MemoryStoreError::ModelMismatch { .. }), - "expected ModelMismatch for different hash, got {:?}", - err - ); -} - -#[tokio::test] -async fn no_embedding_insert_allowed_before_registration() { - let store = make_store(); - // registered_model() should be None - assert!( - store - .registered_model() - .await - .expect("registered_model") - .is_none() - ); - // A plain text memory without an embedding must insert successfully - let rec = record_without_embedding(); - store - .insert(&rec) - .await - .expect("plain insert before registration"); -} - -#[tokio::test] -async fn stats_reports_registered_model_after_first_write() { - let store = make_store(); - let sig = sig_a(); - store.register_model(&sig).await.expect("register"); - let stats = store.get_stats().await.expect("stats"); - assert_eq!(stats.registered_model_name, Some("model-a".to_string())); - assert_eq!(stats.registered_model_dim, Some(256)); -} diff --git a/tests/phase_1/send_bound_variant.rs b/tests/phase_1/send_bound_variant.rs deleted file mode 100644 index c0f02ef..0000000 --- a/tests/phase_1/send_bound_variant.rs +++ /dev/null @@ -1,99 +0,0 @@ -//! Phase 1 integration tests: Arc moves across tokio::spawn. -//! -//! This verifies that `#[trait_variant::make(MemoryStore: Send)]` actually -//! produces a Send-bound future so Arc is movable. - -use chrono::Utc; -use std::sync::Arc; -use tempfile::tempdir; -use uuid::Uuid; -use vestige_core::storage::{MemoryRecord, MemoryStore, SqliteMemoryStore}; - -fn make_store() -> Arc { - let dir = tempdir().unwrap(); - let db = dir.path().join("send_test.db"); - std::mem::forget(dir); - Arc::new(SqliteMemoryStore::new(Some(db)).expect("create")) -} - -fn make_record(content: &str) -> MemoryRecord { - MemoryRecord { - id: Uuid::new_v4(), - domains: vec![], - domain_scores: Default::default(), - content: content.to_string(), - node_type: "fact".to_string(), - tags: vec![], - embedding: None, - created_at: Utc::now(), - updated_at: Utc::now(), - metadata: serde_json::json!({}), - } -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 4)] -async fn arc_dyn_memory_store_moves_across_tokio_tasks() { - let store: Arc = make_store(); - let mut handles = Vec::new(); - for t in 0..16usize { - let store = Arc::clone(&store); - let handle = tokio::spawn(async move { - for i in 0..10usize { - let rec = make_record(&format!("task {t} memory {i}")); - store.insert(&rec).await.expect("insert in spawned task"); - } - }); - handles.push(handle); - } - for h in handles { - h.await.expect("task completed without panic"); - } - let count = store.count().await.expect("count"); - assert_eq!(count, 160, "all 16*10 inserts must be counted"); -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 4)] -async fn concurrent_readers_one_writer() { - let store: Arc = make_store(); - // Pre-populate with some data so readers have something to find - for i in 0..10usize { - let rec = make_record(&format!("concurrent reader memory {i}")); - store.insert(&rec).await.expect("pre-insert"); - } - - let mut handles = Vec::new(); - - // 32 concurrent readers - for _ in 0..32usize { - let store = Arc::clone(&store); - let handle = tokio::spawn(async move { - let results = store.fts_search("concurrent reader", 5).await; - // Should not panic even if results vary due to concurrent writes - results.expect("fts_search in concurrent reader"); - }); - handles.push(handle); - } - - // 1 writer inserting more records - { - let store = Arc::clone(&store); - let writer_handle = tokio::spawn(async move { - for i in 0..20usize { - let rec = make_record(&format!("writer record {i}")); - store.insert(&rec).await.expect("concurrent insert"); - } - }); - handles.push(writer_handle); - } - - for h in handles { - h.await.expect("no panics"); - } - - // Eventual consistency check: total count should be at least 10 (initial) - let count = store.count().await.expect("final count"); - assert!( - count >= 10, - "at least the pre-populated records must persist" - ); -} diff --git a/tests/phase_1/trait_round_trip.rs b/tests/phase_1/trait_round_trip.rs deleted file mode 100644 index ab3e0b2..0000000 --- a/tests/phase_1/trait_round_trip.rs +++ /dev/null @@ -1,217 +0,0 @@ -//! Phase 1 integration tests: round-trip of every trait method through SqliteMemoryStore. - -use chrono::Utc; -use std::sync::Arc; -use tempfile::tempdir; -use uuid::Uuid; -use vestige_core::storage::{ - MemoryEdge, MemoryRecord, MemoryStore, SearchQuery, SqliteMemoryStore, -}; - -fn make_store() -> Arc { - let dir = tempdir().unwrap(); - let db = dir.path().join("test.db"); - // keep the dir alive by leaking it -- this is fine for tests - std::mem::forget(dir); - let store = SqliteMemoryStore::new(Some(db)).expect("create store"); - Arc::new(store) -} - -fn make_record(content: &str) -> MemoryRecord { - MemoryRecord { - id: Uuid::new_v4(), - domains: vec![], - domain_scores: Default::default(), - content: content.to_string(), - node_type: "fact".to_string(), - tags: vec!["integration".to_string()], - embedding: None, - created_at: Utc::now(), - updated_at: Utc::now(), - metadata: serde_json::json!({}), - } -} - -#[tokio::test] -async fn insert_get_update_delete() { - let store = make_store(); - let rec = make_record("round-trip CRUD test"); - let id = rec.id; - - store.insert(&rec).await.expect("insert"); - let got = store.get(id).await.expect("get").expect("exists"); - assert_eq!(got.content, "round-trip CRUD test"); - assert_eq!(got.node_type, "fact"); - assert!(got.domains.is_empty()); - assert!(got.domain_scores.is_empty()); - - let mut updated = got; - updated.content = "updated content".to_string(); - store.update(&updated).await.expect("update"); - - let after_update = store - .get(id) - .await - .expect("get after update") - .expect("exists"); - assert_eq!(after_update.content, "updated content"); - - store.delete(id).await.expect("delete"); - let after_delete = store.get(id).await.expect("get after delete"); - assert!(after_delete.is_none()); -} - -#[tokio::test] -async fn scheduling_upsert_and_due_scan() { - use vestige_core::storage::SchedulingState; - let store = make_store(); - - for i in 0..3usize { - let rec = make_record(&format!("sched memory {i}")); - let id = rec.id; - store.insert(&rec).await.expect("insert"); - let next_review = Utc::now() - chrono::Duration::days((i as i64) + 1); - let state = SchedulingState { - memory_id: id, - stability: 1.0, - difficulty: 0.3, - retrievability: 0.7, - last_review: Some(Utc::now()), - next_review: Some(next_review), - reps: 1, - lapses: 0, - }; - store - .update_scheduling(&state) - .await - .expect("update scheduling"); - } - - let due = store - .get_due_memories(Utc::now(), 10) - .await - .expect("get_due_memories"); - assert_eq!(due.len(), 3, "all 3 should be due"); -} - -#[tokio::test] -async fn edge_crud() { - let store = make_store(); - let rec_a = make_record("edge node A"); - let rec_b = make_record("edge node B"); - let id_a = rec_a.id; - let id_b = rec_b.id; - store.insert(&rec_a).await.expect("insert a"); - store.insert(&rec_b).await.expect("insert b"); - - let edge = MemoryEdge { - source_id: id_a, - target_id: id_b, - edge_type: "semantic".to_string(), - weight: 0.85, - created_at: Utc::now(), - }; - store.add_edge(&edge).await.expect("add edge"); - - let edges = store.get_edges(id_a, None).await.expect("get edges"); - assert!(!edges.is_empty()); - - store.remove_edge(id_a, id_b).await.expect("remove edge"); - let after = store.get_edges(id_a, None).await.expect("get edges after"); - assert!(after.is_empty()); -} - -#[tokio::test] -async fn count_and_stats_track_inserts() { - let store = make_store(); - for i in 0..10usize { - let rec = make_record(&format!("stats memory {i}")); - store.insert(&rec).await.expect("insert"); - } - assert_eq!(store.count().await.expect("count"), 10); - let stats = store.get_stats().await.expect("stats"); - assert_eq!(stats.total_memories, 10); -} - -#[tokio::test] -async fn vacuum_after_deletes_reclaims() { - let dir = tempdir().unwrap(); - let db = dir.path().join("vacuum_test.db"); - let store = SqliteMemoryStore::new(Some(db)).expect("create store"); - let store: Arc = Arc::new(store); - - let mut ids = Vec::new(); - for i in 0..50usize { - let rec = make_record(&format!("vacuum memory {i}")); - let id = store.insert(&rec).await.expect("insert"); - ids.push(id); - } - for id in &ids[..40] { - store.delete(*id).await.expect("delete"); - } - // vacuum should not error - store.vacuum().await.expect("vacuum"); -} - -#[tokio::test] -async fn list_domains_empty_then_upsert_then_delete() { - use vestige_core::storage::Domain; - let store = make_store(); - - let domains = store.list_domains().await.expect("list empty"); - assert!(domains.is_empty()); - - let d = Domain { - id: "test-domain".to_string(), - label: "Test Domain".to_string(), - centroid: vec![0.1f32, 0.2, 0.3], - top_terms: vec!["term1".to_string()], - memory_count: 5, - created_at: Utc::now(), - }; - store.upsert_domain(&d).await.expect("upsert domain"); - let after = store.list_domains().await.expect("list after upsert"); - assert_eq!(after.len(), 1); - assert_eq!(after[0].id, "test-domain"); - - store - .delete_domain("test-domain") - .await - .expect("delete domain"); - let after_delete = store.list_domains().await.expect("list after delete"); - assert!(after_delete.is_empty()); -} - -#[tokio::test] -async fn classify_with_no_domains_returns_empty() { - let store = make_store(); - let result = store.classify(&[0.1f32, 0.2, 0.3]).await.expect("classify"); - assert!(result.is_empty()); -} - -#[tokio::test] -async fn search_hybrid_returns_results() { - let store = make_store(); - let rec = make_record("quantum entanglement superposition physics"); - store.insert(&rec).await.expect("insert"); - - // Verify fts_search works first (sanity check) - let fts_results = store.fts_search("quantum", 10).await.expect("fts_search"); - assert!( - !fts_results.is_empty(), - "fts_search must find 'quantum' after insert" - ); - - let query = SearchQuery { - text: Some("quantum physics".to_string()), - limit: 10, - ..Default::default() - }; - let results = store.search(&query).await.expect("search"); - // FTS results should include our inserted record - assert!( - !results.is_empty(), - "search must return results for 'quantum physics'" - ); - assert!(results[0].score >= 0.0); -}