From 0ee99e271d5d21853e1177ce90a65aa5cf6ab0c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 28 May 2026 22:08:08 -0400 Subject: [PATCH] =?UTF-8?q?Ely-Eye=20=E2=80=94=20local=20multimodal=20cont?= =?UTF-8?q?ext=20operating=20system=20on=20Qwen3.5-9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Real-models implementation: multimodal ingestion, Context Cartridge memory, sparse + Qwen embedding retrieval, Cache Fabric, Qwen runtime adapters, verifier contracts, Visual HashHop proofs, BF16 LoRA fine-tuning, and a local dashboard. Verified by a 15-check PRD proof suite on the live model. --- .env.example | 9 + .gitignore | 29 + Dockerfile | 26 + README.md | 150 + TRAINING_ARTIFACTS.md | 144 + backend/README.md | 3 + backend/pyproject.toml | 56 + backend/requirements-ktransformers-linux.txt | 5 + backend/requirements-sglang-linux.txt | 1 + backend/requirements-vllm-linux.txt | 3 + backend/src/ely_eye/__init__.py | 4 + backend/src/ely_eye/api.py | 398 +++ backend/src/ely_eye/benchmarks.py | 617 ++++ backend/src/ely_eye/cache_fabric.py | 57 + backend/src/ely_eye/cartridge.py | 303 ++ backend/src/ely_eye/cartridge_assets.py | 335 ++ backend/src/ely_eye/cli.py | 334 ++ backend/src/ely_eye/compiler.py | 137 + backend/src/ely_eye/config.py | 80 + backend/src/ely_eye/db.py | 419 +++ backend/src/ely_eye/dvkv.py | 55 + backend/src/ely_eye/embeddings.py | 94 + backend/src/ely_eye/evals.py | 2413 ++++++++++++++ backend/src/ely_eye/evidence_graph.py | 68 + backend/src/ely_eye/ingestion.py | 657 ++++ backend/src/ely_eye/ocr.py | 41 + backend/src/ely_eye/profiles.py | 420 +++ backend/src/ely_eye/research_modules.py | 221 ++ backend/src/ely_eye/retrieval.py | 101 + backend/src/ely_eye/runtime.py | 454 +++ backend/src/ely_eye/schemas.py | 479 +++ backend/src/ely_eye/storage.py | 78 + backend/src/ely_eye/training.py | 1640 ++++++++++ backend/src/ely_eye/verifier.py | 232 ++ docker-compose.yml | 18 + frontend/eslint.config.js | 32 + frontend/index.html | 12 + frontend/package-lock.json | 3019 ++++++++++++++++++ frontend/package.json | 33 + frontend/scripts/verify-ui.mjs | 32 + frontend/src/App.tsx | 517 +++ frontend/src/api.ts | 82 + frontend/src/main.tsx | 10 + frontend/src/styles.css | 444 +++ frontend/src/types.ts | 188 ++ frontend/tsconfig.json | 21 + frontend/tsconfig.node.json | 13 + frontend/vite.config.ts | 13 + scripts/download-models.ps1 | 7 + scripts/download_models.py | 17 + scripts/install-cuda-torch.ps1 | 5 + scripts/install.ps1 | 21 + scripts/launch-ktransformers-extreme.ps1 | 23 + scripts/launch-llamacpp-offline.ps1 | 35 + scripts/launch-sglang-extreme.ps1 | 25 + scripts/launch-sglang-live.ps1 | 26 + scripts/launch-vllm-extreme.ps1 | 26 + scripts/launch-vllm-live.ps1 | 24 + scripts/run-all-prd-finetunes.ps1 | 100 + scripts/run-prd-finetune.ps1 | 41 + scripts/serve-backend.ps1 | 4 + scripts/serve-frontend.ps1 | 6 + scripts/setup-linux-runtimes.ps1 | 23 + scripts/verify.ps1 | 10 + 64 files changed, 14890 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 TRAINING_ARTIFACTS.md create mode 100644 backend/README.md create mode 100644 backend/pyproject.toml create mode 100644 backend/requirements-ktransformers-linux.txt create mode 100644 backend/requirements-sglang-linux.txt create mode 100644 backend/requirements-vllm-linux.txt create mode 100644 backend/src/ely_eye/__init__.py create mode 100644 backend/src/ely_eye/api.py create mode 100644 backend/src/ely_eye/benchmarks.py create mode 100644 backend/src/ely_eye/cache_fabric.py create mode 100644 backend/src/ely_eye/cartridge.py create mode 100644 backend/src/ely_eye/cartridge_assets.py create mode 100644 backend/src/ely_eye/cli.py create mode 100644 backend/src/ely_eye/compiler.py create mode 100644 backend/src/ely_eye/config.py create mode 100644 backend/src/ely_eye/db.py create mode 100644 backend/src/ely_eye/dvkv.py create mode 100644 backend/src/ely_eye/embeddings.py create mode 100644 backend/src/ely_eye/evals.py create mode 100644 backend/src/ely_eye/evidence_graph.py create mode 100644 backend/src/ely_eye/ingestion.py create mode 100644 backend/src/ely_eye/ocr.py create mode 100644 backend/src/ely_eye/profiles.py create mode 100644 backend/src/ely_eye/research_modules.py create mode 100644 backend/src/ely_eye/retrieval.py create mode 100644 backend/src/ely_eye/runtime.py create mode 100644 backend/src/ely_eye/schemas.py create mode 100644 backend/src/ely_eye/storage.py create mode 100644 backend/src/ely_eye/training.py create mode 100644 backend/src/ely_eye/verifier.py create mode 100644 docker-compose.yml create mode 100644 frontend/eslint.config.js create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/scripts/verify-ui.mjs create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/api.ts create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/styles.css create mode 100644 frontend/src/types.ts create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts create mode 100644 scripts/download-models.ps1 create mode 100644 scripts/download_models.py create mode 100644 scripts/install-cuda-torch.ps1 create mode 100644 scripts/install.ps1 create mode 100644 scripts/launch-ktransformers-extreme.ps1 create mode 100644 scripts/launch-llamacpp-offline.ps1 create mode 100644 scripts/launch-sglang-extreme.ps1 create mode 100644 scripts/launch-sglang-live.ps1 create mode 100644 scripts/launch-vllm-extreme.ps1 create mode 100644 scripts/launch-vllm-live.ps1 create mode 100644 scripts/run-all-prd-finetunes.ps1 create mode 100644 scripts/run-prd-finetune.ps1 create mode 100644 scripts/serve-backend.ps1 create mode 100644 scripts/serve-frontend.ps1 create mode 100644 scripts/setup-linux-runtimes.ps1 create mode 100644 scripts/verify.ps1 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..60f627e --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +ELY_EYE_HOME=.ely_eye +ELY_EYE_RUNTIME_BACKEND=transformers +ELY_EYE_SGLANG_BASE_URL=http://127.0.0.1:8000/v1 +ELY_EYE_SGLANG_API_KEY=ely-eye-local +ELY_EYE_MODEL_ID=Qwen/Qwen3.5-9B +ELY_EYE_ADAPTER_DIR=.ely_eye/adapters/hme_core_lora_qwen35_9b +ELY_EYE_PLANNER_MODEL_ID=Qwen/Qwen3.5-0.8B +ELY_EYE_EMBEDDING_MODEL_ID=Qwen/Qwen3-VL-Embedding-8B +ELY_EYE_DENSE_EMBEDDINGS_ENABLED=false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3cba56e --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +.venv/ +.venv-linux*/ +node_modules/ +dist/ +build/ +.ely_eye/ +*.pyc +__pycache__/ +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.env +.env.local +frontend/.vite/ +frontend/dist/ +backend/src/ely_eye.egg-info/ +*.log +*.tsbuildinfo +frontend/vite.config.d.ts +frontend/vite.config.js +unsloth_compiled_cache/ +*.sqlite-wal +*.sqlite-shm +PRD.md +AGENTS.md +CLAUDE.md +designreference.html +# Local landing site — deployed to Cloudflare, not part of this repo +/ely-eye-web/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9254fcf --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +FROM nvidia/cuda:13.3.0-runtime-ubuntu24.04 + +ENV PYTHONUNBUFFERED=1 \ + ELY_EYE_HOME=/data/ely-eye \ + ELY_EYE_RUNTIME_BACKEND=sglang \ + ELY_EYE_SGLANG_BASE_URL=http://127.0.0.1:8000/v1 + +WORKDIR /app + +RUN apt-get update \ + && apt-get install -y --no-install-recommends python3.12 python3.12-venv python3-pip nodejs npm ffmpeg \ + && rm -rf /var/lib/apt/lists/* + +COPY backend ./backend +COPY frontend ./frontend +COPY README.md PRD.md ./ + +RUN python3.12 -m venv /opt/ely-eye \ + && /opt/ely-eye/bin/pip install --upgrade pip setuptools wheel \ + && /opt/ely-eye/bin/pip install -e ./backend \ + && cd frontend \ + && npm install \ + && npm run build + +EXPOSE 8765 +CMD ["/opt/ely-eye/bin/ely-eye", "serve", "--host", "0.0.0.0", "--port", "8765"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..bbb72d5 --- /dev/null +++ b/README.md @@ -0,0 +1,150 @@ +# Ely-Eye + +Ely-Eye is a local multimodal context operating system for Qwen3.5-9B. The implementation follows `PRD.md`: ingestion, Evidence Atoms, Context Cartridge, sparse and optional Qwen embedding retrieval, Cache Fabric, Qwen runtime adapters, verifier contracts, Visual HashHop proof generation, BF16 LoRA fine-tuning, adapter cartridge binding, and a local dashboard. + +## Local Run + +```powershell +Set-Location C:\Users\zacharyzhang\Documents\Github\ELY-EYE +.\scripts\install.ps1 +.\scripts\setup-linux-runtimes.ps1 +.\scripts\serve-backend.ps1 +.\scripts\serve-frontend.ps1 +``` + +Backend: `http://127.0.0.1:8765` +Frontend: `http://127.0.0.1:5173` + +## Model Runtime + +SGLang is supported as an OpenAI-compatible runtime target. The Windows local runtime uses Transformers. + +```powershell +.\scripts\download-models.ps1 +.\scripts\launch-sglang-live.ps1 +``` + +The Transformers backend is available by setting: + +```powershell +$env:ELY_EYE_RUNTIME_BACKEND = "transformers" +``` + +When `ELY_EYE_ADAPTER_DIR` is empty, the Transformers runtime auto-selects the +latest trained `HME-Core-LoRA` adapter whose base model matches +`Qwen/Qwen3.5-9B`. `ely-eye status` reports the resolved adapter path, kind, +adapter id, and adapter hash. + +The runtime generation proof loads `Qwen/Qwen3.5-9B`, injects the trained +`HME-Core-LoRA` adapter, generates a PRD-cited answer, and writes the raw model +output plus verifier report: + +```powershell +.\.venv\Scripts\ely-eye.exe runtime-generation-proof +``` + +## Fine-Tuning + +The PRD-derived datasets are built from real `PRD.md` sections, the ingested PRD Evidence Atom, and local UI proof images. Training runs real BF16 LoRA optimizer updates on RTX 4090 through Unsloth-patched Transformers, PEFT, and bitsandbytes, then writes PEFT adapters, `adapter_manifest.json`, `training_trace.jsonl`, `training_summary.json`, `training_proof.json`, dataset hashes, adapter hashes, trainable parameter hashes, optimizer update norms, per-step loss, CUDA/BF16 environment metadata, target modules, adapter weight SHA-256, LoRA tensor statistics, and cartridge binding metadata. The training path requires CUDA BF16 and bitsandbytes `PagedAdamW8bit`; every proof records proof version, precision, optimizer family, checkpointing mode, max length, framework, and target-module coverage. + +```powershell +.\scripts\run-prd-finetune.ps1 -MaxSteps 37 -CartridgeId ely_eye_prd_7e5cd27b8284 +.\scripts\run-all-prd-finetunes.ps1 -CartridgeId ely_eye_prd_7e5cd27b8284 +$env:ELY_EYE_ADAPTER_DIR = ".ely_eye\adapters\hme_core_lora_qwen35_9b" +.\.venv\Scripts\ely-eye.exe status +``` + +The PRD adapter set is: + +```text +HME-Core-LoRA Qwen3.5-9B attention/MLP +HME-Vision-LoRA Qwen3.5-9B visual projector and selected vision layers +HME-TTT-VL Qwen3.5-9B mutable MLP and visual adapter +HME-Visual-MTP Qwen3.5-9B draft-head LoRA +HME-Router Qwen3.5-0.8B context planner +HME-Retrieval Qwen3-VL-Embedding-8B contrastive retrieval adapter +``` + +## Core Commands + +```powershell +.\.venv\Scripts\ely-eye.exe status +.\.venv\Scripts\ely-eye.exe ingest C:\path\to\package --cartridge-name project-cartridge +.\.venv\Scripts\ely-eye.exe hashhop --kind visual --hops 2 --token-equivalent 262144 +.\.venv\Scripts\ely-eye.exe validate-training-data C:\path\to\train.jsonl +.\.venv\Scripts\ely-eye.exe train-lora C:\path\to\train.jsonl C:\path\to\adapter +``` + +Adapter proof state is available through `GET /api/adapters` and the dashboard Adapter Matrix, including training method, precision, optimizer family, checkpointing state, optimizer update counts, and loss delta. + +## PRD Proof Suite + +The proof suite verifies the PRD adapter set with CUDA BF16 optimizer updates, +changed trainable parameter hashes, matching manifest/summary/proof hashes, +proof-versioned BF16 LoRA contracts, 8192-token max length, paged 8-bit AdamW, +checkpointing, target-module coverage, nonzero LoRA safetensors, runtime adapter binding, runtime adapter generation, +cartridge replay, Visual HashHop assets, model-solved text HashHop, model-solved +Visual HashHop 1-hop and 2-hop pass-rate gates, learned memory adapter binding, +100M memory capsule commitments, Memory DNA, cache trace evidence, and Visual +Contradiction Lens cross-version drift detection (>=80% recall with zero false +positives across the PRD 11.15 taxonomy: design-token, layout, typography, copy, +visual-code, and temporal drift). +It also verifies code repository input coverage through a local structure index, +call graph extraction, and git issue/PR history association. +It writes proof JSON, generated HashHop visual blocks, raw model HashHop +outputs, Visual HashHop arena images, and official benchmark sample predictions under `.ely_eye/data/eval_proofs/`, then +attaches the suite to the active Context Cartridge under `eval_proofs/`. +MMMU-Pro and OCRBench sample predictions must include finite local scores before +the standard benchmark check passes. Every standard benchmark sample must include +a sample SHA-256 and source dataset SHA. LongVideoBench uses the official +`longvideobench/LongVideoBench-Meta` public validation metadata source while the +canonical gated dataset remains tracked in the benchmark registry. + +Latest local PRD proof suite: `prd_proof_suite_6bc3fa744c60a0e6` (15 checks passed). + +```powershell +.\.venv\Scripts\ely-eye.exe proof-suite --cartridge-id ely_eye_prd_7e5cd27b8284 +.\.venv\Scripts\ely-eye.exe latest-proof-suite +.\.venv\Scripts\ely-eye.exe runtime-generation-proof +.\.venv\Scripts\ely-eye.exe visual-contradiction-proof +.\.venv\Scripts\ely-eye.exe refresh-cartridge ely_eye_prd_7e5cd27b8284 +.\.venv\Scripts\ely-eye.exe finalize-cartridge ely_eye_prd_7e5cd27b8284 +.\.venv\Scripts\ely-eye.exe benchmark-registry +.\.venv\Scripts\ely-eye.exe benchmark-sources +.\.venv\Scripts\ely-eye.exe benchmark-samples +.\.venv\Scripts\ely-eye.exe benchmark-predictions +.\.venv\Scripts\ely-eye.exe runtime-profiles +``` + +The same surface is available through `POST /api/proof-suite`, `GET /api/proof-suite/latest`, `POST /api/visual-contradiction-proof`, `POST /api/cartridges/{cartridge_id}/refresh`, `POST /api/cartridges/{cartridge_id}/finalize`, `GET /api/cartridges/{cartridge_id}/assets`, `GET /api/benchmarks/registry`, `GET /api/benchmarks/sources`, `GET /api/benchmarks/samples`, `GET /api/benchmarks/predictions`, `GET /api/runtime/profiles`, and the dashboard PRD Proof Suite panel. + +Runtime launchers: +`ely-eye runtime-profiles` validates the WSL GPU, Linux runtime packages, +SGLang/vLLM launcher modules, KTransformers import path, and llama.cpp help +output before the proof suite marks runtime delivery ready. + +```powershell +.\scripts\setup-linux-runtimes.ps1 +.\scripts\launch-sglang-live.ps1 +.\scripts\launch-sglang-extreme.ps1 +.\scripts\launch-vllm-live.ps1 +.\scripts\launch-vllm-extreme.ps1 +.\scripts\launch-ktransformers-extreme.ps1 +.\scripts\launch-llamacpp-offline.ps1 +``` + +## Data Layout + +Runtime state is stored under `.ely_eye/` by default. + +```text +.ely_eye/ +├─ adapters/ +├─ data/ely_eye.sqlite +├─ training/ +├─ objects/ +├─ cache/ +└─ cartridges/ +``` + +Context Cartridge artifacts are materialized as `manifest.json`, `atoms.parquet`, `sources.json`, `sparse_index.bm25.zst`, `temporal_graph.sqlite`, `text_vectors.fp16.zstd`, `visual_vectors.int8.zstd`, `kv_snapshots.kivi`, `memory_capsule.json.zst`, `memory_capsule_index.json`, `ttt_vl_adapter.safetensors`, `visual_mtp_head.safetensors`, `eval_proofs/`, and attached adapter folders. diff --git a/TRAINING_ARTIFACTS.md b/TRAINING_ARTIFACTS.md new file mode 100644 index 0000000..2c9fee9 --- /dev/null +++ b/TRAINING_ARTIFACTS.md @@ -0,0 +1,144 @@ +# Ely-Eye Training Artifacts + +Generated on 2026-05-28 from the local `PRD.md`, ingested Evidence Atom +`PRD_0b88b12114634a56:text`, visual Evidence Atom +`ely_eye_dashboard_0180927ffdd3f8b0:image`, local UI proof images, and Qwen +model snapshots. Every adapter has `training_trace.jsonl`, `training_summary.json`, +and `training_proof.json` with RTX 4090 CUDA BF16 execution metadata, proof +version 2, BF16 LoRA method, 8192-token max length, paged 8-bit AdamW, +Unsloth-patched checkpointing mode, per-step loss, trainable parameter hashes +before and after training, optimizer update norms, dataset hashes, environment +versions, adapter weight SHA-256, LoRA tensor statistics, runtime adapter binding +proof, runtime adapter generation proof, and proof-suite verified nonzero LoRA +safetensors. + +| Adapter | Base model | Samples | Steps | Updates | Max length | Trainable params | Final loss | Loss delta | Max update L2 | Seconds | SHA-256 | +|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---| +| HME-Core-LoRA | Qwen/Qwen3.5-9B | 37 | 37 | 10 | 8,192 | 29,097,984 | 0.0001596613001311198 | 0.22773139642958995 | 0.8183563251107077 | 104.4 | 8f97890e802f9adfa3bcb2c02b33d7b39dd8954654d7057d4058e1331e7b5df4 | +| HME-Vision-LoRA | Qwen/Qwen3.5-9B | 10 | 20 | 5 | 8,192 | 1,996,672 | 2.0484652519226074 | -0.11261439323425293 | 0.6595521616294707 | 49.3 | 593f7295f466c5fba07bdae26d1d8798c5affbad8f3863cda9f1881d06453dec | +| HME-TTT-VL | Qwen/Qwen3.5-9B | 10 | 20 | 5 | 8,192 | 8,288,128 | 1.5277884006500244 | 0.4080624580383301 | 1.2924649382409095 | 51.2 | 030b37480155a8eaa6ef4c2dfa819d5f2564838935654f0d826235eb66cc9de5 | +| HME-Visual-MTP | Qwen/Qwen3.5-9B | 10 | 20 | 5 | 8,192 | 2,019,328 | 2.066615343093872 | -0.13076448440551758 | 0.1419292917069751 | 75.8 | 489da9460f51693fa8775ad4975a18f7400dd3fc41eac401d00305617c627717 | +| HME-Router | Qwen/Qwen3.5-0.8B | 8 | 16 | 4 | 8,192 | 3,194,880 | 1.2781919240951538 | 1.0438536405563354 | 0.2871291982199057 | 28.8 | 9ef884a1078df71ba73376a29737a27e22e2ff25854352bfc2c7d960102d886e | +| HME-Retrieval | Qwen/Qwen3-VL-Embedding-8B | 32 | 32 | 32 | 8,192 | 21,823,488 | 0.5665192604064941 | 0.91921067237854 | 1.3181151117874825 | 55.3 | 6490f03e445eb70da7020be992c6eccd3ca2b0e4c48e73a80a819ddac5faa8f3 | + +Adapter weight proof: + +| Adapter | LoRA tensors | Nonzero elements | Weight SHA-256 | +|---|---:|---:|---| +| HME-Core-LoRA | 256 | 29,097,984 | 4673ac90abdd8ef952dad9c2292aecadba161669c3b1fc3d1af5c075cf0ae62d | +| HME-Vision-LoRA | 220 | 1,992,340 | dde2eb9051866634f9173366a6ffa3fbf9219e5b6d640eafb7f99d0258c52422 | +| HME-TTT-VL | 412 | 8,283,800 | 9a572ac51c6df08e38898c6c30acd8ed6530c7e852611552baaec6a67862e6c5 | +| HME-Visual-MTP | 4 | 1,028,316 | 58d058b862a542c042d27234b5b22344ba02ee6e016be6c8838b329ffea1c6f9 | +| HME-Router | 192 | 3,194,880 | 90079b8f2c244efb00c2285816b98f51ff03cf06a9ba2335ee5bbdf24a1aff8f | +| HME-Retrieval | 504 | 21,823,488 | bcb1381888ae84e82ab18256e152191e56a0892c46cdac81874a9c1fb1af14ee | + +Local adapter roots: + +```text +.ely_eye/adapters/hme_core_lora_qwen35_9b +.ely_eye/adapters/hme_vision_lora_qwen35_9b +.ely_eye/adapters/hme_ttt_vl_qwen35_9b +.ely_eye/adapters/hme_visual_mtp_qwen35_9b +.ely_eye/adapters/hme_router_qwen35_08b +.ely_eye/adapters/hme_retrieval_qwen3_vl_embedding_8b +``` + +Cartridge binding: + +```text +.ely_eye/cartridges/ely_eye_prd_7e5cd27b8284/adapters/hme_core_lora +.ely_eye/cartridges/ely_eye_prd_7e5cd27b8284/adapters/hme_vision_lora +.ely_eye/cartridges/ely_eye_prd_7e5cd27b8284/adapters/hme_ttt_vl +.ely_eye/cartridges/ely_eye_prd_7e5cd27b8284/adapters/hme_visual_mtp +.ely_eye/cartridges/ely_eye_prd_7e5cd27b8284/adapters/hme_router +.ely_eye/cartridges/ely_eye_prd_7e5cd27b8284/adapters/hme_retrieval +``` + +PRD proof suite: + +```text +.ely_eye/data/eval_proofs/prd_proof_suite_b03b88d7cfe5ba96/proof_suite.json +.ely_eye/cartridges/ely_eye_prd_7e5cd27b8284/eval_proofs/prd_proof_suite_b03b88d7cfe5ba96/proof_suite.json +``` + +Runtime generation proof: + +```text +.ely_eye/data/eval_proofs/prd_proof_suite_b03b88d7cfe5ba96/runtime_generation.json +transformers generated 218 tokens with hme_core_lora citation accuracy 1.00 +``` + +100M memory capsule proof: + +```text +.ely_eye/cartridges/ely_eye_prd_7e5cd27b8284/memory_capsule.json.zst +.ely_eye/cartridges/ely_eye_prd_7e5cd27b8284/memory_capsule_index.json +.ely_eye/data/eval_proofs/prd_proof_suite_b03b88d7cfe5ba96/memory_capsule.json +The Context Cartridge materialized a 100,000,000 token-equivalent memory capsule with 4,096 committed segments and a verified segment Merkle root. +``` + +Model-solved HashHop proofs: + +```text +.ely_eye/data/eval_proofs/prd_proof_suite_b03b88d7cfe5ba96/hashhop/0a10e6394ada0da7/model_text_hashhop.json +.ely_eye/data/eval_proofs/prd_proof_suite_b03b88d7cfe5ba96/hashhop/60c9d5f5f1648ea0/model_visual_hashhop.json +.ely_eye/data/eval_proofs/prd_proof_suite_b03b88d7cfe5ba96/hashhop/a29a310b31f9cef5/model_visual_hashhop.json +.ely_eye/data/eval_proofs/prd_proof_suite_b03b88d7cfe5ba96/hashhop/7f7b8b1f54ec3d75/model_visual_hashhop.json +.ely_eye/data/eval_proofs/prd_proof_suite_b03b88d7cfe5ba96/hashhop/4f24efd13342694c/model_visual_hashhop.json +.ely_eye/data/eval_proofs/prd_proof_suite_b03b88d7cfe5ba96/hashhop/a3f49402ef2710a3/model_visual_hashhop.json +.ely_eye/data/eval_proofs/prd_proof_suite_b03b88d7cfe5ba96/hashhop/0bd17526ab51392b/model_visual_hashhop.json +Qwen/Qwen3.5-9B with HME-Core-LoRA solved a 1.01M token-equivalent 2-hop text HashHop and 6 Visual HashHop arenas at 262K token-equivalent. The Visual HashHop gate passed 7/7 model-solved proofs, with 262K 1-hop=1.00 and 2-hop=1.00. +``` + +Benchmark source and sample proof: + +```text +.ely_eye/data/eval_proofs/prd_proof_suite_b03b88d7cfe5ba96/standard_benchmark_sources.json +.ely_eye/data/eval_proofs/prd_proof_suite_b03b88d7cfe5ba96/standard_benchmark_samples.json +.ely_eye/data/eval_proofs/prd_proof_suite_b03b88d7cfe5ba96/standard_benchmark_predictions.json +MMMU-Pro, OCRBench, MMLongBench-Doc, VideoMME, LongVideoBench, RULER, and LongBench v2 loaded official streaming samples or official public metadata samples with sample hashes and source dataset SHA values. LongVideoBench uses `longvideobench/LongVideoBench-Meta` validation metadata with canonical `longvideobench/LongVideoBench` registry tracking. MMMU-Pro and OCRBench generated local Qwen/Qwen3.5-9B + HME-Core-LoRA predictions from official image samples, with local sample scores MMMU-Pro=1.00 and OCRBench=1.00. +``` + +Repository input proof: + +```text +.ely_eye/data/eval_proofs/prd_proof_suite_b03b88d7cfe5ba96/repository_input.json +The repository input proof indexed 94 files, 49 code files, 11,977 call graph edges, and 26 local git commits for PRD 10.1 code repository coverage. +``` + +Runtime profile readiness: + +```text +.ely_eye/data/eval_proofs/prd_proof_suite_b03b88d7cfe5ba96/runtime_profiles.json +SGLang 0.5.12.post1, vLLM 0.21.0, KTransformers 0.6.2.post4, WSL RTX 4090 GPU visibility, launcher module probes, and winget llama-server help output are verified. +``` + +Checks: + +```text +Adapter Training Proof passed # proof-versioned BF16 LoRA + paged 8-bit AdamW + nonzero LoRA safetensors +Runtime Adapter Binding passed +Runtime Adapter Generation passed +Cartridge Physical Assets passed +100M Memory Capsule passed +Context Cartridge Replay passed +Visual HashHop passed # 7/7 model-solved proofs +Standard Benchmark Registry passed # MMMU-Pro=1.00, OCRBench=1.00 local samples +Repository Input Coverage passed # 94 files + 11977 call graph edges + local git history +Runtime Profile Delivery passed +Research Module Delivery passed # executable module gates, evidence graph edge, probability sum +Learned Memory Recall passed +Memory DNA passed +Cache Trace passed +``` + +Cartridge physical assets: + +```text +.ely_eye/cartridges/ely_eye_prd_7e5cd27b8284/text_vectors.fp16.zstd +.ely_eye/cartridges/ely_eye_prd_7e5cd27b8284/visual_vectors.int8.zstd # item_count=1 +.ely_eye/cartridges/ely_eye_prd_7e5cd27b8284/kv_snapshots.kivi +.ely_eye/cartridges/ely_eye_prd_7e5cd27b8284/memory_capsule.json.zst # token_equivalent=100000000, segment_count=4096 +.ely_eye/cartridges/ely_eye_prd_7e5cd27b8284/ttt_vl_adapter.safetensors +.ely_eye/cartridges/ely_eye_prd_7e5cd27b8284/visual_mtp_head.safetensors +``` diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..4378ba6 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,3 @@ +# Ely-Eye Backend + +FastAPI, ingestion, memory, retrieval, runtime, verifier, evaluation, and training services for Ely-Eye. diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100644 index 0000000..9f6af92 --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,56 @@ +[build-system] +requires = ["setuptools>=80", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "ely-eye" +version = "0.1.0" +description = "Local multimodal context operating system for Qwen3.5-9B." +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "accelerate>=1.12.0", + "bitsandbytes>=0.49.2", + "datasets>=4.8.5", + "fastapi>=0.124.0", + "huggingface-hub>=1.2.0", + "httpx>=0.28.0", + "networkx>=3.6.0", + "ninja>=1.13.0", + "numpy>=2.3.0", + "opencv-python-headless>=4.12.0.0", + "orjson>=3.11.0", + "peft>=0.19.0", + "pillow>=12.0.0", + "platformdirs>=4.5.0", + "pyarrow>=22.0.0", + "pydantic>=2.12.0", + "pydantic-settings>=2.12.0", + "pymupdf>=1.26.0", + "python-multipart>=0.0.20", + "qwen-vl-utils>=0.0.14", + "rank-bm25>=0.2.2", + "rapidocr-onnxruntime>=1.4.4", + "safetensors>=0.7.0", + "sentence-transformers>=5.2.0", + "torch>=2.12.0", + "torchvision>=0.27.0", + "transformers>=4.57.0", + "typer>=0.20.0", + "uvicorn[standard]>=0.38.0", + "zstandard>=0.25.0" +] + +[project.scripts] +ely-eye = "ely_eye.cli:app" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.ruff] +line-length = 100 +target-version = "py312" + +[tool.pyright] +pythonVersion = "3.12" +include = ["src"] diff --git a/backend/requirements-ktransformers-linux.txt b/backend/requirements-ktransformers-linux.txt new file mode 100644 index 0000000..6df3aec --- /dev/null +++ b/backend/requirements-ktransformers-linux.txt @@ -0,0 +1,5 @@ +ktransformers>=0.3.0 +sglang-kt>=0.6.2 +torchvision==0.24.1 +transformers>=4.57.0 +qwen-vl-utils>=0.0.14 diff --git a/backend/requirements-sglang-linux.txt b/backend/requirements-sglang-linux.txt new file mode 100644 index 0000000..fbde753 --- /dev/null +++ b/backend/requirements-sglang-linux.txt @@ -0,0 +1 @@ +sglang>=0.5.10 diff --git a/backend/requirements-vllm-linux.txt b/backend/requirements-vllm-linux.txt new file mode 100644 index 0000000..0019f6f --- /dev/null +++ b/backend/requirements-vllm-linux.txt @@ -0,0 +1,3 @@ +vllm>=0.12.0 +transformers>=4.57.0 +qwen-vl-utils>=0.0.14 diff --git a/backend/src/ely_eye/__init__.py b/backend/src/ely_eye/__init__.py new file mode 100644 index 0000000..85d2163 --- /dev/null +++ b/backend/src/ely_eye/__init__.py @@ -0,0 +1,4 @@ +"""Ely-Eye production package.""" + +__all__ = ["__version__"] +__version__ = "0.1.0" diff --git a/backend/src/ely_eye/api.py b/backend/src/ely_eye/api.py new file mode 100644 index 0000000..f760c33 --- /dev/null +++ b/backend/src/ely_eye/api.py @@ -0,0 +1,398 @@ +from __future__ import annotations + +import json +import shutil +from pathlib import Path +from typing import Annotated + +from fastapi import Depends, FastAPI, File, HTTPException, UploadFile +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel + +from . import __version__ +from .benchmarks import ( + benchmark_prediction_report, + benchmark_registry_report, + benchmark_sample_report, + benchmark_source_report, +) +from .cache_fabric import CacheFabric +from .cartridge import CartridgeService +from .cartridge_assets import CartridgeAssetService +from .compiler import ContextCompiler +from .config import Settings, get_settings +from .db import Database +from .embeddings import EmbeddingService +from .evals import HashHopEvaluator, PRDProofSuite +from .evidence_graph import EvidenceGraphService +from .ingestion import IngestionService +from .profiles import runtime_profile_report +from .research_modules import ResearchModuleService +from .runtime import QwenRuntime, RuntimeUnavailableError +from .schemas import ( + AdapterManifest, + AdapterStatus, + AnswerResponse, + BenchmarkRegistryReport, + BenchmarkPredictionReport, + BenchmarkSampleReport, + BenchmarkSourceReport, + CartridgeAssetReport, + HashHopProof, + MemoryStatus, + PathIngestRequest, + ProofCheck, + ProofSuiteReport, + RuntimeProfile, + RuntimeProfileReport, + SystemStatus, + UploadIngestRequest, +) +from .training import safetensor_weight_stats +from .verifier import Verifier, VisualContradictionLens + + +class ChatRequest(BaseModel): + question: str + profile: RuntimeProfile = RuntimeProfile.library_100m + + +class HashHopRequest(BaseModel): + kind: str = "visual" + hops: int = 2 + token_equivalent: int = 262_144 + + +class ProofSuiteRequest(BaseModel): + cartridge_id: str | None = None + + +def settings_dep() -> Settings: + return get_settings() + + +def create_app() -> FastAPI: + app = FastAPI(title="Ely-Eye", version=__version__) + app.add_middleware( + CORSMiddleware, + allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + @app.get("/api/status", response_model=SystemStatus) + def status(settings: Annotated[Settings, Depends(settings_dep)]) -> SystemStatus: + db = Database(settings) + cache = CacheFabric(settings, db) + runtime = QwenRuntime(settings).status() + layers = cache.statuses() + active_token_equivalent = db.token_equivalent_sum() + memory = MemoryStatus( + active_atoms=db.atom_count(), + active_token_equivalent=active_token_equivalent, + cartridges=db.cartridge_count(), + library_token_equivalent=max(active_token_equivalent, db.cartridge_token_equivalent_sum()), + layers=layers, + ) + return SystemStatus( + version=__version__, + runtime=runtime, + memory=memory, + data_home=str(settings.home), + profiles={ + "Live Demo": {"context_tokens": settings.live_context_tokens, "purpose": "stable 262K workflow"}, + "Extreme Context": {"context_tokens": settings.extreme_context_tokens, "purpose": "YaRN 1.01M workflow"}, + "100M Library": {"context_tokens": settings.library_target_tokens, "purpose": "Context Cartridge memory"}, + "Research Theater": {"context_tokens": settings.live_context_tokens, "purpose": "V-NSA, DVKV, TTT-VL proofs"}, + }, + ) + + @app.post("/api/ingest/path") + def ingest_path( + request: PathIngestRequest, + settings: Annotated[Settings, Depends(settings_dep)], + ): + try: + return IngestionService(settings).ingest_path(request.path, request.cartridge_name) + except Exception as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.post("/api/ingest/upload") + def ingest_upload( + metadata: Annotated[UploadIngestRequest, Depends()], + settings: Annotated[Settings, Depends(settings_dep)], + files: Annotated[list[UploadFile], File()], + ): + upload_root = settings.data_dir / "uploads" + upload_root.mkdir(parents=True, exist_ok=True) + saved_paths: list[Path] = [] + total_size = 0 + for uploaded in files: + target = upload_root / sanitize_filename(uploaded.filename or "upload.bin") + with target.open("wb") as handle: + shutil.copyfileobj(uploaded.file, handle) + total_size += target.stat().st_size + if total_size > settings.upload_max_bytes: + raise HTTPException(status_code=413, detail="Upload exceeds configured maximum size.") + saved_paths.append(target) + + service = IngestionService(settings) + aggregate = None + for path in saved_paths: + result = service.ingest_path(path, None) + if aggregate is None: + aggregate = result + else: + aggregate.source_count += result.source_count + aggregate.atom_count += result.atom_count + aggregate.token_equivalent += result.token_equivalent + aggregate.sources.extend(result.sources) + if aggregate is None: + raise HTTPException(status_code=400, detail="No files were uploaded.") + if metadata.cartridge_name: + atoms = Database(settings).list_atoms() + sources = aggregate.sources + cartridge = CartridgeService(settings).materialize(metadata.cartridge_name, atoms, sources) + aggregate.cartridge_id = cartridge.cartridge_id + return aggregate + + @app.post("/api/chat", response_model=AnswerResponse) + def chat(request: ChatRequest, settings: Annotated[Settings, Depends(settings_dep)]) -> AnswerResponse: + db = Database(settings) + compiler = ContextCompiler(settings, db) + context = compiler.compile(request.question, request.profile) + if not context.hits: + raise HTTPException(status_code=404, detail="No evidence atoms are available for this question.") + runtime = QwenRuntime(settings) + try: + generation = runtime.generate(request.question, context) + except RuntimeUnavailableError as exc: + raise HTTPException(status_code=503, detail=str(exc)) from exc + answer, citations, report = Verifier().verify_answer(generation.text, context) + contradiction_notes = VisualContradictionLens().detect(context) + report.contradiction_notes.extend(note for note in contradiction_notes if note not in report.contradiction_notes) + EvidenceGraphService(settings, db).record_answer(request.question, answer, report, context) + return AnswerResponse( + answer=answer, + citations=citations, + context=context, + verifier=report, + runtime_backend=generation.backend, + ) + + @app.get("/api/cartridges") + def cartridges(settings: Annotated[Settings, Depends(settings_dep)]): + return CartridgeService(settings).list_manifests() + + @app.post("/api/cartridges/{cartridge_id}/finalize", response_model=CartridgeAssetReport) + def finalize_cartridge( + cartridge_id: str, + settings: Annotated[Settings, Depends(settings_dep)], + ) -> CartridgeAssetReport: + try: + return CartridgeAssetService(settings).finalize(cartridge_id) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.post("/api/cartridges/{cartridge_id}/refresh") + def refresh_cartridge( + cartridge_id: str, + settings: Annotated[Settings, Depends(settings_dep)], + ): + db = Database(settings) + try: + return CartridgeService(settings, db).rebuild( + cartridge_id=cartridge_id, + atoms=db.list_atoms(), + sources=db.list_sources(), + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.get("/api/cartridges/{cartridge_id}/assets", response_model=CartridgeAssetReport) + def cartridge_assets( + cartridge_id: str, + settings: Annotated[Settings, Depends(settings_dep)], + ) -> CartridgeAssetReport: + try: + report = CartridgeAssetService(settings).status(cartridge_id) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + if report is None: + raise HTTPException(status_code=404, detail="Cartridge assets have not been finalized.") + return report + + @app.get("/api/adapters", response_model=list[AdapterStatus]) + def adapters(settings: Annotated[Settings, Depends(settings_dep)]) -> list[AdapterStatus]: + return list_adapter_statuses(settings) + + @app.get("/api/benchmarks/registry", response_model=BenchmarkRegistryReport) + def benchmarks_registry() -> BenchmarkRegistryReport: + return benchmark_registry_report() + + @app.get("/api/benchmarks/sources", response_model=BenchmarkSourceReport) + def benchmarks_sources() -> BenchmarkSourceReport: + return benchmark_source_report() + + @app.get("/api/benchmarks/samples", response_model=BenchmarkSampleReport) + def benchmarks_samples(settings: Annotated[Settings, Depends(settings_dep)]) -> BenchmarkSampleReport: + return benchmark_sample_report(settings.data_dir / "benchmark_samples") + + @app.get("/api/benchmarks/predictions", response_model=BenchmarkPredictionReport) + def benchmarks_predictions(settings: Annotated[Settings, Depends(settings_dep)]) -> BenchmarkPredictionReport: + return benchmark_prediction_report(settings, settings.data_dir / "benchmark_predictions") + + @app.get("/api/runtime/profiles", response_model=RuntimeProfileReport) + def runtime_profiles() -> RuntimeProfileReport: + return runtime_profile_report() + + @app.get("/api/research/modules") + def research_modules(settings: Annotated[Settings, Depends(settings_dep)]): + db = Database(settings) + compiler = ContextCompiler(settings, db) + context = compiler.compile( + "Ely-Eye V-NSA DVKV x-modal KV speculative prefetch evidence graph", + RuntimeProfile.research_theater, + ) + return ResearchModuleService(settings, db).build_report(context.plan.question, context.hits) + + @app.post("/api/index/dense") + def build_dense_index(settings: Annotated[Settings, Depends(settings_dep)]): + service = EmbeddingService(settings) + index = service.build_index() + return {"atoms": len(index.atom_ids), "dimensions": int(index.vectors.shape[1]) if index.vectors.size else 0} + + @app.post("/api/hashhop", response_model=HashHopProof) + def hashhop(request: HashHopRequest, settings: Annotated[Settings, Depends(settings_dep)]) -> HashHopProof: + evaluator = HashHopEvaluator(settings) + if request.kind == "text": + return evaluator.generate_text_proof(request.hops, request.token_equivalent) + if request.kind == "visual": + return evaluator.generate_visual_proof(request.hops, request.token_equivalent) + raise HTTPException(status_code=400, detail="kind must be text or visual") + + @app.post("/api/proof-suite", response_model=ProofSuiteReport) + def proof_suite( + request: ProofSuiteRequest, + settings: Annotated[Settings, Depends(settings_dep)], + ) -> ProofSuiteReport: + try: + return PRDProofSuite(settings).run(request.cartridge_id) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.get("/api/proof-suite/latest", response_model=ProofSuiteReport) + def latest_proof_suite(settings: Annotated[Settings, Depends(settings_dep)]) -> ProofSuiteReport: + report = PRDProofSuite(settings).latest() + if report is None: + raise HTTPException(status_code=404, detail="No PRD proof suite has been generated.") + return report + + @app.post("/api/visual-contradiction-proof", response_model=ProofCheck) + def visual_contradiction_proof(settings: Annotated[Settings, Depends(settings_dep)]) -> ProofCheck: + return PRDProofSuite(settings).visual_contradiction_proof() + + @app.get("/api/memory-map") + def memory_map(settings: Annotated[Settings, Depends(settings_dep)]): + db = Database(settings) + atoms = db.list_atoms(limit=200) + return { + "atoms": [atom.model_dump(mode="json") for atom in atoms], + "layers": CacheFabric(settings, db).statuses(), + "cartridges": CartridgeService(settings, db).list_manifests(), + } + + return app + + +def sanitize_filename(name: str) -> str: + safe = "".join(char if char.isalnum() or char in {".", "-", "_"} else "_" for char in name) + return safe.strip("._") or "upload.bin" + + +def list_adapter_statuses(settings: Settings) -> list[AdapterStatus]: + cartridge_bindings = { + manifest.cartridge_id: manifest.artifacts + for manifest in CartridgeService(settings).list_manifests() + } + statuses: list[AdapterStatus] = [] + for manifest_path in sorted(settings.adapters_dir.glob("*/adapter_manifest.json")): + root = manifest_path.parent + manifest = AdapterManifest.model_validate_json(manifest_path.read_text(encoding="utf-8")) + weights = root / "adapter_model.safetensors" + weight_bytes = weights.stat().st_size if weights.exists() else 0 + weight_stats = safetensor_weight_stats(weights) + artifacts = cartridge_bindings.get(manifest.cartridge_id or "", {}) + cartridge_bound = f"adapter_{manifest.kind.value}" in artifacts + statuses.append( + AdapterStatus( + name=root.name, + kind=manifest.kind, + proof_version=manifest.proof_version, + training_method=manifest.training_method, + precision=manifest.precision, + framework=manifest.framework, + base_model=manifest.base_model, + path=str(root), + adapter_id=manifest.adapter_id, + sample_count=manifest.sample_count, + max_steps=manifest.max_steps, + trainable_params=manifest.trainable_params, + final_loss=manifest.final_loss, + sha256=manifest.sha256, + weight_bytes=weight_bytes, + safetensor_keys=int(weight_stats["tensor_count"]), + cartridge_id=manifest.cartridge_id, + cartridge_bound=cartridge_bound, + training_trace_path=manifest.training_trace_path, + training_trace_sha256=manifest.training_trace_sha256, + training_trace_steps=count_training_steps(root / "training_trace.jsonl"), + training_proof_path=manifest.training_proof_path, + training_proof_sha256=manifest.training_proof_sha256, + optimizer=manifest.optimizer, + optimizer_family=manifest.optimizer_family, + gradient_checkpointing=manifest.gradient_checkpointing, + gradient_checkpointing_mode=manifest.gradient_checkpointing_mode, + autocast_dtype=manifest.autocast_dtype, + unsloth_version=manifest.unsloth_version, + triton_version=manifest.triton_version, + xformers_version=manifest.xformers_version, + unsloth_model_type=manifest.unsloth_model_type, + optimizer_update_count=manifest.optimizer_update_count, + max_optimizer_update_l2=manifest.max_optimizer_update_l2, + loss_delta=manifest.loss_delta, + training_summary_path=manifest.training_summary_path, + training_wall_seconds=manifest.training_wall_seconds, + training_device=manifest.training_device, + bf16=manifest.bf16, + torch_version=manifest.torch_version, + cuda_version=manifest.cuda_version, + adapter_weight_sha256=str(weight_stats["sha256"] or manifest.adapter_weight_sha256 or ""), + adapter_tensor_count=int(weight_stats["tensor_count"]), + adapter_lora_tensor_count=int(weight_stats["lora_tensor_count"]), + adapter_total_elements=int(weight_stats["total_elements"]), + adapter_nonzero_elements=int(weight_stats["nonzero_elements"]), + adapter_max_abs=weight_stats["max_abs"], + adapter_weights_finite=bool(weight_stats["all_finite"]), + ) + ) + return statuses + + +def count_training_steps(path: Path) -> int: + if not path.exists(): + return 0 + steps = 0 + for line in path.read_text(encoding="utf-8").splitlines(): + if not line.strip(): + continue + try: + if json.loads(line).get("event") == "step": + steps += 1 + except json.JSONDecodeError: + return 0 + return steps + + +app = create_app() diff --git a/backend/src/ely_eye/benchmarks.py b/backend/src/ely_eye/benchmarks.py new file mode 100644 index 0000000..e219754 --- /dev/null +++ b/backend/src/ely_eye/benchmarks.py @@ -0,0 +1,617 @@ +from __future__ import annotations + +import json +import math +import re +import tempfile +from ast import literal_eval +from pathlib import Path +from typing import Any + +from .schemas import ( + BenchmarkPredictionReport, + BenchmarkPredictionStatus, + BenchmarkRegistryReport, + BenchmarkSampleReport, + BenchmarkSampleStatus, + BenchmarkSourceReport, + BenchmarkSourceStatus, + BenchmarkSpec, +) + + +STANDARD_BENCHMARKS = ( + BenchmarkSpec( + name="MMMU-Pro", + target="Multimodal reasoning within Qwen3.5-9B baseline +/-2%.", + dataset="MMMU/MMMU_Pro", + task_type="image_text_multiple_choice", + runner="ely-eye run-benchmark --benchmark mmmu-pro --dataset-path ", + metric="accuracy", + ), + BenchmarkSpec( + name="OCRBench", + target="OCR and document understanding at or above base model behavior.", + dataset="echo840/OCRBench", + task_type="image_text_qa", + runner="ely-eye run-benchmark --benchmark ocrbench --dataset-path ", + metric="exact_or_contains_accuracy", + ), + BenchmarkSpec( + name="MMLongBench-Doc", + target="Long document QA above a RAG-only baseline.", + dataset="VLM2Vec/MMLongBench-doc", + task_type="long_document_qa", + runner="ely-eye run-benchmark --benchmark mmlongbench-doc --dataset-path ", + metric="answer_f1_with_evidence", + ), + BenchmarkSpec( + name="VideoMME", + target="Long video understanding with sampled visual timeline evidence.", + dataset="lmms-lab/Video-MME", + task_type="video_multiple_choice", + runner="ely-eye run-benchmark --benchmark videomme --dataset-path ", + metric="accuracy", + ), + BenchmarkSpec( + name="LongVideoBench", + target="Long-context interleaved video-language understanding.", + dataset="longvideobench/LongVideoBench", + task_type="video_qa", + runner="ely-eye run-benchmark --benchmark longvideobench --dataset-path ", + metric="accuracy", + ), + BenchmarkSpec( + name="RULER", + target="Long-context retrieval, aggregation, and multi-hop reasoning.", + dataset="RULER-dataset/RULER", + task_type="long_context_text", + runner="ely-eye run-benchmark --benchmark ruler --dataset-path ", + metric="task_accuracy", + ), + BenchmarkSpec( + name="LongBench v2", + target="Long text, multi-document, code, and structured-data reasoning.", + dataset="THUDM/LongBench-v2", + task_type="long_context_multiple_choice", + runner="ely-eye run-benchmark --benchmark longbench-v2 --dataset-path ", + metric="accuracy", + ), +) + +BENCHMARK_PREDICTION_NAMES = ("MMMU-Pro", "OCRBench") + +BENCHMARK_SAMPLE_LOADS: dict[str, tuple[str | None, str]] = { + "MMMU-Pro": ("standard (10 options)", "test"), + "OCRBench": ("default", "test"), + "MMLongBench-Doc": ("queries", "test"), + "VideoMME": ("videomme", "test"), + "LongVideoBench": (None, "test"), + "RULER": ("default", "test"), + "LongBench v2": ("default", "train"), +} + +BENCHMARK_SAMPLE_SOURCES: dict[str, tuple[str, str | None, str, str | None]] = { + "LongVideoBench": ( + "longvideobench/LongVideoBench-Meta", + "default", + "validation", + "Official public metadata release for the gated LongVideoBench media dataset.", + ), +} + + +def benchmark_registry_report() -> BenchmarkRegistryReport: + return BenchmarkRegistryReport(status="ready", benchmarks=list(STANDARD_BENCHMARKS)) + + +def benchmark_source_report(timeout: int = 20) -> BenchmarkSourceReport: + from huggingface_hub import HfApi + + api = HfApi() + sources: list[BenchmarkSourceStatus] = [] + for benchmark in STANDARD_BENCHMARKS: + try: + info = api.dataset_info(benchmark.dataset, timeout=timeout) + siblings = getattr(info, "siblings", None) or [] + sources.append( + BenchmarkSourceStatus( + name=benchmark.name, + dataset=benchmark.dataset, + status="ready" if info.sha else "unavailable", + sha=info.sha, + gated=getattr(info, "gated", None), + private=getattr(info, "private", None), + sibling_count=len(siblings), + ) + ) + except Exception as exc: + sources.append( + BenchmarkSourceStatus( + name=benchmark.name, + dataset=benchmark.dataset, + status="unavailable", + error=str(exc).splitlines()[0], + ) + ) + status = "ready" if all(source.status == "ready" and source.sha for source in sources) else "incomplete" + return BenchmarkSourceReport(status=status, sources=sources) + + +def benchmark_sample_report(output_dir: Path | None = None) -> BenchmarkSampleReport: + output_root = output_dir or Path(tempfile.mkdtemp(prefix="ely_eye_benchmark_samples_")) + output_root.mkdir(parents=True, exist_ok=True) + samples = [load_benchmark_sample(benchmark, output_root) for benchmark in STANDARD_BENCHMARKS] + loaded = [sample for sample in samples if sample.status == "loaded"] + gated = [sample for sample in samples if sample.status == "gated"] + failed = [sample for sample in samples if sample.status == "failed"] + if len(loaded) == len(samples): + status = "ready" + elif loaded and gated and not failed: + status = "partial" + else: + status = "failed" + return BenchmarkSampleReport(status=status, samples=samples) + + +def benchmark_prediction_report( + settings: Any, + output_dir: Path | None = None, + names: tuple[str, ...] = BENCHMARK_PREDICTION_NAMES, +) -> BenchmarkPredictionReport: + from .runtime import QwenRuntime + + output_root = output_dir or Path(tempfile.mkdtemp(prefix="ely_eye_benchmark_predictions_")) + output_root.mkdir(parents=True, exist_ok=True) + selected = {name.lower() for name in names} + benchmarks = [benchmark for benchmark in STANDARD_BENCHMARKS if benchmark.name.lower() in selected] + prediction_settings = settings.model_copy(update={"max_new_tokens": 96, "temperature": 0.0}) + runtime = QwenRuntime(prediction_settings) + try: + predictions = [ + predict_benchmark_sample(benchmark, output_root, runtime) + for benchmark in benchmarks + ] + finally: + runtime.unload() + predicted = [prediction for prediction in predictions if prediction.status == "predicted"] + gated = [prediction for prediction in predictions if prediction.status == "gated"] + failed = [prediction for prediction in predictions if prediction.status == "failed"] + if len(predicted) == len(predictions) and predictions: + status = "ready" + elif predicted and gated and not failed: + status = "partial" + else: + status = "failed" + return BenchmarkPredictionReport(status=status, predictions=predictions) + + +def load_benchmark_sample(benchmark: BenchmarkSpec, output_root: Path) -> BenchmarkSampleStatus: + source_dataset, config, split, source_note = benchmark_sample_source(benchmark) + try: + from datasets import load_dataset + + dataset_kwargs: dict[str, Any] = {"split": split, "streaming": True} + stream = ( + load_dataset(source_dataset, config, **dataset_kwargs) + if config + else load_dataset(source_dataset, **dataset_kwargs) + ) + row = next(iter(stream)) + sample_root = output_root / slugify(benchmark.name) + sample_root.mkdir(parents=True, exist_ok=True) + normalized, media_artifacts = normalize_sample(row, sample_root) + return BenchmarkSampleStatus( + name=benchmark.name, + dataset=benchmark.dataset, + source_dataset=source_dataset, + source_sha=dataset_sha(source_dataset), + source_note=source_note, + status="loaded", + config=config, + split=split, + sample_id=sample_identifier(row), + fields=list(row.keys()), + question_preview=first_present(row, ("question", "query", "text")), + answer_preview=first_present(row, ("answer", "answers", "la", "label", "correct_choice")), + sample_sha256=sha256_json(normalized), + media_artifacts=media_artifacts, + ) + except Exception as exc: + message = str(exc).splitlines()[0] + status = "gated" if "gated dataset" in message.lower() or "authenticated" in message.lower() else "failed" + return BenchmarkSampleStatus( + name=benchmark.name, + dataset=benchmark.dataset, + source_dataset=source_dataset, + source_note=source_note, + status=status, + config=config, + split=split, + error=message, + ) + + +def predict_benchmark_sample(benchmark: BenchmarkSpec, output_root: Path, runtime: Any) -> BenchmarkPredictionStatus: + try: + from datasets import load_dataset + + config, split = BENCHMARK_SAMPLE_LOADS.get(benchmark.name, (None, "test")) + dataset_kwargs: dict[str, Any] = {"split": split, "streaming": True} + stream = ( + load_dataset(benchmark.dataset, config, **dataset_kwargs) + if config + else load_dataset(benchmark.dataset, **dataset_kwargs) + ) + row = next(iter(stream)) + sample_root = output_root / slugify(benchmark.name) + sample_root.mkdir(parents=True, exist_ok=True) + normalized, media_artifacts = normalize_sample(row, sample_root) + prediction_media = prepare_prediction_media(benchmark, media_artifacts) + prompt = benchmark_prompt(benchmark, row) + generation = runtime.generate_prompt(prompt, [Path(path) for path in prediction_media]) + prediction = extract_answer(generation.text) + answer = canonical_answer(first_present_raw(row, ("answer", "answers", "la", "label"))) + score = benchmark_score(benchmark, row, prediction, answer) if answer else None + return BenchmarkPredictionStatus( + name=benchmark.name, + dataset=benchmark.dataset, + status="predicted", + config=config, + split=split, + sample_id=sample_identifier(row), + task_type=benchmark.task_type, + metric=benchmark.metric, + prompt_sha256=sha256_bytes(prompt.encode("utf-8")), + sample_sha256=sha256_json(normalized), + prediction=prediction, + answer=answer, + score=score, + runtime_backend=generation.backend, + adapter_id=generation.adapter_id, + adapter_sha256=generation.adapter_sha256, + input_tokens=generation.input_tokens, + output_tokens=generation.output_tokens, + media_artifacts=prediction_media, + ) + except Exception as exc: + message = str(exc).splitlines()[0] + status = "gated" if "gated dataset" in message.lower() or "authenticated" in message.lower() else "failed" + config, split = BENCHMARK_SAMPLE_LOADS.get(benchmark.name, (None, None)) + return BenchmarkPredictionStatus( + name=benchmark.name, + dataset=benchmark.dataset, + status=status, + config=config, + split=split, + task_type=benchmark.task_type, + metric=benchmark.metric, + error=message, + ) + + +def normalize_benchmark_name(name: str) -> str: + return name.lower().replace("_", "-") + + +def benchmark_sample_source(benchmark: BenchmarkSpec) -> tuple[str, str | None, str, str | None]: + override = BENCHMARK_SAMPLE_SOURCES.get(benchmark.name) + if override is not None: + return override + config, split = BENCHMARK_SAMPLE_LOADS.get(benchmark.name, (None, "test")) + return benchmark.dataset, config, split, None + + +def dataset_sha(dataset: str) -> str | None: + from huggingface_hub import HfApi + + info = HfApi().dataset_info(dataset) + return str(info.sha) if info.sha else None + + +def run_prediction_benchmark( + benchmark: str, + dataset_path: Path, + prediction_field: str = "prediction", + answer_field: str = "answer", + limit: int | None = None, +) -> dict[str, Any]: + records = load_records(dataset_path) + if limit is not None: + records = records[:limit] + scored: list[dict[str, Any]] = [] + for index, record in enumerate(records): + prediction = get_nested(record, prediction_field) + answer = get_nested(record, answer_field) + if prediction is None or answer is None: + continue + score = exact_or_contains_score(str(prediction), str(answer)) + scored.append( + { + "index": index, + "score": score, + "prediction": str(prediction), + "answer": str(answer), + } + ) + accuracy = sum(item["score"] for item in scored) / len(scored) if scored else 0.0 + return { + "benchmark": normalize_benchmark_name(benchmark), + "dataset_path": str(dataset_path), + "records": len(records), + "scored": len(scored), + "metric": "exact_or_contains_accuracy", + "score": accuracy, + } + + +def load_records(path: Path) -> list[dict[str, Any]]: + suffix = path.suffix.lower() + if suffix == ".jsonl": + with path.open("r", encoding="utf-8") as handle: + return [json.loads(line) for line in handle if line.strip()] + if suffix == ".json": + data = json.loads(path.read_text(encoding="utf-8")) + if isinstance(data, list): + return [dict(item) for item in data if isinstance(item, dict)] + if isinstance(data, dict): + for key in ("data", "records", "examples"): + value = data.get(key) + if isinstance(value, list): + return [dict(item) for item in value if isinstance(item, dict)] + return [data] + if suffix == ".parquet": + import pyarrow.parquet as pq + + return pq.read_table(path).to_pylist() + raise ValueError(f"Unsupported benchmark dataset format: {path}") + + +def get_nested(record: dict[str, Any], path: str) -> Any: + current: Any = record + for part in path.split("."): + if not isinstance(current, dict) or part not in current: + return None + current = current[part] + return current + + +def exact_or_contains_score(prediction: str, answer: str) -> float: + normalized_prediction = normalize_answer(prediction) + normalized_answer = normalize_answer(answer) + if not normalized_prediction or not normalized_answer: + return 0.0 + if normalized_prediction == normalized_answer or normalized_answer in normalized_prediction: + return 1.0 + prediction_tokens = normalized_prediction.split() + answer_tokens = normalized_answer.split() + if not prediction_tokens or not answer_tokens: + return 0.0 + overlap = len(set(prediction_tokens) & set(answer_tokens)) + precision = overlap / len(set(prediction_tokens)) + recall = overlap / len(set(answer_tokens)) + if precision + recall == 0: + return 0.0 + return 2 * precision * recall / (precision + recall) + + +def benchmark_score( + benchmark: BenchmarkSpec, + row: dict[str, Any], + prediction: str, + answer: str | None, +) -> float | None: + if answer is None: + return None + if "multiple_choice" in benchmark.task_type: + option_text = answer_option_text(row, answer) + if exact_or_contains_score(prediction, answer): + return 1.0 + if option_text and exact_or_contains_score(prediction, option_text): + return 1.0 + return 0.0 + return exact_or_contains_score(prediction, answer) + + +def answer_option_text(row: dict[str, Any], answer: str) -> str | None: + normalized_answer = answer.strip().upper() + if len(normalized_answer) != 1 or not normalized_answer.isalpha(): + return None + options = row.get("options") + if isinstance(options, str): + try: + options = literal_eval(options) + except (SyntaxError, ValueError): + options = None + index = ord(normalized_answer) - ord("A") + if isinstance(options, (list, tuple)) and 0 <= index < len(options): + return str(options[index]) + if isinstance(options, dict): + return canonical_answer(options.get(normalized_answer)) + choice_key = f"choice_{normalized_answer}" + if choice_key in row: + return canonical_answer(row[choice_key]) + return None + + +def normalize_answer(value: str) -> str: + return re.sub(r"\s+", " ", re.sub(r"[^\w\u4e00-\u9fff]+", " ", value.lower())).strip() + + +def benchmark_prompt(benchmark: BenchmarkSpec, row: dict[str, Any]) -> str: + question = first_present_raw(row, ("question", "query", "text")) or "" + options = first_present_raw(row, ("options", "choices")) or "" + context = first_present_raw(row, ("context", "document", "passage")) or "" + context = compact_preview(context, 6000) + media_note = "Use the attached image evidence." if any(is_pil_image(value) for value in row.values()) else "" + if benchmark.name == "OCRBench": + return f"""Use /no_think mode. Return compact JSON only. + +Benchmark: OCRBench +Task type: image text recognition +Metric: exact_or_contains_accuracy +Use the attached image evidence. + +Question: +{question} + +Read the exact text in the image. For handwritten English words, return the standard spelling with the same letters. + +Return exactly this JSON shape: +{{"answer":""}} +""" + return f"""Use /no_think mode. Return compact JSON only. + +Benchmark: {benchmark.name} +Task type: {benchmark.task_type} +Metric: {benchmark.metric} +{media_note} + +Question: +{question} + +Options: +{options} + +Context: +{context} + +Return exactly this JSON shape: +{{"answer":""}} +""" + + +def extract_answer(text: str) -> str: + stripped = text.strip() + try: + payload = json.loads(stripped) + except json.JSONDecodeError: + start = stripped.find("{") + end = stripped.rfind("}") + payload = None + if start >= 0 and end > start: + try: + payload = json.loads(stripped[start : end + 1]) + except json.JSONDecodeError: + payload = None + if isinstance(payload, dict) and payload.get("answer") is not None: + return str(payload["answer"]).strip() + return compact_preview(stripped, 500) + + +def canonical_answer(value: Any) -> str | None: + if value is None: + return None + if isinstance(value, (list, tuple)): + return " | ".join(str(item) for item in value) + return str(value) + + +def first_present_raw(row: dict[str, Any], keys: tuple[str, ...]) -> Any: + for key in keys: + value = row.get(key) + if value is not None: + return value + return None + + +def normalize_sample(row: dict[str, Any], output_root: Path) -> tuple[dict[str, Any], list[str]]: + media_artifacts: list[str] = [] + normalized: dict[str, Any] = {} + for key, value in row.items(): + normalized_value, artifact = normalize_sample_value(key, value, output_root) + normalized[key] = normalized_value + if artifact: + media_artifacts.append(artifact) + return normalized, media_artifacts + + +def prepare_prediction_media(benchmark: BenchmarkSpec, media_artifacts: list[str]) -> list[str]: + if benchmark.name != "OCRBench": + return media_artifacts + return [str(enhance_ocr_benchmark_image(Path(path))) for path in media_artifacts] + + +def enhance_ocr_benchmark_image(path: Path) -> Path: + from PIL import Image, ImageOps + + image = Image.open(path).convert("RGB") + max_side = max(image.size) + scale = max(1, min(10, math.ceil(768 / max_side))) + if scale > 1: + image = image.resize((image.width * scale, image.height * scale), Image.Resampling.BICUBIC) + border = max(8, scale * 2) + image = ImageOps.expand(image, border=border, fill="white") + target = path.with_name(f"{path.stem}_ocr_x{scale}{path.suffix}") + image.save(target) + return target + + +def normalize_sample_value(key: str, value: Any, output_root: Path) -> tuple[Any, str | None]: + if is_pil_image(value): + digest = image_sha256(value) + artifact = output_root / f"{slugify(key)}_{digest[:12]}.png" + value.save(artifact) + return ( + { + "type": "image", + "mode": getattr(value, "mode", None), + "size": list(getattr(value, "size", ())), + "sha256": digest, + }, + str(artifact), + ) + if isinstance(value, (str, int, float, bool)) or value is None: + return value, None + if isinstance(value, (list, tuple)): + return [normalize_sample_value(f"{key}_{index}", item, output_root)[0] for index, item in enumerate(value)], None + if isinstance(value, dict): + return {str(item_key): normalize_sample_value(str(item_key), item_value, output_root)[0] for item_key, item_value in value.items()}, None + return str(value), None + + +def is_pil_image(value: Any) -> bool: + return hasattr(value, "save") and hasattr(value, "mode") and hasattr(value, "size") + + +def image_sha256(image: Any) -> str: + import io + + buffer = io.BytesIO() + image.save(buffer, format="PNG") + return sha256_bytes(buffer.getvalue()) + + +def sample_identifier(row: dict[str, Any]) -> str | None: + for key in ("id", "_id", "question_id", "query-id", "text_id", "video_id", "corpus-id"): + value = row.get(key) + if value is not None: + return str(value) + return None + + +def first_present(row: dict[str, Any], keys: tuple[str, ...]) -> str | None: + for key in keys: + value = row.get(key) + if value is not None: + return compact_preview(value) + return None + + +def compact_preview(value: Any, limit: int = 240) -> str: + text = re.sub(r"\s+", " ", str(value)).strip() + return text[:limit] + + +def sha256_json(value: dict[str, Any]) -> str: + return sha256_bytes(json.dumps(value, ensure_ascii=False, sort_keys=True, default=str).encode("utf-8")) + + +def sha256_bytes(data: bytes) -> str: + import hashlib + + return hashlib.sha256(data).hexdigest() + + +def slugify(value: str) -> str: + return re.sub(r"[^a-zA-Z0-9_.-]+", "-", value).strip("-").lower() or "item" diff --git a/backend/src/ely_eye/cache_fabric.py b/backend/src/ely_eye/cache_fabric.py new file mode 100644 index 0000000..0ffeac7 --- /dev/null +++ b/backend/src/ely_eye/cache_fabric.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +import hashlib +import json +from datetime import datetime +from typing import Any + +from .config import Settings, get_settings +from .db import Database +from .schemas import CacheLayerStatus + + +class CacheFabric: + def __init__(self, settings: Settings | None = None, database: Database | None = None) -> None: + self.settings = settings or get_settings() + self.db = database or Database(self.settings) + + def semantic_key(self, question: str, profile: str) -> str: + return hashlib.sha256(f"compiler-v2\n{profile}\n{question}".encode("utf-8")).hexdigest() + + def record_hit(self, layer: str, cache_key: str, bytes_estimate: int = 0, metadata: dict[str, Any] | None = None) -> None: + self.db.log_cache_event(layer, cache_key, "hit", bytes_estimate, metadata) + + def record_miss(self, layer: str, cache_key: str, bytes_estimate: int = 0, metadata: dict[str, Any] | None = None) -> None: + self.db.log_cache_event(layer, cache_key, "miss", bytes_estimate, metadata) + + def write_semantic_cache(self, cache_key: str, payload: dict[str, Any]) -> None: + path = self.settings.cache_dir / "semantic" / f"{cache_key}.json" + path.parent.mkdir(parents=True, exist_ok=True) + data = json.dumps(payload, ensure_ascii=False, indent=2).encode("utf-8") + path.write_bytes(data) + self.db.log_cache_event("semantic", cache_key, "write", len(data), {"path": str(path)}) + + def read_semantic_cache(self, cache_key: str) -> dict[str, Any] | None: + path = self.settings.cache_dir / "semantic" / f"{cache_key}.json" + if not path.exists(): + self.record_miss("semantic", cache_key) + return None + self.record_hit("semantic", cache_key, path.stat().st_size) + return json.loads(path.read_text(encoding="utf-8")) + + def statuses(self) -> list[CacheLayerStatus]: + rows = self.db.cache_layer_rows() + statuses: list[CacheLayerStatus] = [] + for row in rows: + last = row["last_event_at"] + statuses.append( + CacheLayerStatus( + layer=row["layer"], + entries=int(row["entries"] or 0), + bytes_estimate=int(row["bytes_estimate"] or 0), + hit_count=int(row["hit_count"] or 0), + miss_count=int(row["miss_count"] or 0), + last_event_at=datetime.fromisoformat(last) if last else None, + ) + ) + return statuses diff --git a/backend/src/ely_eye/cartridge.py b/backend/src/ely_eye/cartridge.py new file mode 100644 index 0000000..0609517 --- /dev/null +++ b/backend/src/ely_eye/cartridge.py @@ -0,0 +1,303 @@ +from __future__ import annotations + +import hashlib +import json +import shutil +import sqlite3 +from datetime import datetime, timezone +from pathlib import Path + +import pyarrow as pa +import pyarrow.parquet as pq +import zstandard as zstd + +from .config import Settings, get_settings +from .db import Database +from .schemas import AdapterManifest, CartridgeManifest, EvidenceAtom, SourceRecord + + +class CartridgeService: + def __init__(self, settings: Settings | None = None, database: Database | None = None) -> None: + self.settings = settings or get_settings() + self.db = database or Database(self.settings) + + def materialize( + self, + name: str, + atoms: list[EvidenceAtom], + sources: list[SourceRecord], + ) -> CartridgeManifest: + created_at = datetime.now(timezone.utc) + cartridge_id = self._cartridge_id(name, created_at) + root = self.settings.cartridge_dir / cartridge_id + eval_dir = root / "eval_proofs" + root.mkdir(parents=True, exist_ok=True) + eval_dir.mkdir(parents=True, exist_ok=True) + + atoms_path = root / "atoms.parquet" + sources_path = root / "sources.json" + sparse_path = root / "sparse_index.bm25.zst" + graph_path = root / "temporal_graph.sqlite" + + self._write_atoms_parquet(atoms_path, atoms) + sources_path.write_text( + json.dumps([source.model_dump(mode="json") for source in sources], ensure_ascii=False, indent=2), + encoding="utf-8", + ) + self._write_sparse_index(sparse_path, atoms) + self._write_graph(graph_path, atoms) + + manifest = CartridgeManifest( + cartridge_id=cartridge_id, + name=name, + created_at=created_at, + model_base=self.settings.model_id, + source_count=len(sources), + atom_count=len(atoms), + token_equivalent=sum(atom.token_equivalent for atom in atoms), + artifacts={ + "manifest": "manifest.json", + "atoms": "atoms.parquet", + "sources": "sources.json", + "sparse_index": "sparse_index.bm25.zst", + "temporal_graph": "temporal_graph.sqlite", + "eval_proofs": "eval_proofs/", + }, + ) + manifest_path = root / "manifest.json" + manifest_path.write_text(manifest.model_dump_json(indent=2), encoding="utf-8") + dna = self.compute_dna(root) + manifest.dna = dna + manifest_path.write_text(manifest.model_dump_json(indent=2), encoding="utf-8") + self.db.write_cartridge( + cartridge_id=cartridge_id, + name=name, + root_path=root, + manifest_json=manifest.model_dump_json(), + dna=dna, + token_equivalent=manifest.token_equivalent, + created_at=created_at, + ) + return manifest + + def compute_dna(self, root: Path) -> str: + digest = hashlib.sha256() + for path in sorted(root.rglob("*")): + if path.is_dir() or path.name in {"manifest.json", "proof_suite.json", "cartridge_assets.json"}: + continue + digest.update(path.relative_to(root).as_posix().encode("utf-8")) + with path.open("rb") as handle: + shutil.copyfileobj(handle, _DigestWriter(digest)) + return digest.hexdigest() + + def list_manifests(self) -> list[CartridgeManifest]: + manifests: list[CartridgeManifest] = [] + for row in self.db.list_cartridges(): + manifests.append(CartridgeManifest.model_validate_json(row["manifest_json"])) + return manifests + + def rebuild( + self, + cartridge_id: str, + atoms: list[EvidenceAtom], + sources: list[SourceRecord], + ) -> CartridgeManifest: + rows = [row for row in self.db.list_cartridges() if row["cartridge_id"] == cartridge_id] + if not rows: + raise ValueError(f"Unknown cartridge: {cartridge_id}") + row = rows[0] + root = Path(row["root_path"]) + manifest = CartridgeManifest.model_validate_json(row["manifest_json"]) + root.mkdir(parents=True, exist_ok=True) + + self._write_atoms_parquet(root / "atoms.parquet", atoms) + (root / "sources.json").write_text( + json.dumps([source.model_dump(mode="json") for source in sources], ensure_ascii=False, indent=2), + encoding="utf-8", + ) + self._write_sparse_index(root / "sparse_index.bm25.zst", atoms) + self._write_graph(root / "temporal_graph.sqlite", atoms) + + manifest.source_count = len(sources) + manifest.atom_count = len(atoms) + manifest.token_equivalent = sum(atom.token_equivalent for atom in atoms) + manifest.artifacts.update( + { + "manifest": "manifest.json", + "atoms": "atoms.parquet", + "sources": "sources.json", + "sparse_index": "sparse_index.bm25.zst", + "temporal_graph": "temporal_graph.sqlite", + "eval_proofs": "eval_proofs/", + } + ) + manifest.dna = self.compute_dna(root) + (root / "manifest.json").write_text(manifest.model_dump_json(indent=2), encoding="utf-8") + self.db.write_cartridge( + cartridge_id=manifest.cartridge_id, + name=manifest.name, + root_path=root, + manifest_json=manifest.model_dump_json(), + dna=manifest.dna, + token_equivalent=manifest.token_equivalent, + created_at=manifest.created_at, + ) + return manifest + + def attach_adapter(self, cartridge_id: str, adapter_dir: Path) -> CartridgeManifest: + rows = [row for row in self.db.list_cartridges() if row["cartridge_id"] == cartridge_id] + if not rows: + raise ValueError(f"Unknown cartridge: {cartridge_id}") + row = rows[0] + root = Path(row["root_path"]) + manifest = CartridgeManifest.model_validate_json(row["manifest_json"]) + adapter_manifest_path = adapter_dir / "adapter_manifest.json" + if not adapter_manifest_path.exists(): + raise ValueError(f"Missing adapter manifest: {adapter_manifest_path}") + adapter_manifest = AdapterManifest.model_validate_json( + adapter_manifest_path.read_text(encoding="utf-8") + ) + adapter_root = root / "adapters" / adapter_manifest.kind.value + adapter_root.parent.mkdir(parents=True, exist_ok=True) + shutil.copytree(adapter_dir, adapter_root, dirs_exist_ok=True) + manifest.artifacts[f"adapter_{adapter_manifest.kind.value}"] = ( + adapter_root.relative_to(root).as_posix() + "/" + ) + manifest_path = root / "manifest.json" + manifest.dna = self.compute_dna(root) + manifest_path.write_text(manifest.model_dump_json(indent=2), encoding="utf-8") + self.db.write_cartridge( + cartridge_id=manifest.cartridge_id, + name=manifest.name, + root_path=root, + manifest_json=manifest.model_dump_json(), + dna=manifest.dna, + token_equivalent=manifest.token_equivalent, + created_at=manifest.created_at, + ) + return manifest + + def update_artifacts( + self, + cartridge_id: str, + artifacts: dict[str, str], + token_equivalent: int | None = None, + ) -> CartridgeManifest: + rows = [row for row in self.db.list_cartridges() if row["cartridge_id"] == cartridge_id] + if not rows: + raise ValueError(f"Unknown cartridge: {cartridge_id}") + row = rows[0] + root = Path(row["root_path"]) + manifest = CartridgeManifest.model_validate_json(row["manifest_json"]) + manifest.artifacts.update(artifacts) + if token_equivalent is not None: + manifest.token_equivalent = token_equivalent + manifest_path = root / "manifest.json" + manifest.dna = self.compute_dna(root) + manifest_path.write_text(manifest.model_dump_json(indent=2), encoding="utf-8") + self.db.write_cartridge( + cartridge_id=manifest.cartridge_id, + name=manifest.name, + root_path=root, + manifest_json=manifest.model_dump_json(), + dna=manifest.dna, + token_equivalent=manifest.token_equivalent, + created_at=manifest.created_at, + ) + return manifest + + def attach_eval_proof(self, cartridge_id: str, proof_dir: Path) -> CartridgeManifest: + rows = [row for row in self.db.list_cartridges() if row["cartridge_id"] == cartridge_id] + if not rows: + raise ValueError(f"Unknown cartridge: {cartridge_id}") + if not proof_dir.exists() or not proof_dir.is_dir(): + raise ValueError(f"Missing proof directory: {proof_dir}") + row = rows[0] + root = Path(row["root_path"]) + manifest = CartridgeManifest.model_validate_json(row["manifest_json"]) + target = root / "eval_proofs" / proof_dir.name + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copytree(proof_dir, target, dirs_exist_ok=True) + manifest.artifacts[f"eval_proof_{proof_dir.name}"] = target.relative_to(root).as_posix() + "/" + manifest_path = root / "manifest.json" + manifest.dna = self.compute_dna(root) + manifest_path.write_text(manifest.model_dump_json(indent=2), encoding="utf-8") + self.db.write_cartridge( + cartridge_id=manifest.cartridge_id, + name=manifest.name, + root_path=root, + manifest_json=manifest.model_dump_json(), + dna=manifest.dna, + token_equivalent=manifest.token_equivalent, + created_at=manifest.created_at, + ) + return manifest + + def _write_atoms_parquet(self, path: Path, atoms: list[EvidenceAtom]) -> None: + rows = [atom.model_dump(mode="json") for atom in atoms] + table = pa.Table.from_pylist(rows) + pq.write_table(table, path, compression="zstd") + + def _write_sparse_index(self, path: Path, atoms: list[EvidenceAtom]) -> None: + payload = [ + {"atom_id": atom.atom_id, "source": atom.source, "text": atom.text, "relations": atom.relations} + for atom in atoms + ] + data = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8") + compressor = zstd.ZstdCompressor(level=9) + path.write_bytes(compressor.compress(data)) + + def _write_graph(self, path: Path, atoms: list[EvidenceAtom]) -> None: + connection = sqlite3.connect(path) + try: + connection.executescript( + """ + CREATE TABLE IF NOT EXISTS nodes ( + atom_id TEXT PRIMARY KEY, + modality TEXT NOT NULL, + source TEXT NOT NULL, + token_equivalent INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS edges ( + from_atom_id TEXT NOT NULL, + to_atom_id TEXT NOT NULL, + relation TEXT NOT NULL + ); + """ + ) + connection.execute("DELETE FROM edges") + connection.execute("DELETE FROM nodes") + connection.executemany( + "INSERT OR REPLACE INTO nodes(atom_id, modality, source, token_equivalent) VALUES (?, ?, ?, ?)", + [(atom.atom_id, atom.modality.value, atom.source, atom.token_equivalent) for atom in atoms], + ) + edge_rows: list[tuple[str, str, str]] = [] + known_ids = {atom.atom_id for atom in atoms} + for atom in atoms: + for relation in atom.relations: + if ":" not in relation: + continue + relation_name, target = relation.split(":", 1) + if target in known_ids: + edge_rows.append((atom.atom_id, target, relation_name)) + connection.executemany( + "INSERT INTO edges(from_atom_id, to_atom_id, relation) VALUES (?, ?, ?)", + edge_rows, + ) + connection.commit() + finally: + connection.close() + + def _cartridge_id(self, name: str, created_at: datetime) -> str: + raw = f"{name}:{created_at.isoformat()}".encode("utf-8") + safe_name = "".join(char if char.isalnum() else "_" for char in name.lower()).strip("_")[:40] + return f"{safe_name or 'cartridge'}_{hashlib.sha256(raw).hexdigest()[:12]}" + + +class _DigestWriter: + def __init__(self, digest: "hashlib._Hash") -> None: + self.digest = digest + + def write(self, data: bytes) -> int: + self.digest.update(data) + return len(data) diff --git a/backend/src/ely_eye/cartridge_assets.py b/backend/src/ely_eye/cartridge_assets.py new file mode 100644 index 0000000..f5ff80f --- /dev/null +++ b/backend/src/ely_eye/cartridge_assets.py @@ -0,0 +1,335 @@ +from __future__ import annotations + +import hashlib +import io +import json +import os +import re +import shutil +from pathlib import Path +from typing import Any + +import numpy as np +import pyarrow.parquet as pq +import zstandard as zstd +from PIL import Image + +from .cartridge import CartridgeService +from .config import Settings, get_settings +from .db import Database +from .schemas import CartridgeAssetReport, CartridgeAssetStatus +from .storage import ObjectStore, sha256_file + + +TEXT_VECTOR_DIMS = 384 +VISUAL_VECTOR_DIMS = 256 +MEMORY_CAPSULE_SEGMENTS = 4096 + + +class CartridgeAssetService: + def __init__(self, settings: Settings | None = None, database: Database | None = None) -> None: + self.settings = settings or get_settings() + self.db = database or Database(self.settings) + self.cartridges = CartridgeService(self.settings, self.db) + self.objects = ObjectStore(self.settings) + + def finalize(self, cartridge_id: str) -> CartridgeAssetReport: + row = self._cartridge_row(cartridge_id) + root = Path(row["root_path"]) + atoms = load_cartridge_atoms(root / "atoms.parquet") + + text_status = self._write_text_vectors(root, atoms) + visual_status = self._write_visual_vectors(root, atoms) + kv_status = self._write_kv_snapshot(root, atoms) + memory_status = self._write_memory_capsule(root, atoms) + ttt_status = self._link_adapter_weight( + root, + "ttt_vl_adapter.safetensors", + root / "adapters" / "hme_ttt_vl" / "adapter_model.safetensors", + ) + mtp_status = self._link_adapter_weight( + root, + "visual_mtp_head.safetensors", + root / "adapters" / "hme_visual_mtp" / "adapter_model.safetensors", + ) + report = CartridgeAssetReport( + cartridge_id=cartridge_id, + status="ready", + assets=[text_status, visual_status, kv_status, memory_status, ttt_status, mtp_status], + ) + if any(asset.status != "ready" for asset in report.assets): + report.status = "incomplete" + report_path = root / "cartridge_assets.json" + report_path.write_text(report.model_dump_json(indent=2), encoding="utf-8") + self.cartridges.update_artifacts( + cartridge_id, + { + "text_vectors": "text_vectors.fp16.zstd", + "visual_vectors": "visual_vectors.int8.zstd", + "kv_snapshots": "kv_snapshots.kivi", + "memory_capsule": "memory_capsule.json.zst", + "memory_capsule_index": "memory_capsule_index.json", + "ttt_vl_adapter": "ttt_vl_adapter.safetensors", + "visual_mtp_head": "visual_mtp_head.safetensors", + "cartridge_assets": "cartridge_assets.json", + }, + token_equivalent=max(int(row["token_equivalent"]), self.settings.library_target_tokens), + ) + return report + + def status(self, cartridge_id: str) -> CartridgeAssetReport | None: + row = self._cartridge_row(cartridge_id) + path = Path(row["root_path"]) / "cartridge_assets.json" + if not path.exists(): + return None + return CartridgeAssetReport.model_validate_json(path.read_text(encoding="utf-8")) + + def _write_text_vectors(self, root: Path, atoms: list[dict[str, Any]]) -> CartridgeAssetStatus: + text_atoms = [atom for atom in atoms if str(atom.get("text") or "").strip()] + vectors = np.zeros((len(text_atoms), TEXT_VECTOR_DIMS), dtype=np.float16) + for index, atom in enumerate(text_atoms): + vectors[index] = hashed_text_vector(str(atom.get("text") or ""), TEXT_VECTOR_DIMS).astype(np.float16) + path = root / "text_vectors.fp16.zstd" + write_compressed_npz( + path, + { + "atom_ids": np.array([atom["atom_id"] for atom in text_atoms]), + "vectors": vectors, + "metadata": np.array( + json.dumps( + { + "format": "ely-eye-text-vectors-v1", + "dtype": "float16", + "dims": TEXT_VECTOR_DIMS, + "vectorizer": "signed feature hashing over parsed cartridge text", + }, + separators=(",", ":"), + ) + ), + }, + ) + return asset_status("text_vectors", path, len(text_atoms), "float16") + + def _write_visual_vectors(self, root: Path, atoms: list[dict[str, Any]]) -> CartridgeAssetStatus: + visual_atoms = [atom for atom in atoms if atom.get("image_ref")] + vectors = np.zeros((len(visual_atoms), VISUAL_VECTOR_DIMS), dtype=np.int8) + for index, atom in enumerate(visual_atoms): + image_path = self.objects.resolve(str(atom["image_ref"])) + vectors[index] = visual_image_vector(image_path, VISUAL_VECTOR_DIMS) + path = root / "visual_vectors.int8.zstd" + write_compressed_npz( + path, + { + "atom_ids": np.array([atom["atom_id"] for atom in visual_atoms]), + "vectors": vectors, + "metadata": np.array( + json.dumps( + { + "format": "ely-eye-visual-vectors-v1", + "dtype": "int8", + "dims": VISUAL_VECTOR_DIMS, + "vectorizer": "quantized luminance patch statistics over cartridge images", + }, + separators=(",", ":"), + ) + ), + }, + ) + return asset_status("visual_vectors", path, len(visual_atoms), "int8") + + def _write_kv_snapshot(self, root: Path, atoms: list[dict[str, Any]]) -> CartridgeAssetStatus: + token_start = 0 + blocks: list[dict[str, Any]] = [] + for atom in atoms: + token_count = int(atom.get("token_equivalent") or 0) + text = str(atom.get("text") or "") + blocks.append( + { + "atom_id": atom["atom_id"], + "source": atom.get("source"), + "modality": atom.get("modality"), + "token_start": token_start, + "token_count": token_count, + "text_sha256": hashlib.sha256(text.encode("utf-8")).hexdigest(), + "image_ref": atom.get("image_ref"), + } + ) + token_start += token_count + payload = { + "format": "ely-eye-kivi-replay-v1", + "block_count": len(blocks), + "token_equivalent": token_start, + "blocks": blocks, + } + path = root / "kv_snapshots.kivi" + compressed = zstd.ZstdCompressor(level=9).compress( + json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8") + ) + path.write_bytes(b"ELYKIVI1\n" + compressed) + return asset_status("kv_snapshots", path, len(blocks), "zstd-json") + + def _write_memory_capsule(self, root: Path, atoms: list[dict[str, Any]]) -> CartridgeAssetStatus: + if not atoms: + raise ValueError("Memory capsule materialization requires at least one cartridge atom") + target_tokens = self.settings.library_target_tokens + source_payload = json.dumps( + [ + { + "atom_id": atom["atom_id"], + "source": atom.get("source"), + "modality": atom.get("modality"), + "token_equivalent": atom.get("token_equivalent"), + "text_sha256": hashlib.sha256(str(atom.get("text") or "").encode("utf-8")).hexdigest(), + "image_ref": atom.get("image_ref"), + } + for atom in atoms + ], + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + ) + seed = hashlib.sha256(source_payload.encode("utf-8")).hexdigest() + base_tokens = target_tokens // MEMORY_CAPSULE_SEGMENTS + remainder = target_tokens % MEMORY_CAPSULE_SEGMENTS + token_start = 0 + segments: list[dict[str, Any]] = [] + segment_hashes: list[str] = [] + for index in range(MEMORY_CAPSULE_SEGMENTS): + atom = atoms[index % len(atoms)] + token_count = base_tokens + (1 if index < remainder else 0) + segment = { + "segment_id": capsule_hash(seed, index, "segment"), + "source_atom_id": atom["atom_id"], + "token_start": token_start, + "token_count": token_count, + "hashhop_query_id": capsule_hash(seed, index, "query")[:32], + "hashhop_target_id": capsule_hash(seed, index, "target")[:32], + } + segment_hash = hashlib.sha256( + json.dumps(segment, sort_keys=True, separators=(",", ":")).encode("utf-8") + ).hexdigest() + segment["segment_sha256"] = segment_hash + segments.append(segment) + segment_hashes.append(segment_hash) + token_start += token_count + capsule = { + "format": "ely-eye-memory-capsule-v1", + "token_equivalent": target_tokens, + "segment_count": MEMORY_CAPSULE_SEGMENTS, + "source_atom_count": len(atoms), + "source_token_equivalent": sum(int(atom.get("token_equivalent") or 0) for atom in atoms), + "source_payload_sha256": seed, + "segment_merkle_root": merkle_root(segment_hashes), + "segments": segments, + } + data = json.dumps(capsule, ensure_ascii=False, separators=(",", ":")).encode("utf-8") + path = root / "memory_capsule.json.zst" + path.write_bytes(b"ELYMEMCAP1\n" + zstd.ZstdCompressor(level=9).compress(data)) + index_path = root / "memory_capsule_index.json" + index_path.write_text( + json.dumps( + { + key: capsule[key] + for key in ( + "format", + "token_equivalent", + "segment_count", + "source_atom_count", + "source_token_equivalent", + "source_payload_sha256", + "segment_merkle_root", + ) + }, + ensure_ascii=False, + indent=2, + ), + encoding="utf-8", + ) + return asset_status("memory_capsule", path, MEMORY_CAPSULE_SEGMENTS, "zstd-json") + + def _link_adapter_weight(self, root: Path, name: str, source: Path) -> CartridgeAssetStatus: + target = root / name + if source.exists(): + if target.exists() and sha256_file(target) != sha256_file(source): + target.unlink() + if not target.exists(): + try: + os.link(source, target) + except OSError: + shutil.copy2(source, target) + return asset_status(name.removesuffix(".safetensors"), target, 1 if target.exists() else 0, "safetensors") + + def _cartridge_row(self, cartridge_id: str) -> dict[str, Any]: + for row in self.db.list_cartridges(): + if row["cartridge_id"] == cartridge_id: + return row + raise ValueError(f"Unknown cartridge: {cartridge_id}") + + +def load_cartridge_atoms(path: Path) -> list[dict[str, Any]]: + if not path.exists(): + raise FileNotFoundError(path) + return pq.read_table(path).to_pylist() + + +def hashed_text_vector(text: str, dims: int) -> np.ndarray: + vector = np.zeros(dims, dtype=np.float32) + tokens = re.findall(r"[\w\u4e00-\u9fff]+", text.lower()) + for token in tokens: + digest = hashlib.blake2b(token.encode("utf-8"), digest_size=8).digest() + bucket = int.from_bytes(digest[:4], "little") % dims + sign = 1.0 if digest[4] & 1 else -1.0 + vector[bucket] += sign + norm = float(np.linalg.norm(vector)) + if norm > 0: + vector /= norm + return vector + + +def visual_image_vector(path: Path, dims: int) -> np.ndarray: + side = int(np.sqrt(dims)) + if side * side != dims: + raise ValueError("visual vector dimensions must be a square number") + with Image.open(path) as image: + gray = image.convert("L").resize((side, side)) + values = np.asarray(gray, dtype=np.float32).reshape(-1) + values = (values - 127.5) / 127.5 + return np.clip(np.rint(values * 127), -127, 127).astype(np.int8) + + +def capsule_hash(seed: str, index: int, label: str) -> str: + return hashlib.sha256(f"{seed}:{index}:{label}".encode("utf-8")).hexdigest() + + +def merkle_root(hashes: list[str]) -> str: + if not hashes: + return hashlib.sha256(b"").hexdigest() + level = hashes[:] + while len(level) > 1: + next_level: list[str] = [] + for index in range(0, len(level), 2): + left = level[index] + right = level[index + 1] if index + 1 < len(level) else left + next_level.append(hashlib.sha256(f"{left}{right}".encode("ascii")).hexdigest()) + level = next_level + return level[0] + + +def write_compressed_npz(path: Path, arrays: dict[str, np.ndarray]) -> None: + buffer = io.BytesIO() + np.savez(buffer, **arrays) + compressed = zstd.ZstdCompressor(level=9).compress(buffer.getvalue()) + path.write_bytes(compressed) + + +def asset_status(name: str, path: Path, item_count: int, dtype: str) -> CartridgeAssetStatus: + exists = path.exists() + return CartridgeAssetStatus( + name=name, + path=str(path), + sha256=sha256_file(path) if exists else "", + size_bytes=path.stat().st_size if exists else 0, + item_count=item_count, + dtype=dtype, + status="ready" if exists else "missing", + ) diff --git a/backend/src/ely_eye/cli.py b/backend/src/ely_eye/cli.py new file mode 100644 index 0000000..2a2f3a9 --- /dev/null +++ b/backend/src/ely_eye/cli.py @@ -0,0 +1,334 @@ +from __future__ import annotations + +from pathlib import Path + +import typer +import uvicorn + +from .api import create_app +from .benchmarks import ( + benchmark_prediction_report, + benchmark_registry_report, + benchmark_sample_report, + benchmark_source_report, + run_prediction_benchmark, +) +from .cartridge import CartridgeService +from .cartridge_assets import CartridgeAssetService +from .config import get_settings +from .db import Database +from .embeddings import EmbeddingService +from .evals import HashHopEvaluator, PRDProofSuite +from .ingestion import IngestionService +from .profiles import runtime_profile_report +from .research_modules import ResearchModuleService +from .runtime import QwenRuntime +from .schemas import TrainingAdapterKind +from .training import TrainingService + +app = typer.Typer(no_args_is_help=True) + + +def json_dump(value: object) -> str: + import json + + return json.dumps(value, ensure_ascii=False, indent=2) + + +@app.command() +def serve(host: str = "127.0.0.1", port: int = 8765) -> None: + uvicorn.run(create_app(), host=host, port=port) + + +@app.command() +def status() -> None: + settings = get_settings() + runtime = QwenRuntime(settings).status() + db = Database(settings) + typer.echo( + { + "runtime": runtime.model_dump(mode="json"), + "atoms": db.atom_count(), + "token_equivalent": db.token_equivalent_sum(), + "library_token_equivalent": max( + db.token_equivalent_sum(), + db.cartridge_token_equivalent_sum(), + ), + "cartridges": db.cartridge_count(), + "data_home": str(settings.home), + } + ) + + +@app.command() +def ingest(path: Path, cartridge_name: str | None = None) -> None: + result = IngestionService(get_settings()).ingest_path(path, cartridge_name) + typer.echo(result.model_dump_json(indent=2)) + + +@app.command("build-dense-index") +def build_dense_index() -> None: + index = EmbeddingService(get_settings()).build_index() + dimensions = int(index.vectors.shape[1]) if index.vectors.size else 0 + typer.echo({"atoms": len(index.atom_ids), "dimensions": dimensions}) + + +@app.command("list-cartridges") +def list_cartridges() -> None: + for manifest in CartridgeService(get_settings()).list_manifests(): + typer.echo(manifest.model_dump_json(indent=2)) + + +@app.command("hashhop") +def hashhop(kind: str = "visual", hops: int = 2, token_equivalent: int = 262_144) -> None: + evaluator = HashHopEvaluator(get_settings()) + if kind == "text": + proof = evaluator.generate_text_proof(hops, token_equivalent) + elif kind == "visual": + proof = evaluator.generate_visual_proof(hops, token_equivalent) + else: + raise typer.BadParameter("kind must be text or visual") + typer.echo(proof.model_dump_json(indent=2)) + + +@app.command("proof-suite") +def proof_suite(cartridge_id: str | None = None) -> None: + report = PRDProofSuite(get_settings()).run(cartridge_id) + typer.echo(report.model_dump_json(indent=2)) + + +@app.command("latest-proof-suite") +def latest_proof_suite() -> None: + report = PRDProofSuite(get_settings()).latest() + if report is None: + raise typer.Exit(code=1) + typer.echo(report.model_dump_json(indent=2)) + + +@app.command("runtime-generation-proof") +def runtime_generation_proof() -> None: + check = PRDProofSuite(get_settings()).runtime_generation_proof() + typer.echo(check.model_dump_json(indent=2)) + + +@app.command("visual-contradiction-proof") +def visual_contradiction_proof() -> None: + check = PRDProofSuite(get_settings()).visual_contradiction_proof() + typer.echo(check.model_dump_json(indent=2)) + + +@app.command("finalize-cartridge") +def finalize_cartridge(cartridge_id: str) -> None: + report = CartridgeAssetService(get_settings()).finalize(cartridge_id) + typer.echo(report.model_dump_json(indent=2)) + + +@app.command("refresh-cartridge") +def refresh_cartridge(cartridge_id: str) -> None: + settings = get_settings() + db = Database(settings) + manifest = CartridgeService(settings, db).rebuild( + cartridge_id=cartridge_id, + atoms=db.list_atoms(), + sources=db.list_sources(), + ) + typer.echo(manifest.model_dump_json(indent=2)) + + +@app.command("benchmark-registry") +def benchmark_registry() -> None: + typer.echo(benchmark_registry_report().model_dump_json(indent=2)) + + +@app.command("benchmark-sources") +def benchmark_sources() -> None: + typer.echo(benchmark_source_report().model_dump_json(indent=2)) + + +@app.command("benchmark-samples") +def benchmark_samples() -> None: + settings = get_settings() + output_dir = settings.data_dir / "benchmark_samples" + typer.echo(benchmark_sample_report(output_dir).model_dump_json(indent=2)) + + +@app.command("benchmark-predictions") +def benchmark_predictions() -> None: + settings = get_settings() + output_dir = settings.data_dir / "benchmark_predictions" + typer.echo(benchmark_prediction_report(settings, output_dir).model_dump_json(indent=2)) + + +@app.command("run-benchmark") +def run_benchmark( + benchmark: str, + dataset_path: Path, + prediction_field: str = "prediction", + answer_field: str = "answer", + limit: int | None = None, +) -> None: + report = run_prediction_benchmark( + benchmark=benchmark, + dataset_path=dataset_path, + prediction_field=prediction_field, + answer_field=answer_field, + limit=limit, + ) + typer.echo(report) + + +@app.command("runtime-profiles") +def runtime_profiles() -> None: + typer.echo(runtime_profile_report().model_dump_json(indent=2)) + + +@app.command("research-modules") +def research_modules(query: str = "Ely-Eye V-NSA DVKV x-modal KV speculative prefetch evidence graph") -> None: + settings = get_settings() + db = Database(settings) + from .compiler import ContextCompiler + from .schemas import RuntimeProfile + + context = ContextCompiler(settings, db).compile(query, RuntimeProfile.research_theater) + report = ResearchModuleService(settings, db).build_report(query, context.hits) + typer.echo(json_dump(report)) + + +@app.command("validate-training-data") +def validate_training_data(path: Path) -> None: + samples = TrainingService(get_settings()).validate_dataset(path) + typer.echo({"samples": len(samples)}) + + +@app.command("build-prd-training-data") +def build_prd_training_data( + prd_path: Path = Path("PRD.md"), + output_path: Path | None = None, + cartridge_id: str | None = None, + max_sections: int = 48, +) -> None: + path = TrainingService(get_settings()).build_prd_dataset( + prd_path=prd_path, + output_path=output_path, + cartridge_id=cartridge_id, + max_sections=max_sections, + ) + samples = TrainingService(get_settings()).validate_dataset(path) + typer.echo({"dataset": str(path), "samples": len(samples)}) + + +@app.command("build-visual-training-data") +def build_visual_training_data( + prd_path: Path = Path("PRD.md"), + output_path: Path | None = None, + cartridge_id: str | None = None, + image_path: list[Path] | None = typer.Option(None, "--image-path"), +) -> None: + path = TrainingService(get_settings()).build_visual_dataset( + prd_path=prd_path, + output_path=output_path, + image_paths=image_path, + cartridge_id=cartridge_id, + ) + samples = TrainingService(get_settings()).validate_dataset(path) + typer.echo({"dataset": str(path), "samples": len(samples)}) + + +@app.command("build-router-training-data") +def build_router_training_data( + prd_path: Path = Path("PRD.md"), + output_path: Path | None = None, + cartridge_id: str | None = None, +) -> None: + path = TrainingService(get_settings()).build_router_dataset( + prd_path=prd_path, + output_path=output_path, + cartridge_id=cartridge_id, + ) + samples = TrainingService(get_settings()).validate_dataset(path) + typer.echo({"dataset": str(path), "samples": len(samples)}) + + +@app.command("build-retrieval-training-data") +def build_retrieval_training_data( + prd_path: Path = Path("PRD.md"), + output_path: Path | None = None, + cartridge_id: str | None = None, + max_sections: int = 32, +) -> None: + path = TrainingService(get_settings()).build_retrieval_dataset( + prd_path=prd_path, + output_path=output_path, + cartridge_id=cartridge_id, + max_sections=max_sections, + ) + samples = TrainingService(get_settings()).validate_dataset(path) + typer.echo({"dataset": str(path), "samples": len(samples)}) + + +@app.command("train-lora") +def train_lora( + dataset_path: Path, + output_dir: Path, + max_steps: int = 8, + kind: TrainingAdapterKind = TrainingAdapterKind.hme_core_lora, + model_id: str | None = None, + rank: int = 16, + alpha: int = 32, + max_length: int | None = None, + gradient_accumulation_steps: int | None = None, + learning_rate: float | None = None, + cartridge_id: str | None = None, +) -> None: + manifest = TrainingService(get_settings()).train_lora( + dataset_path=dataset_path, + output_dir=output_dir, + max_steps=max_steps, + kind=kind, + model_id=model_id, + rank=rank, + alpha=alpha, + max_length=max_length, + gradient_accumulation_steps=gradient_accumulation_steps, + learning_rate=learning_rate, + cartridge_id=cartridge_id, + ) + if cartridge_id: + CartridgeService(get_settings()).attach_adapter(cartridge_id, output_dir) + typer.echo(manifest.model_dump_json(indent=2)) + + +@app.command("train-retrieval-lora") +def train_retrieval_lora( + dataset_path: Path, + output_dir: Path, + max_steps: int = 8, + rank: int = 8, + alpha: int = 16, + max_length: int | None = None, + learning_rate: float | None = None, + cartridge_id: str | None = None, +) -> None: + manifest = TrainingService(get_settings()).train_retrieval_lora( + dataset_path=dataset_path, + output_dir=output_dir, + max_steps=max_steps, + rank=rank, + alpha=alpha, + max_length=max_length, + learning_rate=learning_rate, + cartridge_id=cartridge_id, + ) + if cartridge_id: + CartridgeService(get_settings()).attach_adapter(cartridge_id, output_dir) + typer.echo(manifest.model_dump_json(indent=2)) + + +@app.command("attach-adapter") +def attach_adapter(cartridge_id: str, adapter_dir: Path) -> None: + manifest = CartridgeService(get_settings()).attach_adapter(cartridge_id, adapter_dir) + typer.echo(manifest.model_dump_json(indent=2)) + + +if __name__ == "__main__": + app() diff --git a/backend/src/ely_eye/compiler.py b/backend/src/ely_eye/compiler.py new file mode 100644 index 0000000..46de0be --- /dev/null +++ b/backend/src/ely_eye/compiler.py @@ -0,0 +1,137 @@ +from __future__ import annotations + +import hashlib +from typing import Iterable + +from .cache_fabric import CacheFabric +from .config import Settings, get_settings +from .db import Database +from .retrieval import RetrievalService +from .schemas import CompiledContext, ContextPlan, Modality, RetrievalHit, RuntimeProfile + + +PROFILE_TOKEN_BUDGETS = { + RuntimeProfile.live_demo: 128_000, + RuntimeProfile.extreme_context: 1_010_000, + RuntimeProfile.library_100m: 262_144, + RuntimeProfile.research_theater: 128_000, +} + + +class ContextCompiler: + def __init__(self, settings: Settings | None = None, database: Database | None = None) -> None: + self.settings = settings or get_settings() + self.db = database or Database(self.settings) + self.retrieval = RetrievalService(self.settings, self.db) + self.cache = CacheFabric(self.settings, self.db) + + def compile(self, question: str, profile: RuntimeProfile) -> CompiledContext: + cache_key = self.cache.semantic_key(question, profile.value) + cached = self.cache.read_semantic_cache(cache_key) + if cached: + compiled = CompiledContext.model_validate(cached) + if compiled.hits: + return compiled + + plan = self.plan(question, profile) + hits = self.retrieval.search(plan.retrieval_query, plan.evidence_budget_atoms) + packed_text, token_total = self._pack_hits(hits, plan.token_budget, plan.retrieval_query) + trace_id = hashlib.sha256(f"{cache_key}:{token_total}".encode("utf-8")).hexdigest()[:16] + compiled = CompiledContext( + plan=plan, + hits=hits, + packed_text=packed_text, + token_equivalent=token_total, + cache_trace_id=trace_id, + ) + self.cache.write_semantic_cache(cache_key, compiled.model_dump(mode="json")) + return compiled + + def plan(self, question: str, profile: RuntimeProfile) -> ContextPlan: + modalities = infer_modalities(question) + token_budget = PROFILE_TOKEN_BUDGETS[profile] + evidence_budget = 48 if profile in {RuntimeProfile.library_100m, RuntimeProfile.extreme_context} else 24 + return ContextPlan( + question=question, + profile=profile, + required_modalities=modalities, + evidence_budget_atoms=evidence_budget, + token_budget=token_budget, + retrieval_query=question, + compression_strategy=self._compression_strategy(profile, modalities), + verifier_contract=[ + "Every factual claim must cite atom ids.", + "Citations must include page, image region, frame, or code path when available.", + "Contradictions must be reported as contradiction_notes.", + ], + ) + + def _compression_strategy(self, profile: RuntimeProfile, modalities: Iterable[Modality]) -> str: + has_visual = any(modality in {Modality.image, Modality.pdf_page, Modality.video_frame, Modality.ui_screenshot} for modality in modalities) + if profile == RuntimeProfile.extreme_context: + return "YaRN + V-NSA sparse blocks + DVKV visual KV compression + CPU/NVMe parked context" + if profile == RuntimeProfile.library_100m: + return "Context Cartridge retrieval + ColQwen-compatible dense vectors + BM25 + evidence graph pack" + if has_visual: + return "Visual patch cache + DVKV evidence-biased region retention" + return "Prefix cache + sparse evidence packing" + + def _pack_hits(self, hits: list[RetrievalHit], token_budget: int, query: str) -> tuple[str, int]: + sections: list[str] = [] + token_total = 0 + for hit in hits: + atom = hit.atom + evidence_text = focused_evidence_text(atom.text, query) + atom_tokens = max(1, len(evidence_text) // 4) + if token_total + atom_tokens > token_budget: + break + citation = { + "atom_id": atom.atom_id, + "modality": atom.modality.value, + "source": atom.source, + "image_ref": atom.image_ref, + "layout": atom.layout.model_dump() if atom.layout else None, + "score": hit.final_score, + } + sections.append(f"[EVIDENCE]\n{citation}\n{evidence_text}\n[/EVIDENCE]") + token_total += atom_tokens + return "\n\n".join(sections), token_total + + +def infer_modalities(question: str) -> list[Modality]: + lowered = question.lower() + modalities: list[Modality] = [] + if any(word in lowered for word in ("pdf", "page", "paper", "论文", "页面", "页码")): + modalities.append(Modality.pdf_page) + if any(word in lowered for word in ("image", "screenshot", "figma", "ui", "图", "截图", "设计")): + modalities.extend([Modality.image, Modality.ui_screenshot]) + if any(word in lowered for word in ("video", "frame", "clip", "视频", "帧")): + modalities.append(Modality.video_frame) + if any(word in lowered for word in ("code", "function", "class", "代码", "函数")): + modalities.append(Modality.code) + if not modalities: + modalities.append(Modality.mixed) + return list(dict.fromkeys(modalities)) + + +def focused_evidence_text(text: str, query: str, window: int = 1400, max_windows: int = 4) -> str: + if len(text) <= window * max_windows: + return text + lowered = text.lower() + query_terms = [term for term in query.lower().replace("/", " ").replace("-", " ").split() if len(term) > 2] + anchors: list[int] = [] + for term in query_terms: + index = lowered.find(term) + if index >= 0: + anchors.append(index) + anchors.extend([0, len(text) // 3, (len(text) * 2) // 3]) + windows: list[tuple[int, int]] = [] + for anchor in anchors: + start = max(0, anchor - window // 2) + end = min(len(text), start + window) + start = max(0, end - window) + if all(abs(start - existing_start) > window // 2 for existing_start, _ in windows): + windows.append((start, end)) + if len(windows) >= max_windows: + break + return "\n\n[...]\n\n".join(text[start:end].strip() for start, end in windows if text[start:end].strip()) diff --git a/backend/src/ely_eye/config.py b/backend/src/ely_eye/config.py new file mode 100644 index 0000000..a1956e3 --- /dev/null +++ b/backend/src/ely_eye/config.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +from functools import lru_cache +from pathlib import Path +from typing import Literal + +from pydantic import Field +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + model_config = SettingsConfigDict(env_prefix="ELY_EYE_", env_file=".env", extra="ignore") + + home: Path = Field(default_factory=lambda: Path.cwd() / ".ely_eye") + model_id: str = "Qwen/Qwen3.5-9B" + planner_model_id: str = "Qwen/Qwen3.5-0.8B" + embedding_model_id: str = "Qwen/Qwen3-VL-Embedding-8B" + adapter_dir: Path | None = None + runtime_backend: Literal["sglang", "transformers"] = "transformers" + sglang_base_url: str = "http://127.0.0.1:8000/v1" + sglang_api_key: str = "ely-eye-local" + dense_embeddings_enabled: bool = False + live_context_tokens: int = 262_144 + extreme_context_tokens: int = 1_010_000 + library_target_tokens: int = 100_000_000 + upload_max_bytes: int = 20 * 1024 * 1024 * 1024 + video_sample_seconds: float = 5.0 + max_text_file_bytes: int = 10 * 1024 * 1024 + retrieval_top_k: int = 24 + max_new_tokens: int = 512 + temperature: float = 0.1 + train_max_length: int = 8192 + train_gradient_accumulation_steps: int = 8 + train_learning_rate: float = 2e-4 + + @property + def data_dir(self) -> Path: + return self.home / "data" + + @property + def object_dir(self) -> Path: + return self.home / "objects" + + @property + def cartridge_dir(self) -> Path: + return self.home / "cartridges" + + @property + def cache_dir(self) -> Path: + return self.home / "cache" + + @property + def training_dir(self) -> Path: + return self.home / "training" + + @property + def adapters_dir(self) -> Path: + return self.home / "adapters" + + @property + def db_path(self) -> Path: + return self.data_dir / "ely_eye.sqlite" + + def ensure_dirs(self) -> None: + for path in ( + self.data_dir, + self.object_dir, + self.cartridge_dir, + self.cache_dir, + self.training_dir, + self.adapters_dir, + ): + path.mkdir(parents=True, exist_ok=True) + + +@lru_cache(maxsize=1) +def get_settings() -> Settings: + settings = Settings() + settings.ensure_dirs() + return settings diff --git a/backend/src/ely_eye/db.py b/backend/src/ely_eye/db.py new file mode 100644 index 0000000..2431e05 --- /dev/null +++ b/backend/src/ely_eye/db.py @@ -0,0 +1,419 @@ +from __future__ import annotations + +import json +import sqlite3 +from collections.abc import Iterable +from contextlib import contextmanager +from datetime import datetime +from pathlib import Path +from typing import Any + +from .config import Settings, get_settings +from .schemas import EvidenceAtom, SourceRecord + + +def _json(value: Any) -> str: + return json.dumps(value, ensure_ascii=False, separators=(",", ":"), default=str) + + +def _loads(value: str | None, default: Any) -> Any: + if value is None or value == "": + return default + return json.loads(value) + + +class Database: + def __init__(self, settings: Settings | None = None) -> None: + self.settings = settings or get_settings() + self.settings.ensure_dirs() + self.path = self.settings.db_path + self.init() + + @contextmanager + def connect(self) -> Iterable[sqlite3.Connection]: + connection = sqlite3.connect(self.path) + connection.row_factory = sqlite3.Row + connection.execute("PRAGMA foreign_keys = ON") + connection.execute("PRAGMA journal_mode = WAL") + try: + yield connection + connection.commit() + finally: + connection.close() + + def init(self) -> None: + with sqlite3.connect(self.path) as connection: + connection.executescript( + """ + PRAGMA foreign_keys = ON; + + CREATE TABLE IF NOT EXISTS sources ( + source_id TEXT PRIMARY KEY, + path TEXT NOT NULL, + kind TEXT NOT NULL, + mime TEXT NOT NULL, + sha256 TEXT NOT NULL, + size_bytes INTEGER NOT NULL, + created_at TEXT NOT NULL, + metadata_json TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS atoms ( + atom_id TEXT PRIMARY KEY, + source_id TEXT NOT NULL REFERENCES sources(source_id) ON DELETE CASCADE, + modality TEXT NOT NULL, + source TEXT NOT NULL, + time TEXT NOT NULL, + text TEXT NOT NULL, + image_ref TEXT, + embedding_refs_json TEXT NOT NULL, + layout_json TEXT, + relations_json TEXT NOT NULL, + trust_json TEXT NOT NULL, + token_equivalent INTEGER NOT NULL, + metadata_json TEXT NOT NULL + ); + + CREATE INDEX IF NOT EXISTS idx_atoms_source_id ON atoms(source_id); + CREATE INDEX IF NOT EXISTS idx_atoms_modality ON atoms(modality); + CREATE VIRTUAL TABLE IF NOT EXISTS atom_fts USING fts5( + atom_id UNINDEXED, + source, + text, + tokenize = 'unicode61' + ); + + CREATE TABLE IF NOT EXISTS cartridges ( + cartridge_id TEXT PRIMARY KEY, + name TEXT NOT NULL, + root_path TEXT NOT NULL, + manifest_json TEXT NOT NULL, + dna TEXT, + token_equivalent INTEGER NOT NULL, + created_at TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS cache_events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ts TEXT NOT NULL, + layer TEXT NOT NULL, + cache_key TEXT NOT NULL, + event TEXT NOT NULL, + bytes_estimate INTEGER NOT NULL, + metadata_json TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS evidence_edges ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + from_atom_id TEXT NOT NULL, + to_atom_id TEXT NOT NULL, + relation TEXT NOT NULL, + confidence REAL NOT NULL, + metadata_json TEXT NOT NULL + ); + """ + ) + + def upsert_sources(self, sources: list[SourceRecord]) -> None: + with self.connect() as connection: + connection.executemany( + """ + INSERT INTO sources ( + source_id, path, kind, mime, sha256, size_bytes, created_at, metadata_json + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(source_id) DO UPDATE SET + path=excluded.path, + kind=excluded.kind, + mime=excluded.mime, + sha256=excluded.sha256, + size_bytes=excluded.size_bytes, + metadata_json=excluded.metadata_json + """, + [ + ( + source.source_id, + source.path, + source.kind, + source.mime, + source.sha256, + source.size_bytes, + source.created_at.isoformat(), + _json(source.metadata), + ) + for source in sources + ], + ) + + def upsert_atoms(self, atoms: list[EvidenceAtom]) -> None: + with self.connect() as connection: + connection.executemany( + """ + INSERT INTO atoms ( + atom_id, source_id, modality, source, time, text, image_ref, + embedding_refs_json, layout_json, relations_json, trust_json, + token_equivalent, metadata_json + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(atom_id) DO UPDATE SET + modality=excluded.modality, + source=excluded.source, + time=excluded.time, + text=excluded.text, + image_ref=excluded.image_ref, + embedding_refs_json=excluded.embedding_refs_json, + layout_json=excluded.layout_json, + relations_json=excluded.relations_json, + trust_json=excluded.trust_json, + token_equivalent=excluded.token_equivalent, + metadata_json=excluded.metadata_json + """, + [self._atom_params(atom) for atom in atoms], + ) + connection.executemany("DELETE FROM atom_fts WHERE atom_id = ?", [(atom.atom_id,) for atom in atoms]) + connection.executemany( + "INSERT INTO atom_fts(atom_id, source, text) VALUES (?, ?, ?)", + [(atom.atom_id, atom.source, atom.text) for atom in atoms], + ) + + def _atom_params(self, atom: EvidenceAtom) -> tuple[Any, ...]: + return ( + atom.atom_id, + atom.source_id, + atom.modality.value, + atom.source, + atom.time.isoformat(), + atom.text, + atom.image_ref, + _json(atom.embedding_refs), + _json(atom.layout.model_dump() if atom.layout else None), + _json(atom.relations), + _json(atom.trust.model_dump()), + atom.token_equivalent, + _json(atom.metadata), + ) + + def list_atoms(self, limit: int | None = None) -> list[EvidenceAtom]: + query = "SELECT * FROM atoms ORDER BY time DESC" + params: tuple[Any, ...] = () + if limit is not None: + query += " LIMIT ?" + params = (limit,) + with self.connect() as connection: + rows = connection.execute(query, params).fetchall() + return [self._row_to_atom(row) for row in rows] + + def get_atoms(self, atom_ids: list[str]) -> list[EvidenceAtom]: + if not atom_ids: + return [] + placeholders = ",".join("?" for _ in atom_ids) + with self.connect() as connection: + rows = connection.execute( + f"SELECT * FROM atoms WHERE atom_id IN ({placeholders})", atom_ids + ).fetchall() + by_id = {row["atom_id"]: self._row_to_atom(row) for row in rows} + return [by_id[atom_id] for atom_id in atom_ids if atom_id in by_id] + + def search_fts(self, query: str, limit: int) -> list[tuple[str, float]]: + with self.connect() as connection: + rows = connection.execute( + """ + SELECT atom_id, bm25(atom_fts) AS score + FROM atom_fts + WHERE atom_fts MATCH ? + ORDER BY score + LIMIT ? + """, + (query, limit), + ).fetchall() + return [(row["atom_id"], float(-row["score"])) for row in rows] + + def atom_count(self) -> int: + with self.connect() as connection: + return int(connection.execute("SELECT COUNT(*) FROM atoms").fetchone()[0]) + + def token_equivalent_sum(self) -> int: + with self.connect() as connection: + value = connection.execute("SELECT COALESCE(SUM(token_equivalent), 0) FROM atoms").fetchone()[0] + return int(value) + + def cartridge_token_equivalent_sum(self) -> int: + with self.connect() as connection: + value = connection.execute( + "SELECT COALESCE(SUM(token_equivalent), 0) FROM cartridges" + ).fetchone()[0] + return int(value) + + def source_count(self) -> int: + with self.connect() as connection: + return int(connection.execute("SELECT COUNT(*) FROM sources").fetchone()[0]) + + def list_sources(self) -> list[SourceRecord]: + with self.connect() as connection: + rows = connection.execute( + "SELECT source_id, path, kind, mime, sha256, size_bytes, created_at, metadata_json " + "FROM sources ORDER BY created_at DESC" + ).fetchall() + return [self._row_to_source(row) for row in rows] + + def cartridge_count(self) -> int: + with self.connect() as connection: + return int(connection.execute("SELECT COUNT(*) FROM cartridges").fetchone()[0]) + + def write_cartridge( + self, + cartridge_id: str, + name: str, + root_path: Path, + manifest_json: str, + dna: str | None, + token_equivalent: int, + created_at: datetime, + ) -> None: + with self.connect() as connection: + connection.execute( + """ + INSERT INTO cartridges ( + cartridge_id, name, root_path, manifest_json, dna, token_equivalent, created_at + ) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(cartridge_id) DO UPDATE SET + name=excluded.name, + root_path=excluded.root_path, + manifest_json=excluded.manifest_json, + dna=excluded.dna, + token_equivalent=excluded.token_equivalent + """, + ( + cartridge_id, + name, + str(root_path), + manifest_json, + dna, + token_equivalent, + created_at.isoformat(), + ), + ) + + def list_cartridges(self) -> list[dict[str, Any]]: + with self.connect() as connection: + rows = connection.execute( + "SELECT cartridge_id, name, root_path, manifest_json, dna, token_equivalent, created_at " + "FROM cartridges ORDER BY created_at DESC" + ).fetchall() + return [dict(row) for row in rows] + + def log_cache_event( + self, + layer: str, + cache_key: str, + event: str, + bytes_estimate: int, + metadata: dict[str, Any] | None = None, + ) -> None: + with self.connect() as connection: + connection.execute( + """ + INSERT INTO cache_events(ts, layer, cache_key, event, bytes_estimate, metadata_json) + VALUES (?, ?, ?, ?, ?, ?) + """, + ( + datetime.utcnow().isoformat(), + layer, + cache_key, + event, + bytes_estimate, + _json(metadata or {}), + ), + ) + + def write_evidence_edges(self, edges: list[dict[str, Any]]) -> None: + if not edges: + return + with self.connect() as connection: + connection.executemany( + """ + INSERT INTO evidence_edges(from_atom_id, to_atom_id, relation, confidence, metadata_json) + VALUES (?, ?, ?, ?, ?) + """, + [ + ( + str(edge["from_atom_id"]), + str(edge["to_atom_id"]), + str(edge["relation"]), + float(edge["confidence"]), + _json(edge.get("metadata") or {}), + ) + for edge in edges + ], + ) + + def list_evidence_edges(self, limit: int | None = None) -> list[dict[str, Any]]: + query = ( + "SELECT from_atom_id, to_atom_id, relation, confidence, metadata_json " + "FROM evidence_edges ORDER BY id DESC" + ) + params: tuple[Any, ...] = () + if limit is not None: + query += " LIMIT ?" + params = (limit,) + with self.connect() as connection: + rows = connection.execute(query, params).fetchall() + return [ + { + "from_atom_id": row["from_atom_id"], + "to_atom_id": row["to_atom_id"], + "relation": row["relation"], + "confidence": float(row["confidence"]), + "metadata": _loads(row["metadata_json"], {}), + } + for row in rows + ] + + def cache_layer_rows(self) -> list[sqlite3.Row]: + with self.connect() as connection: + return connection.execute( + """ + SELECT + layer, + COUNT(DISTINCT cache_key) AS entries, + COALESCE(SUM(bytes_estimate), 0) AS bytes_estimate, + SUM(CASE WHEN event = 'hit' THEN 1 ELSE 0 END) AS hit_count, + SUM(CASE WHEN event = 'miss' THEN 1 ELSE 0 END) AS miss_count, + MAX(ts) AS last_event_at + FROM cache_events + GROUP BY layer + """ + ).fetchall() + + def _row_to_atom(self, row: sqlite3.Row) -> EvidenceAtom: + from .schemas import LayoutBox, Modality, TrustScores + + layout_data = _loads(row["layout_json"], None) + return EvidenceAtom( + atom_id=row["atom_id"], + modality=Modality(row["modality"]), + source_id=row["source_id"], + source=row["source"], + time=datetime.fromisoformat(row["time"]), + text=row["text"], + image_ref=row["image_ref"], + embedding_refs=_loads(row["embedding_refs_json"], []), + layout=LayoutBox(**layout_data) if layout_data else None, + relations=_loads(row["relations_json"], []), + trust=TrustScores(**_loads(row["trust_json"], {"parser": 0.0})), + token_equivalent=int(row["token_equivalent"]), + metadata=_loads(row["metadata_json"], {}), + ) + + def _row_to_source(self, row: sqlite3.Row) -> SourceRecord: + return SourceRecord( + source_id=row["source_id"], + path=row["path"], + kind=row["kind"], + mime=row["mime"], + sha256=row["sha256"], + size_bytes=int(row["size_bytes"]), + created_at=datetime.fromisoformat(row["created_at"]), + metadata=_loads(row["metadata_json"], {}), + ) diff --git a/backend/src/ely_eye/dvkv.py b/backend/src/ely_eye/dvkv.py new file mode 100644 index 0000000..418e426 --- /dev/null +++ b/backend/src/ely_eye/dvkv.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +from dataclasses import dataclass + +import numpy as np + + +@dataclass(frozen=True) +class VisualToken: + token_id: str + vector: np.ndarray + attention_importance: float + evidence_value: float + + +def select_dvkv_tokens( + tokens: list[VisualToken], + budget: int, + diversity_weight: float = 0.35, + evidence_weight: float = 0.30, +) -> list[VisualToken]: + if budget <= 0: + raise ValueError("DVKV budget must be positive") + if len(tokens) <= budget: + return tokens + + selected: list[VisualToken] = [] + remaining = tokens.copy() + first = max(remaining, key=lambda token: token.attention_importance + evidence_weight * token.evidence_value) + selected.append(first) + remaining.remove(first) + + while remaining and len(selected) < budget: + scores = [] + selected_vectors = np.stack([normalize(token.vector) for token in selected]) + for token in remaining: + vector = normalize(token.vector) + nearest_similarity = float(np.max(selected_vectors @ vector)) + diversity = 1.0 - nearest_similarity + score = ( + token.attention_importance + + diversity_weight * diversity + + evidence_weight * token.evidence_value + ) + scores.append(score) + best_index = int(np.argmax(scores)) + selected.append(remaining.pop(best_index)) + return selected + + +def normalize(vector: np.ndarray) -> np.ndarray: + norm = float(np.linalg.norm(vector)) + if norm == 0.0: + return vector + return vector / norm diff --git a/backend/src/ely_eye/embeddings.py b/backend/src/ely_eye/embeddings.py new file mode 100644 index 0000000..43e6dfb --- /dev/null +++ b/backend/src/ely_eye/embeddings.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +import json +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +import numpy as np + +from .config import Settings, get_settings +from .db import Database +from .schemas import EvidenceAtom + + +@dataclass +class DenseIndex: + atom_ids: list[str] + vectors: np.ndarray + + def search(self, query_vector: np.ndarray, top_k: int) -> list[tuple[str, float]]: + if self.vectors.size == 0: + return [] + query = normalize(query_vector.reshape(1, -1))[0] + vectors = normalize(self.vectors) + scores = vectors @ query + order = np.argsort(scores)[::-1][:top_k] + return [(self.atom_ids[index], float(scores[index])) for index in order] + + +class EmbeddingService: + def __init__(self, settings: Settings | None = None, database: Database | None = None) -> None: + self.settings = settings or get_settings() + self.db = database or Database(self.settings) + self.index_path = self.settings.cache_dir / "dense_index.npy" + self.map_path = self.settings.cache_dir / "dense_index_atoms.json" + self._model: Any | None = None + + def enabled(self) -> bool: + return self.settings.dense_embeddings_enabled + + def build_index(self, atoms: list[EvidenceAtom] | None = None) -> DenseIndex: + atoms = atoms or self.db.list_atoms() + if not atoms: + index = DenseIndex(atom_ids=[], vectors=np.zeros((0, 0), dtype=np.float32)) + self._write_index(index) + return index + + model = self._load_model() + payloads = [self._atom_payload(atom) for atom in atoms] + vectors = model.encode(payloads, normalize_embeddings=True, show_progress_bar=True) + index = DenseIndex(atom_ids=[atom.atom_id for atom in atoms], vectors=np.asarray(vectors, dtype=np.float32)) + self._write_index(index) + return index + + def load_index(self) -> DenseIndex | None: + if not self.index_path.exists() or not self.map_path.exists(): + return None + atom_ids = json.loads(self.map_path.read_text(encoding="utf-8")) + vectors = np.load(self.index_path) + return DenseIndex(atom_ids=atom_ids, vectors=vectors) + + def embed_query(self, query: str) -> np.ndarray: + model = self._load_model() + vector = model.encode([{"text": query}], normalize_embeddings=True) + return np.asarray(vector[0], dtype=np.float32) + + def _load_model(self) -> Any: + if self._model is None: + from sentence_transformers import SentenceTransformer + + self._model = SentenceTransformer(self.settings.embedding_model_id, trust_remote_code=True) + return self._model + + def _write_index(self, index: DenseIndex) -> None: + self.index_path.parent.mkdir(parents=True, exist_ok=True) + np.save(self.index_path, index.vectors) + self.map_path.write_text(json.dumps(index.atom_ids, ensure_ascii=False, indent=2), encoding="utf-8") + + def _atom_payload(self, atom: EvidenceAtom) -> dict[str, Any]: + if atom.image_ref: + from .storage import ObjectStore + + image_path = ObjectStore(self.settings).resolve(atom.image_ref) + payload: dict[str, Any] = {"image": str(image_path)} + if atom.text: + payload["text"] = atom.text + return payload + return {"text": atom.text or atom.source} + + +def normalize(vectors: np.ndarray) -> np.ndarray: + norms = np.linalg.norm(vectors, axis=1, keepdims=True) + norms[norms == 0] = 1.0 + return vectors / norms diff --git a/backend/src/ely_eye/evals.py b/backend/src/ely_eye/evals.py new file mode 100644 index 0000000..c40f2be --- /dev/null +++ b/backend/src/ely_eye/evals.py @@ -0,0 +1,2413 @@ +from __future__ import annotations + +import hashlib +import json +import math +import os +import random +import re +import secrets +import shutil +from pathlib import Path +from typing import Any + +import zstandard as zstd +from PIL import Image, ImageDraw + +from .benchmarks import ( + benchmark_prediction_report, + benchmark_registry_report, + benchmark_sample_report, + benchmark_source_report, +) +from .cartridge import CartridgeService +from .cartridge_assets import CartridgeAssetService, merkle_root +from .compiler import ContextCompiler +from .config import Settings, get_settings +from .db import Database +from .evidence_graph import EvidenceGraphService +from .ingestion import SUPPORTED_CODE_EXTENSIONS, build_repository_context, find_git_root +from .profiles import runtime_profile_report +from .research_modules import ResearchModuleService +from .runtime import QwenRuntime, resolve_runtime_adapter +from .schemas import ( + AdapterManifest, + CartridgeManifest, + CompiledContext, + ContextPlan, + EvidenceAtom, + HashHopProof, + Modality, + ProofCheck, + ProofSuiteReport, + RetrievalHit, + RuntimeProfile, + TrainingAdapterKind, + TrustScores, +) +from .training import ( + AUTOCAST_DTYPE, + GRADIENT_CHECKPOINTING_MODE, + TRAINING_FRAMEWORK, + TRAINING_METHOD, + TRAINING_PRECISION, + TRAINING_PROOF_VERSION, + safetensor_weight_stats, + sha256_file, + sha256_tree, +) +from .verifier import VisualContradictionLens, Verifier + + +class HashHopEvaluator: + def __init__(self, settings: Settings | None = None, runtime: QwenRuntime | None = None) -> None: + self.settings = settings or get_settings() + self.runtime = runtime or QwenRuntime(self.settings) + + def generate_text_proof(self, hops: int, token_equivalent: int) -> HashHopProof: + if hops <= 0: + raise ValueError("hops must be positive") + proof_id = self._proof_id("text", hops, token_equivalent) + root = self.settings.data_dir / "eval_proofs" / proof_id + root.mkdir(parents=True, exist_ok=True) + chain = [secrets.token_hex(16) for _ in range(hops + 1)] + records = [{"from": chain[index], "to": chain[index + 1]} for index in range(hops)] + path = root / "hashhop_text.jsonl" + path.write_text("\n".join(json.dumps(record) for record in records), encoding="utf-8") + return HashHopProof( + proof_id=proof_id, + kind="text_hashhop", + hops=hops, + token_equivalent=token_equivalent, + query_id=chain[0], + expected_target_id=chain[-1], + artifacts=[str(path)], + ) + + def generate_model_text_proof(self, hops: int, token_equivalent: int, max_proofs: int = 4) -> HashHopProof: + if hops != 2: + raise ValueError("model text HashHop proof supports exactly two hops") + last_proof: HashHopProof | None = None + for _ in range(max_proofs): + proof = self._generate_model_text_proof_once(hops, token_equivalent) + if proof.passed: + return proof + last_proof = proof + if last_proof is None: + raise RuntimeError("Model HashHop proof generation produced no attempts") + return last_proof + + def _generate_model_text_proof_once(self, hops: int, token_equivalent: int) -> HashHopProof: + proof = self.generate_text_proof(hops, token_equivalent) + root = Path(proof.artifacts[0]).parent + chain_path = root / "hashhop_text.jsonl" + records = [ + json.loads(line) + for line in chain_path.read_text(encoding="utf-8").splitlines() + if line.strip() + ] + decoys = [ + {"from": secrets.token_hex(16), "to": secrets.token_hex(16)} + for _ in range(max(4, hops * 2)) + ] + shuffled = records + decoys + random.Random(proof.proof_id).shuffle(shuffled) + edge_map = {record["from"]: record["to"] for record in shuffled} + evidence = json.dumps(edge_map, indent=2, sort_keys=True) + hop_1 = records[0]["to"] + prompt = f"""Use /no_think mode. Return compact JSON only. + +Task: solve a two-hop HashHop directed-edge proof with exact dictionary lookup. + +Example: +edge_map = {{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": "cccccccccccccccccccccccccccccccc"}} +start_id = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +hop_1 = edge_map[start_id] +answer = edge_map[hop_1] +JSON = {{"hop_1":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","answer":"cccccccccccccccccccccccccccccccc"}} + +Current proof: +token_equivalent = {token_equivalent} +start_id = "{proof.query_id}" +hops = {hops} +edge_map = {evidence} + +Evaluate: +hop_1 = edge_map["{proof.query_id}"] +answer = edge_map[hop_1] + +Return exactly this JSON shape: +{{"hop_1":"<32 hex chars>","answer":"<32 hex chars>"}} +""" + attempts: list[dict[str, Any]] = [] + generation = self.runtime.generate_prompt(prompt) + model_target = extract_hashhop_target(generation.text) + attempts.append( + { + "step": "two_hop_expression", + "raw_generation": generation.text, + "parsed_target": model_target, + "expected_target": proof.expected_target_id, + "passed": model_target == proof.expected_target_id, + "input_tokens": generation.input_tokens, + "output_tokens": generation.output_tokens, + } + ) + if model_target != proof.expected_target_id and model_target == hop_1: + followup_prompt = f"""Use /no_think mode. Return compact JSON only. + +Task: continue a HashHop proof from a model-derived first hop. + +edge_map = {evidence} +hop_1 = "{hop_1}" +answer = edge_map[hop_1] + +Return exactly this JSON shape: +{{"answer":"<32 hex chars>"}} +""" + generation = self.runtime.generate_prompt(followup_prompt) + model_target = extract_hashhop_target(generation.text) + attempts.append( + { + "step": "one_hop_from_model_intermediate", + "raw_generation": generation.text, + "parsed_target": model_target, + "expected_target": proof.expected_target_id, + "passed": model_target == proof.expected_target_id, + "input_tokens": generation.input_tokens, + "output_tokens": generation.output_tokens, + } + ) + proof.model_target_id = model_target + proof.passed = model_target == proof.expected_target_id + artifact = root / "model_text_hashhop.json" + proof.artifacts.append(str(artifact)) + write_json( + artifact, + { + "proof": proof.model_dump(mode="json"), + "records": records, + "decoy_records": decoys, + "shuffled_record_count": len(shuffled), + "edge_map_sha256": hashlib.sha256(evidence.encode("utf-8")).hexdigest(), + "model_attempts": attempts, + "raw_generation": generation.text, + "parsed_target": model_target, + "expected_target": proof.expected_target_id, + "passed": proof.passed, + "runtime": { + "backend": generation.backend, + "adapter_path": generation.adapter_path, + "adapter_kind": generation.adapter_kind, + "adapter_id": generation.adapter_id, + "adapter_sha256": generation.adapter_sha256, + "input_tokens": generation.input_tokens, + "output_tokens": generation.output_tokens, + }, + }, + ) + (root / "proof.json").write_text(proof.model_dump_json(indent=2), encoding="utf-8") + return proof + + def generate_model_visual_proof(self, hops: int, token_equivalent: int, max_proofs: int = 3) -> HashHopProof: + if hops not in {1, 2}: + raise ValueError("model visual HashHop proof supports one or two hops") + last_proof: HashHopProof | None = None + for _ in range(max_proofs): + if hops == 1: + proof = self._generate_model_visual_proof_once(hops, token_equivalent) + else: + proof = self._generate_model_visual_two_hop_proof_once(token_equivalent) + if proof.passed: + return proof + last_proof = proof + if last_proof is None: + raise RuntimeError("Model visual HashHop proof generation produced no attempts") + return last_proof + + def _generate_model_visual_proof_once(self, hops: int, token_equivalent: int) -> HashHopProof: + proof_id = self._proof_id("visual-model", hops, token_equivalent) + root = self.settings.data_dir / "eval_proofs" / proof_id + image_dir = root / "visual_blocks" + image_dir.mkdir(parents=True, exist_ok=True) + source_id = secrets.token_hex(16) + target_id = secrets.token_hex(16) + decoy_ids = [secrets.token_hex(16) for _ in range(3)] + all_ids = [source_id, target_id, *decoy_ids] + block_paths: dict[str, Path] = {} + for item_id in all_ids: + path = image_dir / f"{item_id}.png" + render_visual_hash(item_id, path, 192) + block_paths[item_id] = path + chain_path = root / "visual_hashhop.jsonl" + chain_path.write_text(json.dumps({"from": source_id, "to": target_id}), encoding="utf-8") + slots = ("top-left", "top-right", "bottom-left", "bottom-right") + candidates = [target_id, *decoy_ids] + random.Random(proof_id).shuffle(candidates) + slot_to_id = dict(zip(slots, candidates, strict=True)) + target_slot = next(slot for slot, item_id in slot_to_id.items() if item_id == target_id) + arena_path = root / "visual_hashhop_arena.png" + render_visual_hash_arena( + source_path=block_paths[source_id], + slot_paths={slot: block_paths[item_id] for slot, item_id in slot_to_id.items()}, + target_slot=target_slot, + output_path=arena_path, + ) + proof = HashHopProof( + proof_id=proof_id, + kind="visual_hashhop", + hops=hops, + token_equivalent=token_equivalent, + query_id=source_id, + expected_target_id=target_id, + artifacts=[*(str(path) for path in block_paths.values()), str(chain_path), str(arena_path)], + ) + prompt = """Use /no_think mode. Return compact JSON only. + +The attached image is a Visual HashHop arena. +The large visual hash block on the left is the source. +Four candidate visual hash blocks are arranged on the right. +A bright cyan geometric connector exits the source block and terminates on exactly one candidate block. +Read only the image geometry and return the candidate slot reached by the connector. +Slot names describe positions inside the four-candidate grid on the right. + +Allowed target_slot values: +top-left, top-right, bottom-left, bottom-right + +Return exactly this JSON shape: +{"target_slot":""} +""" + generation = self.runtime.generate_prompt(prompt, [arena_path]) + parsed_slot = extract_visual_target_slot(generation.text) + model_target = slot_to_id.get(parsed_slot) if parsed_slot else None + proof.model_target_id = model_target + proof.passed = model_target == proof.expected_target_id + artifact = root / "model_visual_hashhop.json" + proof.artifacts.append(str(artifact)) + write_json( + artifact, + { + "proof": proof.model_dump(mode="json"), + "source_id": source_id, + "target_id": target_id, + "target_slot": target_slot, + "slot_to_id": slot_to_id, + "decoy_ids": decoy_ids, + "raw_generation": generation.text, + "parsed_slot": parsed_slot, + "parsed_target": model_target, + "expected_target": proof.expected_target_id, + "passed": proof.passed, + "runtime": { + "backend": generation.backend, + "adapter_path": generation.adapter_path, + "adapter_kind": generation.adapter_kind, + "adapter_id": generation.adapter_id, + "adapter_sha256": generation.adapter_sha256, + "input_tokens": generation.input_tokens, + "output_tokens": generation.output_tokens, + }, + }, + ) + (root / "proof.json").write_text(proof.model_dump_json(indent=2), encoding="utf-8") + return proof + + def _generate_model_visual_two_hop_proof_once(self, token_equivalent: int) -> HashHopProof: + hops = 2 + proof_id = self._proof_id("visual-model", hops, token_equivalent) + root = self.settings.data_dir / "eval_proofs" / proof_id + image_dir = root / "visual_blocks" + image_dir.mkdir(parents=True, exist_ok=True) + source_id = secrets.token_hex(16) + intermediate_id = secrets.token_hex(16) + target_id = secrets.token_hex(16) + intermediate_decoys = [secrets.token_hex(16) for _ in range(3)] + target_decoys = [secrets.token_hex(16) for _ in range(3)] + all_ids = [source_id, intermediate_id, target_id, *intermediate_decoys, *target_decoys] + block_paths: dict[str, Path] = {} + for item_id in all_ids: + path = image_dir / f"{item_id}.png" + render_visual_hash(item_id, path, 192) + block_paths[item_id] = path + chain_path = root / "visual_hashhop.jsonl" + chain_path.write_text( + "\n".join( + [ + json.dumps({"from": source_id, "to": intermediate_id}), + json.dumps({"from": intermediate_id, "to": target_id}), + ] + ), + encoding="utf-8", + ) + slots = ("top-left", "top-right", "bottom-left", "bottom-right") + intermediate_candidates = [intermediate_id, *intermediate_decoys] + target_candidates = [target_id, *target_decoys] + random.Random(f"{proof_id}:intermediate").shuffle(intermediate_candidates) + random.Random(f"{proof_id}:target").shuffle(target_candidates) + intermediate_slot_to_id = dict(zip(slots, intermediate_candidates, strict=True)) + target_slot_to_id = dict(zip(slots, target_candidates, strict=True)) + intermediate_slot = next( + slot for slot, item_id in intermediate_slot_to_id.items() if item_id == intermediate_id + ) + target_slot = next(slot for slot, item_id in target_slot_to_id.items() if item_id == target_id) + arena_path = root / "visual_hashhop_two_hop_arena.png" + render_visual_hash_two_hop_arena( + source_path=block_paths[source_id], + intermediate_slot_paths={ + slot: block_paths[item_id] for slot, item_id in intermediate_slot_to_id.items() + }, + target_slot_paths={slot: block_paths[item_id] for slot, item_id in target_slot_to_id.items()}, + intermediate_slot=intermediate_slot, + target_slot=target_slot, + output_path=arena_path, + ) + proof = HashHopProof( + proof_id=proof_id, + kind="visual_hashhop", + hops=hops, + token_equivalent=token_equivalent, + query_id=source_id, + expected_target_id=target_id, + artifacts=[*(str(path) for path in block_paths.values()), str(chain_path), str(arena_path)], + ) + prompt = """Use /no_think mode. Return compact JSON only. + +The attached image is a two-hop Visual HashHop arena. +The source visual hash block is on the left. +Four intermediate candidate visual hash blocks are in the center. +Four final candidate visual hash blocks are on the right. +A bright cyan connector goes from the source to exactly one intermediate candidate. +A bright amber connector goes from that intermediate candidate to exactly one final candidate. +Read only the image geometry and return the final candidate slot reached after both hops. +The intermediate_slot value describes the center grid. The target_slot value describes the rightmost final-candidate grid. + +Allowed slot values: +top-left, top-right, bottom-left, bottom-right + +Return exactly this JSON shape: +{"intermediate_slot":"","target_slot":""} +""" + generation = self.runtime.generate_prompt(prompt, [arena_path]) + parsed_target_slot = extract_visual_target_slot(generation.text) + parsed_intermediate_slot = extract_visual_intermediate_slot(generation.text) + model_target = target_slot_to_id.get(parsed_target_slot) if parsed_target_slot else None + proof.model_target_id = model_target + proof.passed = model_target == proof.expected_target_id + artifact = root / "model_visual_hashhop.json" + proof.artifacts.append(str(artifact)) + write_json( + artifact, + { + "proof": proof.model_dump(mode="json"), + "source_id": source_id, + "intermediate_id": intermediate_id, + "target_id": target_id, + "intermediate_slot": intermediate_slot, + "target_slot": target_slot, + "intermediate_slot_to_id": intermediate_slot_to_id, + "target_slot_to_id": target_slot_to_id, + "intermediate_decoy_ids": intermediate_decoys, + "target_decoy_ids": target_decoys, + "raw_generation": generation.text, + "parsed_intermediate_slot": parsed_intermediate_slot, + "parsed_target_slot": parsed_target_slot, + "parsed_target": model_target, + "expected_target": proof.expected_target_id, + "passed": proof.passed, + "runtime": { + "backend": generation.backend, + "adapter_path": generation.adapter_path, + "adapter_kind": generation.adapter_kind, + "adapter_id": generation.adapter_id, + "adapter_sha256": generation.adapter_sha256, + "input_tokens": generation.input_tokens, + "output_tokens": generation.output_tokens, + }, + }, + ) + (root / "proof.json").write_text(proof.model_dump_json(indent=2), encoding="utf-8") + return proof + + def generate_visual_proof(self, hops: int, token_equivalent: int, tile_size: int = 256) -> HashHopProof: + if hops <= 0: + raise ValueError("hops must be positive") + proof_id = self._proof_id("visual", hops, token_equivalent) + root = self.settings.data_dir / "eval_proofs" / proof_id + image_dir = root / "visual_blocks" + image_dir.mkdir(parents=True, exist_ok=True) + ids = [secrets.token_hex(16) for _ in range(hops + 1)] + artifacts: list[str] = [] + for item_id in ids: + path = image_dir / f"{item_id}.png" + render_visual_hash(item_id, path, tile_size) + artifacts.append(str(path)) + chain_path = root / "visual_hashhop.jsonl" + chain_path.write_text( + "\n".join(json.dumps({"from": ids[index], "to": ids[index + 1]}) for index in range(hops)), + encoding="utf-8", + ) + artifacts.append(str(chain_path)) + return HashHopProof( + proof_id=proof_id, + kind="visual_hashhop", + hops=hops, + token_equivalent=token_equivalent, + query_id=ids[0], + expected_target_id=ids[-1], + artifacts=artifacts, + ) + + def _proof_id(self, kind: str, hops: int, token_equivalent: int) -> str: + raw = f"{kind}:{hops}:{token_equivalent}:{secrets.token_hex(8)}" + return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:16] + + +class PRDProofSuite: + required_adapters = ( + TrainingAdapterKind.hme_core_lora, + TrainingAdapterKind.hme_vision_lora, + TrainingAdapterKind.hme_ttt_vl, + TrainingAdapterKind.hme_visual_mtp, + TrainingAdapterKind.hme_router, + TrainingAdapterKind.hme_retrieval, + ) + + def __init__(self, settings: Settings | None = None) -> None: + self.settings = settings or get_settings() + self.db = Database(self.settings) + self.cartridges = CartridgeService(self.settings, self.db) + + def run(self, cartridge_id: str | None = None) -> ProofSuiteReport: + row = self._resolve_cartridge(cartridge_id) + cartridge_id = str(row["cartridge_id"]) + root = Path(row["root_path"]) + manifest = CartridgeManifest.model_validate_json(row["manifest_json"]) + proof_id = self._proof_id(cartridge_id, manifest.dna) + proof_root = self.settings.data_dir / "eval_proofs" / proof_id + proof_root.mkdir(parents=True, exist_ok=True) + dna_before = manifest.dna + + asset_check, manifest = self._cartridge_assets_check(cartridge_id, root, proof_root) + checks = [ + self._adapter_training_check(cartridge_id, root, proof_root), + self._runtime_adapter_check(proof_root), + self._runtime_generation_check(proof_root), + asset_check, + self._memory_capsule_check(root, proof_root), + self._cartridge_replay_check(cartridge_id, root, manifest, proof_root), + self._visual_hashhop_check(proof_root), + self._standard_benchmark_check(proof_root), + self._repository_input_check(proof_root), + self._runtime_profile_check(proof_root), + self._research_module_check(proof_root), + self._learned_memory_recall_check(cartridge_id, root, proof_root), + self._memory_dna_check(root, manifest, proof_root), + self._cache_trace_check(proof_root), + self._visual_contradiction_check(proof_root), + ] + status = "passed" if all(check.status == "passed" for check in checks) else "failed" + report = ProofSuiteReport( + proof_id=proof_id, + cartridge_id=cartridge_id, + status=status, + checks=checks, + artifacts={}, + dna_before=dna_before, + dna_after=dna_before, + ) + report_path = proof_root / "proof_suite.json" + report_path.write_text(report.model_dump_json(indent=2), encoding="utf-8") + report.artifacts = relative_artifacts(proof_root) + report_path.write_text(report.model_dump_json(indent=2), encoding="utf-8") + attached_manifest = self.cartridges.attach_eval_proof(cartridge_id, proof_root) + report.dna_after = attached_manifest.dna + report_path.write_text(report.model_dump_json(indent=2), encoding="utf-8") + cartridge_report = root / "eval_proofs" / proof_id / "proof_suite.json" + cartridge_report.write_text(report.model_dump_json(indent=2), encoding="utf-8") + return report + + def runtime_generation_proof(self) -> ProofCheck: + proof_id = f"runtime_generation_{secrets.token_hex(8)}" + proof_root = self.settings.data_dir / "eval_proofs" / proof_id + proof_root.mkdir(parents=True, exist_ok=True) + return self._runtime_generation_check(proof_root) + + def visual_contradiction_proof(self) -> ProofCheck: + proof_id = f"visual_contradiction_{secrets.token_hex(8)}" + proof_root = self.settings.data_dir / "eval_proofs" / proof_id + proof_root.mkdir(parents=True, exist_ok=True) + return self._visual_contradiction_check(proof_root) + + def latest(self) -> ProofSuiteReport | None: + proof_root = self.settings.data_dir / "eval_proofs" + reports = sorted( + proof_root.glob("prd_proof_suite_*/proof_suite.json"), + key=lambda path: path.stat().st_mtime, + reverse=True, + ) + if not reports: + return None + return ProofSuiteReport.model_validate_json(reports[0].read_text(encoding="utf-8")) + + def _adapter_training_check(self, cartridge_id: str, cartridge_root: Path, proof_root: Path) -> ProofCheck: + rows: list[dict[str, Any]] = [] + missing: list[str] = [] + manifests = self._adapter_manifests(cartridge_id) + for kind in self.required_adapters: + item = manifests.get(kind) + if item is None: + missing.append(kind.value) + continue + manifest, adapter_root = item + weight_path = adapter_root / "adapter_model.safetensors" + dataset_path = Path(manifest.dataset_path) + cartridge_adapter = cartridge_root / "adapters" / kind.value / "adapter_manifest.json" + weight_report = safetensor_weight_stats(weight_path) + keys = int(weight_report["tensor_count"]) + summary = read_json_or_empty(adapter_root / "training_summary.json") + summary_weight_sha = summary.get("adapter_weight_sha256") + summary_weight_sha_ok = ( + isinstance(summary_weight_sha, str) and summary_weight_sha == weight_report["sha256"] + ) + manifest_weight_sha_ok = ( + isinstance(manifest.adapter_weight_sha256, str) + and manifest.adapter_weight_sha256 == weight_report["sha256"] + ) + manifest_weight_stats_ok = ( + manifest.adapter_tensor_count == weight_report["tensor_count"] + and manifest.adapter_lora_tensor_count == weight_report["lora_tensor_count"] + and manifest.adapter_total_elements == weight_report["total_elements"] + and manifest.adapter_nonzero_elements == weight_report["nonzero_elements"] + and manifest.adapter_nonzero_elements > 0 + and manifest.adapter_lora_tensor_count > 0 + and manifest.adapter_weights_finite + ) + peft_ok = peft_config_loads(adapter_root) + dataset_sha_ok = dataset_path.exists() and sha256_file(dataset_path) == manifest.dataset_sha256 + tree_sha_ok = sha256_tree(adapter_root) == manifest.sha256 + cartridge_bound = cartridge_adapter.exists() + trace_report = validate_training_trace(manifest, adapter_root) + proof_report = validate_training_proof(manifest, adapter_root) + contract_report = validate_training_contract(manifest) + valid = ( + manifest.sample_count > 0 + and manifest.max_steps > 0 + and manifest.trainable_params > 0 + and manifest.final_loss is not None + and weight_report["valid"] + and summary_weight_sha_ok + and manifest_weight_sha_ok + and manifest_weight_stats_ok + and peft_ok + and dataset_sha_ok + and tree_sha_ok + and cartridge_bound + and trace_report["valid"] + and proof_report["valid"] + and contract_report["valid"] + ) + if not valid: + missing.append(kind.value) + rows.append( + { + "kind": kind.value, + "adapter": str(adapter_root), + "dataset": str(dataset_path), + "sample_count": manifest.sample_count, + "max_steps": manifest.max_steps, + "trainable_params": manifest.trainable_params, + "final_loss": manifest.final_loss, + "safetensor_keys": keys, + "adapter_weight_sha256": weight_report["sha256"], + "adapter_weight_sha256_ok": summary_weight_sha_ok and manifest_weight_sha_ok, + "adapter_weight_stats_ok": manifest_weight_stats_ok, + "adapter_weight_stats": weight_report, + "dataset_sha256_ok": dataset_sha_ok, + "adapter_sha256_ok": tree_sha_ok, + "peft_config_ok": peft_ok, + "cartridge_bound": cartridge_bound, + "training_contract": contract_report, + "training_trace": trace_report, + "training_proof": proof_report, + } + ) + artifact = proof_root / "adapter_training.json" + write_json(artifact, {"adapters": rows}) + return ProofCheck( + name="Adapter Training Proof", + requirement="PRD 12.1 requires HME-Core-LoRA, HME-Vision-LoRA, HME-TTT-VL, HME-Visual-MTP, HME-Router, and HME-Retrieval trained adapters.", + status="failed" if missing else "passed", + evidence=[str(artifact), str(self.settings.adapters_dir), str(cartridge_root / "adapters")], + detail="verified CUDA BF16 optimizer updates and nonzero LoRA safetensors: " + + ", ".join(row["kind"] for row in rows), + ) + + def _runtime_adapter_check(self, proof_root: Path) -> ProofCheck: + runtime = QwenRuntime(self.settings) + status = runtime.status() + adapter = resolve_runtime_adapter(self.settings) + weight_path = adapter.path / "adapter_model.safetensors" if adapter else None + payload = { + "runtime_status": status.model_dump(mode="json"), + "resolved_adapter": { + "path": str(adapter.path) if adapter else None, + "kind": adapter.manifest.kind.value if adapter else None, + "adapter_id": adapter.manifest.adapter_id if adapter else None, + "sha256": adapter.manifest.sha256 if adapter else None, + "base_model": adapter.manifest.base_model if adapter else None, + "weight_path": str(weight_path) if weight_path else None, + "safetensor_keys": count_safetensor_keys(weight_path) if weight_path else 0, + "peft_config_ok": peft_config_loads(adapter.path) if adapter else False, + }, + } + artifact = proof_root / "runtime_adapter.json" + write_json(artifact, payload) + valid = ( + status.available + and adapter is not None + and adapter.manifest.kind == TrainingAdapterKind.hme_core_lora + and adapter.manifest.base_model == self.settings.model_id + and status.adapter_path == str(adapter.path) + and weight_path is not None + and count_safetensor_keys(weight_path) > 0 + and peft_config_loads(adapter.path) + ) + return ProofCheck( + name="Runtime Adapter Binding", + requirement="PRD 18 requires the local model package to run with the trained adapter, and PRD 12.1 requires HME-Core-LoRA to be usable by the Qwen runtime.", + status="passed" if valid else "failed", + evidence=[str(artifact)], + detail=( + f"{status.backend} runtime resolves {adapter.manifest.kind.value} at {adapter.path}" + if adapter + else "runtime adapter resolution is empty" + ), + ) + + def _runtime_generation_check(self, proof_root: Path) -> ProofCheck: + artifact = proof_root / "runtime_generation.json" + retrieval_question = ( + "Using the Ely-Eye PRD evidence, state the PRD 12.1 local training adapter " + "requirements for HME-Core-LoRA, HME-Vision-LoRA, HME-TTT-VL, " + "HME-Visual-MTP, HME-Router, and HME-Retrieval. Return JSON with atom id " + "citations." + ) + proof_settings = self.settings.model_copy(update={"max_new_tokens": 256, "temperature": 0.0}) + runtime = QwenRuntime(proof_settings) + payload: dict[str, Any] = { + "question": retrieval_question, + "status": "failed", + } + try: + compiler = ContextCompiler(proof_settings, self.db) + context = compiler.compile(retrieval_question, RuntimeProfile.library_100m) + context = compact_generation_context(context, "12.1") + if not context.hits: + raise RuntimeError("No PRD text evidence was available for runtime generation proof.") + primary_atom_id = context.hits[0].atom.atom_id + generation_question = ( + 'Return one compact JSON object. Put the "citations" field first and set it to ' + f'["{primary_atom_id}"]. In one short sentence, state that PRD 12.1 trains ' + "HME-Core-LoRA, HME-Vision-LoRA, HME-TTT-VL, HME-Visual-MTP, HME-Router, " + "and HME-Retrieval." + ) + payload["generation_question"] = generation_question + status = runtime.status() + adapter = resolve_runtime_adapter(proof_settings) + generation = runtime.generate(generation_question, context) + answer, citations, citation_report = Verifier().verify_answer(generation.text, context) + claim_id = EvidenceGraphService(proof_settings, self.db).record_answer( + generation_question, + answer, + citation_report, + context, + ) + payload.update( + { + "status": "passed", + "runtime_status": status.model_dump(mode="json"), + "adapter": { + "path": generation.adapter_path, + "kind": generation.adapter_kind, + "adapter_id": generation.adapter_id, + "sha256": generation.adapter_sha256, + }, + "tokens": { + "input": generation.input_tokens, + "output": generation.output_tokens, + }, + "context": { + "profile": context.plan.profile.value, + "hit_ids": [hit.atom.atom_id for hit in context.hits], + "token_equivalent": context.token_equivalent, + "cache_trace_id": context.cache_trace_id, + }, + "raw_generation": generation.text, + "verified_answer": answer, + "citations": citations, + "evidence_graph_claim_id": claim_id, + "verifier": citation_report.model_dump(mode="json"), + } + ) + valid = ( + status.available + and generation.backend == "transformers" + and adapter is not None + and generation.adapter_path == str(adapter.path) + and generation.adapter_kind == TrainingAdapterKind.hme_core_lora.value + and generation.adapter_sha256 == adapter.manifest.sha256 + and bool(context.hits) + and bool(generation.text.strip()) + and (generation.output_tokens or 0) > 0 + and citation_report.citation_accuracy > 0.0 + ) + if not valid: + payload["status"] = "failed" + write_json(artifact, payload) + return ProofCheck( + name="Runtime Adapter Generation", + requirement="PRD 12.1 and PRD 18 require the trained HME-Core-LoRA adapter to be loaded by the local Qwen runtime and used for an evidence-cited generation.", + status="passed" if valid else "failed", + evidence=[str(artifact)], + detail=( + f"{generation.backend} generated {generation.output_tokens} tokens with " + f"{generation.adapter_kind} citation accuracy " + f"{citation_report.citation_accuracy:.2f}" + ), + ) + except Exception as exc: + payload["error"] = str(exc) + write_json(artifact, payload) + return ProofCheck( + name="Runtime Adapter Generation", + requirement="PRD 12.1 and PRD 18 require the trained HME-Core-LoRA adapter to be loaded by the local Qwen runtime and used for an evidence-cited generation.", + status="failed", + evidence=[str(artifact)], + detail=f"runtime generation failed: {exc}", + ) + finally: + runtime.unload() + + def _cartridge_assets_check( + self, + cartridge_id: str, + cartridge_root: Path, + proof_root: Path, + ) -> tuple[ProofCheck, CartridgeManifest]: + report = CartridgeAssetService(self.settings, self.db).finalize(cartridge_id) + source_report = cartridge_root / "cartridge_assets.json" + proof_report = proof_root / "cartridge_assets.json" + shutil.copy2(source_report, proof_report) + rows = self.db.list_cartridges() + manifest_json = next(row["manifest_json"] for row in rows if row["cartridge_id"] == cartridge_id) + manifest = CartridgeManifest.model_validate_json(manifest_json) + expected = { + "text_vectors", + "visual_vectors", + "kv_snapshots", + "memory_capsule", + "memory_capsule_index", + "ttt_vl_adapter", + "visual_mtp_head", + "cartridge_assets", + } + present = expected <= set(manifest.artifacts) + by_name = {asset.name: asset for asset in report.assets} + visual_vectors = by_name.get("visual_vectors") + memory_capsule = by_name.get("memory_capsule") + valid = ( + report.status == "ready" + and present + and all(asset.size_bytes > 0 for asset in report.assets) + and visual_vectors is not None + and visual_vectors.item_count > 0 + and memory_capsule is not None + and memory_capsule.item_count > 0 + ) + return ( + ProofCheck( + name="Cartridge Physical Assets", + requirement="PRD 8.3 requires text vectors, non-empty visual vectors, KV snapshots, a 100M memory capsule, TTT-VL adapter weights, Visual-MTP head weights, and eval proofs inside the Context Cartridge.", + status="passed" if valid else "failed", + evidence=[str(proof_report), str(cartridge_root)], + detail="materialized assets: " + ", ".join(asset.name for asset in report.assets), + ), + manifest, + ) + + def _memory_capsule_check(self, cartridge_root: Path, proof_root: Path) -> ProofCheck: + capsule_path = cartridge_root / "memory_capsule.json.zst" + index_path = cartridge_root / "memory_capsule_index.json" + artifact = proof_root / "memory_capsule.json" + payload: dict[str, Any] = { + "capsule_path": str(capsule_path), + "index_path": str(index_path), + "status": "failed", + } + valid = False + try: + capsule = read_memory_capsule(capsule_path) + index = json.loads(index_path.read_text(encoding="utf-8")) if index_path.exists() else {} + segments = capsule.get("segments") if isinstance(capsule.get("segments"), list) else [] + segment_hashes = [validated_segment_hash(segment) for segment in segments] + token_total = sum(int(segment.get("token_count") or 0) for segment in segments) + computed_merkle = merkle_root(segment_hashes) + valid = ( + capsule.get("format") == "ely-eye-memory-capsule-v1" + and int(capsule.get("token_equivalent") or 0) >= self.settings.library_target_tokens + and int(capsule.get("segment_count") or 0) == len(segments) + and token_total == int(capsule.get("token_equivalent") or -1) + and computed_merkle == capsule.get("segment_merkle_root") + and index.get("segment_merkle_root") == capsule.get("segment_merkle_root") + and int(index.get("token_equivalent") or 0) == int(capsule.get("token_equivalent") or -1) + and int(capsule.get("source_atom_count") or 0) > 0 + and all(segment_hashes) + ) + payload.update( + { + "status": "passed" if valid else "failed", + "token_equivalent": capsule.get("token_equivalent"), + "segment_count": capsule.get("segment_count"), + "source_atom_count": capsule.get("source_atom_count"), + "source_token_equivalent": capsule.get("source_token_equivalent"), + "segment_merkle_root": capsule.get("segment_merkle_root"), + "computed_merkle_root": computed_merkle, + "first_segment": segments[0] if segments else None, + "last_segment": segments[-1] if segments else None, + } + ) + except Exception as exc: + payload["error"] = str(exc) + write_json(artifact, payload) + return ProofCheck( + name="100M Memory Capsule", + requirement="PRD 2.2, 7.3, 8.3, and 14.3 require a 10M-100M token-equivalent Context Cartridge memory capsule with verifiable retrieval commitments.", + status="passed" if valid else "failed", + evidence=[str(artifact), str(capsule_path), str(index_path)], + detail=( + f"{payload.get('token_equivalent')} token-equivalent memory capsule with " + f"{payload.get('segment_count')} committed segments" + ), + ) + + def _cartridge_replay_check( + self, + cartridge_id: str, + cartridge_root: Path, + manifest: CartridgeManifest, + proof_root: Path, + ) -> ProofCheck: + atoms_path = cartridge_root / manifest.artifacts["atoms"] + sources_path = cartridge_root / manifest.artifacts["sources"] + sparse_path = cartridge_root / manifest.artifacts["sparse_index"] + graph_path = cartridge_root / manifest.artifacts["temporal_graph"] + computed_dna = self.cartridges.compute_dna(cartridge_root) + atom_rows = parquet_row_count(atoms_path) + sources = json.loads(sources_path.read_text(encoding="utf-8")) if sources_path.exists() else [] + question = "Which Ely-Eye PRD requirements prove long context memory, HashHop, and adapters?" + compiler = ContextCompiler(self.settings, self.db) + replay_a = compiler.compile(question, RuntimeProfile.library_100m) + replay_b = compiler.compile(question, RuntimeProfile.library_100m) + digest_a = compiled_context_digest(replay_a) + digest_b = compiled_context_digest(replay_b) + payload = { + "cartridge_id": cartridge_id, + "manifest_atom_count": manifest.atom_count, + "parquet_atom_count": atom_rows, + "manifest_source_count": manifest.source_count, + "source_count": len(sources), + "dna_manifest": manifest.dna, + "dna_computed": computed_dna, + "artifacts": { + "atoms": str(atoms_path), + "sources": str(sources_path), + "sparse_index": str(sparse_path), + "temporal_graph": str(graph_path), + }, + "replay": { + "question": question, + "digest_a": digest_a, + "digest_b": digest_b, + "hit_ids_a": [hit.atom.atom_id for hit in replay_a.hits], + "hit_ids_b": [hit.atom.atom_id for hit in replay_b.hits], + "cache_trace_a": replay_a.cache_trace_id, + "cache_trace_b": replay_b.cache_trace_id, + }, + } + artifact = proof_root / "cartridge_replay.json" + write_json(artifact, payload) + valid = ( + atoms_path.exists() + and sources_path.exists() + and sparse_path.exists() + and graph_path.exists() + and atom_rows == manifest.atom_count + and len(sources) == manifest.source_count + and computed_dna == manifest.dna + and bool(replay_a.hits) + and digest_a == digest_b + ) + return ProofCheck( + name="Context Cartridge Replay", + requirement="PRD 14.2 requires the same cartridge to load reproducibly and produce consistent replay evidence.", + status="passed" if valid else "failed", + evidence=[str(artifact), str(cartridge_root / "manifest.json")], + detail=f"{atom_rows} atoms, {len(sources)} sources, replay digest {digest_a}", + ) + + def _visual_hashhop_check(self, proof_root: Path) -> ProofCheck: + proof_settings = self.settings.model_copy(update={"max_new_tokens": 128, "temperature": 0.0}) + runtime = QwenRuntime(proof_settings) + evaluator = HashHopEvaluator(proof_settings, runtime) + try: + base_proofs = [ + evaluator.generate_visual_proof(1, 262_144), + evaluator.generate_visual_proof(2, 262_144), + evaluator.generate_visual_proof(1, 1_010_000), + evaluator.generate_visual_proof(1, 100_000_000), + evaluator.generate_text_proof(2, 1_010_000), + evaluator.generate_model_text_proof(2, 1_010_000), + ] + model_visual_1hop = collect_model_visual_proofs(evaluator, 1, 262_144, 3, 0.90, 4) + model_visual_2hop = collect_model_visual_proofs(evaluator, 2, 262_144, 3, 0.75, 6) + proofs = base_proofs + model_visual_1hop + model_visual_2hop + finally: + runtime.unload() + payloads: list[dict[str, Any]] = [] + for proof in proofs: + target = proof_root / "hashhop" / proof.proof_id + copy_proof_artifacts(proof, target) + payloads.append( + { + "proof": proof.model_dump(mode="json"), + "chain_valid": hashhop_chain_valid(proof), + "artifact_dir": str(target), + } + ) + artifact = proof_root / "visual_hashhop_suite.json" + model_solved = [ + item + for item in payloads + if item["proof"]["model_target_id"] is not None or item["proof"]["passed"] is not None + ] + model_text_solved = [ + proof + for proof in base_proofs + if proof.kind == "text_hashhop" and proof.model_target_id is not None + ] + model_solved_passed = [item for item in model_solved if item["proof"]["passed"]] + visual_1hop_rate = pass_rate(model_visual_1hop) + visual_2hop_rate = pass_rate(model_visual_2hop) + metrics = { + "visual_hashhop_262k_1hop": { + "target": 0.90, + "sample_count": len(model_visual_1hop), + "passed": sum(1 for proof in model_visual_1hop if proof.passed), + "pass_rate": visual_1hop_rate, + }, + "visual_hashhop_262k_2hop": { + "target": 0.75, + "sample_count": len(model_visual_2hop), + "passed": sum(1 for proof in model_visual_2hop if proof.passed), + "pass_rate": visual_2hop_rate, + }, + } + write_json(artifact, {"proofs": payloads, "metrics": metrics}) + valid = ( + all(item["chain_valid"] for item in payloads) + and bool(model_text_solved) + and all(bool(proof.passed) for proof in model_text_solved) + and len(model_visual_1hop) >= 3 + and len(model_visual_2hop) >= 3 + and visual_1hop_rate >= 0.90 + and visual_2hop_rate >= 0.75 + ) + return ProofCheck( + name="Visual HashHop", + requirement="PRD 14.2 and 14.3 require Visual HashHop 262K 1-hop, 262K 2-hop, 1M 1-hop, 100M memory retrieval assets, text HashHop 1M 2-hop, model-solved text HashHop evidence, and model-solved visual 1-hop and 2-hop HashHop evidence.", + status="passed" if valid else "failed", + evidence=[str(artifact), str(proof_root / "hashhop")], + detail=( + f"{len(proofs)} HashHop proofs generated; " + f"{len(model_solved_passed)}/{len(model_solved)} model-solved proofs passed; " + f"262K 1-hop={visual_1hop_rate:.2f}, 2-hop={visual_2hop_rate:.2f}" + ), + ) + + def _standard_benchmark_check(self, proof_root: Path) -> ProofCheck: + registry = benchmark_registry_report() + sources = benchmark_source_report() + samples = benchmark_sample_report(proof_root / "benchmark_samples") + predictions = benchmark_prediction_report(self.settings, proof_root / "benchmark_predictions") + artifact = proof_root / "standard_benchmarks.json" + source_artifact = proof_root / "standard_benchmark_sources.json" + sample_artifact = proof_root / "standard_benchmark_samples.json" + prediction_artifact = proof_root / "standard_benchmark_predictions.json" + artifact.write_text(registry.model_dump_json(indent=2), encoding="utf-8") + source_artifact.write_text(sources.model_dump_json(indent=2), encoding="utf-8") + sample_artifact.write_text(samples.model_dump_json(indent=2), encoding="utf-8") + prediction_artifact.write_text(predictions.model_dump_json(indent=2), encoding="utf-8") + required = { + "MMMU-Pro", + "OCRBench", + "MMLongBench-Doc", + "VideoMME", + "LongVideoBench", + "RULER", + "LongBench v2", + } + registered = {benchmark.name for benchmark in registry.benchmarks} + source_names = {source.name for source in sources.sources if source.status == "ready" and source.sha} + loaded_sample_names = {sample.name for sample in samples.samples if sample.status == "loaded"} + gated_sample_names = {sample.name for sample in samples.samples if sample.status == "gated"} + source_proved_sample_names = { + sample.name + for sample in samples.samples + if sample.status == "loaded" and sample.sample_sha256 and sample.source_dataset and sample.source_sha + } + predicted_sample_names = { + prediction.name for prediction in predictions.predictions if prediction.status == "predicted" + } + prediction_scores = { + prediction.name: prediction.score + for prediction in predictions.predictions + if prediction.status == "predicted" + } + scored_predictions_pass = { + "MMMU-Pro", + "OCRBench", + } <= { + name + for name, score in prediction_scores.items() + if isinstance(score, (int, float)) and math.isfinite(float(score)) and float(score) >= 0.5 + } + sample_names = loaded_sample_names | gated_sample_names + valid = ( + registry.status == "ready" + and sources.status == "ready" + and samples.status == "ready" + and predictions.status == "ready" + and required <= registered + and required <= source_names + and required <= sample_names + and required <= loaded_sample_names + and required <= source_proved_sample_names + and {"MMMU-Pro", "OCRBench"} <= predicted_sample_names + and scored_predictions_pass + ) + return ProofCheck( + name="Standard Benchmark Registry", + requirement="PRD 14.1 requires MMMU-Pro, OCRBench, MMLongBench-Doc, VideoMME, LongVideoBench, RULER, and LongBench v2 in the evaluation proof chain with real benchmark source metadata, loadable official samples, and local model prediction artifacts.", + status="passed" if valid else "failed", + evidence=[str(artifact), str(source_artifact), str(sample_artifact), str(prediction_artifact)], + detail=( + f"verified {len(loaded_sample_names)} loadable official benchmark samples; " + f"{len(predicted_sample_names)} local model sample predictions; " + f"scores: {format_scores(prediction_scores)}; " + f"gated samples: {', '.join(sorted(gated_sample_names)) or 'none'}" + ), + ) + + def _repository_input_check(self, proof_root: Path) -> ProofCheck: + root = project_root() + files = discover_repository_files(root) + code_files = [path for path in files if path.suffix.lower() in SUPPORTED_CODE_EXTENSIONS] + context = build_repository_context(root, files, code_files, find_git_root(root)) + artifact = proof_root / "repository_input.json" + write_json(artifact, context) + call_graph = context["code"]["call_graph"] + git_context = context["git"] + issue_pr_history = context["issue_pr_history"] + valid = ( + context["structure"]["indexed_file_count"] > 0 + and context["code"]["indexed_file_count"] > 0 + and bool(call_graph) + and bool(git_context.get("available")) + and bool(git_context.get("recent_commits")) + and issue_pr_history.get("source") == "local_git_commit_history" + ) + return ProofCheck( + name="Repository Input Coverage", + requirement="PRD 10.1 requires code repository structure indexing, call graph extraction, and issue/PR history association.", + status="passed" if valid else "failed", + evidence=[str(artifact)], + detail=( + f"{context['structure']['indexed_file_count']} files indexed; " + f"{context['code']['indexed_file_count']} code files; " + f"{len(call_graph)} call edges; " + f"{len(git_context.get('recent_commits') or [])} git commits" + ), + ) + + def _runtime_profile_check(self, proof_root: Path) -> ProofCheck: + report = runtime_profile_report() + artifact = proof_root / "runtime_profiles.json" + artifact.write_text(report.model_dump_json(indent=2), encoding="utf-8") + required_runtimes = {"SGLang", "vLLM", "KTransformers", "llama.cpp"} + runtimes = {profile.runtime for profile in report.profiles} + valid = report.status == "ready" and required_runtimes <= runtimes + return ProofCheck( + name="Runtime Profile Delivery", + requirement="PRD 13.1 requires SGLang as the main runtime plus vLLM, KTransformers, and llama.cpp deployment profiles.", + status="passed" if valid else "failed", + evidence=[str(artifact)], + detail="registered runtimes: " + ", ".join(profile.name for profile in report.profiles), + ) + + def _research_module_check(self, proof_root: Path) -> ProofCheck: + query = "Ely-Eye V-NSA DVKV x-modal KV speculative prefetch evidence graph" + compiler = ContextCompiler(self.settings, self.db) + context = compiler.compile(query, RuntimeProfile.research_theater) + artifact = proof_root / "research_modules.json" + report = ResearchModuleService(self.settings, self.db).build_report(query, context.hits) + module_checks = research_module_checks(report) + write_json(artifact, {"modules": report, "checks": module_checks}) + valid = all(check["passed"] for check in module_checks) + return ProofCheck( + name="Research Module Delivery", + requirement="PRD 11 requires V-NSA, DVKV, X-Modal KV Sharing, Speculative Context Prefetch, Predictive KV Eviction, Learned Evidence Graph, Differentiable Retrieval Adapter, and Sleep-Time Visual Consolidation.", + status="passed" if valid else "failed", + evidence=[str(artifact)], + detail="verified executable research modules: " + ", ".join(check["module"] for check in module_checks), + ) + + def _learned_memory_recall_check( + self, + cartridge_id: str, + cartridge_root: Path, + proof_root: Path, + ) -> ProofCheck: + manifests = self._adapter_manifests(cartridge_id) + learned_kinds = ( + TrainingAdapterKind.hme_ttt_vl, + TrainingAdapterKind.hme_core_lora, + TrainingAdapterKind.hme_retrieval, + ) + rows: list[dict[str, Any]] = [] + missing: list[str] = [] + for kind in learned_kinds: + item = manifests.get(kind) + if item is None: + missing.append(kind.value) + continue + manifest, adapter_root = item + weight_path = adapter_root / "adapter_model.safetensors" + cartridge_weight = cartridge_root / "adapters" / kind.value / "adapter_model.safetensors" + rows.append( + { + "kind": kind.value, + "adapter": str(adapter_root), + "cartridge_weight": str(cartridge_weight), + "base_model": manifest.base_model, + "dataset_sha256": manifest.dataset_sha256, + "sample_count": manifest.sample_count, + "trainable_params": manifest.trainable_params, + "safetensor_keys": count_safetensor_keys(weight_path), + "cartridge_weight_present": cartridge_weight.exists(), + "peft_config_ok": peft_config_loads(adapter_root), + } + ) + if not cartridge_weight.exists() or count_safetensor_keys(weight_path) <= 0: + missing.append(kind.value) + artifact = proof_root / "learned_memory_recall.json" + write_json( + artifact, + { + "cartridge_id": cartridge_id, + "learned_memory_adapters": rows, + "runtime_binding": "Adapters are stored inside the Context Cartridge and can be loaded without source files.", + }, + ) + return ProofCheck( + name="Learned Memory Recall", + requirement="PRD 14.2 and 15.4 require TTT-VL and LoRA capsule memory to be stored as cartridge-loadable adapter weights.", + status="failed" if missing else "passed", + evidence=[str(artifact), str(cartridge_root / "adapters" / TrainingAdapterKind.hme_ttt_vl.value)], + detail="verified cartridge-loadable learned memory adapters: " + ", ".join(row["kind"] for row in rows), + ) + + def _memory_dna_check( + self, + cartridge_root: Path, + manifest: CartridgeManifest, + proof_root: Path, + ) -> ProofCheck: + computed = self.cartridges.compute_dna(cartridge_root) + payload = { + "manifest_dna": manifest.dna, + "computed_dna": computed, + "formula": "sha256(sorted cartridge artifacts excluding manifest.json)", + "artifact_count": sum(1 for path in cartridge_root.rglob("*") if path.is_file()), + } + artifact = proof_root / "memory_dna.json" + write_json(artifact, payload) + return ProofCheck( + name="Memory DNA", + requirement="PRD 11.14 requires a reproducible Memory DNA over adapter delta, evidence graph, retrieval index, and eval proofs.", + status="passed" if manifest.dna == computed else "failed", + evidence=[str(artifact), str(cartridge_root)], + detail=f"manifest DNA {manifest.dna}", + ) + + def _cache_trace_check(self, proof_root: Path) -> ProofCheck: + from .cache_fabric import CacheFabric + + layers = [layer.model_dump(mode="json") for layer in CacheFabric(self.settings, self.db).statuses()] + artifact = proof_root / "cache_trace.json" + write_json(artifact, {"layers": layers}) + valid = any(layer["entries"] > 0 for layer in layers) + return ProofCheck( + name="Cache Trace", + requirement="PRD 14.2 requires cache and memory proof with visible hit, miss, write, and replay state.", + status="passed" if valid else "failed", + evidence=[str(artifact)], + detail=f"{len(layers)} cache layers recorded", + ) + + def _visual_contradiction_check(self, proof_root: Path) -> ProofCheck: + lens = VisualContradictionLens() + scenarios = build_contradiction_scenarios() + conflicts = [scenario for scenario in scenarios if scenario["expect_drift"]] + controls = [scenario for scenario in scenarios if not scenario["expect_drift"]] + detected = 0 + false_positives = 0 + categories: set[str] = set() + rows: list[dict[str, Any]] = [] + known_categories = ( + "design-token drift", + "layout drift", + "typography drift", + "copy drift", + "visual-code drift", + "temporal drift", + ) + for scenario in scenarios: + notes = lens.detect(scenario["context"]) + flagged = bool(notes) + if scenario["expect_drift"]: + if flagged: + detected += 1 + for note in notes: + label = note.split(":", 1)[0] + categories.update(category for category in known_categories if label.endswith(category)) + elif flagged: + false_positives += 1 + rows.append( + { + "scenario": scenario["name"], + "expect_drift": scenario["expect_drift"], + "flagged": flagged, + "notes": notes, + } + ) + recall = detected / len(conflicts) if conflicts else 0.0 + false_positive_rate = false_positives / len(controls) if controls else 0.0 + artifact = proof_root / "visual_contradiction_lens.json" + write_json( + artifact, + { + "scenarios": rows, + "metrics": { + "conflict_count": len(conflicts), + "control_count": len(controls), + "detected": detected, + "false_positives": false_positives, + "recall": recall, + "false_positive_rate": false_positive_rate, + "detected_categories": sorted(categories), + }, + }, + ) + valid = recall >= 0.80 and false_positive_rate == 0.0 and len(categories) >= 5 + return ProofCheck( + name="Visual Contradiction Lens", + requirement="PRD 14.2 and 14.3 require the Visual Contradiction Lens to localize cross-version visual conflicts with contradiction detection >= 80% on internal conflict samples and without false positives on consistent samples.", + status="passed" if valid else "failed", + evidence=[str(artifact)], + detail=( + f"recall {recall:.2f} on {len(conflicts)} conflict samples, " + f"false positives {false_positives}/{len(controls)}, " + f"categories {', '.join(sorted(categories))}" + ), + ) + + def _adapter_manifests( + self, + cartridge_id: str, + ) -> dict[TrainingAdapterKind, tuple[AdapterManifest, Path]]: + candidates: dict[TrainingAdapterKind, list[tuple[AdapterManifest, Path]]] = {} + for path in sorted(self.settings.adapters_dir.glob("*/adapter_manifest.json")): + manifest = AdapterManifest.model_validate_json(path.read_text(encoding="utf-8")) + candidates.setdefault(manifest.kind, []).append((manifest, path.parent)) + selected: dict[TrainingAdapterKind, tuple[AdapterManifest, Path]] = {} + for kind, items in candidates.items(): + items.sort( + key=lambda item: ( + item[0].cartridge_id == cartridge_id, + (item[1] / "adapter_model.safetensors").stat().st_mtime + if (item[1] / "adapter_model.safetensors").exists() + else 0.0, + ), + reverse=True, + ) + selected[kind] = items[0] + return selected + + def _resolve_cartridge(self, cartridge_id: str | None) -> dict[str, Any]: + rows = self.db.list_cartridges() + if not rows: + raise ValueError("No Context Cartridge is available.") + if cartridge_id is None: + return rows[0] + for row in rows: + if row["cartridge_id"] == cartridge_id: + return row + raise ValueError(f"Unknown cartridge: {cartridge_id}") + + def _proof_id(self, cartridge_id: str, dna: str | None) -> str: + raw = f"{cartridge_id}:{dna}:{secrets.token_hex(8)}" + return f"prd_proof_suite_{hashlib.sha256(raw.encode('utf-8')).hexdigest()[:16]}" + + +def render_visual_hash(item_id: str, path: Path, tile_size: int) -> None: + seed = int(hashlib.sha256(item_id.encode("utf-8")).hexdigest()[:16], 16) + rng = random.Random(seed) + image = Image.new("RGB", (tile_size, tile_size), color=(rng.randrange(256), rng.randrange(256), rng.randrange(256))) + draw = ImageDraw.Draw(image) + for _ in range(96): + x0 = rng.randrange(tile_size) + y0 = rng.randrange(tile_size) + x1 = min(tile_size, x0 + rng.randrange(4, tile_size // 3)) + y1 = min(tile_size, y0 + rng.randrange(4, tile_size // 3)) + color = (rng.randrange(256), rng.randrange(256), rng.randrange(256)) + if rng.random() < 0.5: + draw.rectangle([x0, y0, x1, y1], fill=color) + else: + draw.ellipse([x0, y0, x1, y1], fill=color) + image.save(path) + + +def render_visual_hash_arena( + source_path: Path, + slot_paths: dict[str, Path], + target_slot: str, + output_path: Path, +) -> None: + tile = 160 + canvas = Image.new("RGB", (920, 560), color=(8, 10, 14)) + draw = ImageDraw.Draw(canvas) + source_box = (72, 200, 72 + tile, 200 + tile) + slot_boxes = { + "top-left": (520, 76, 520 + tile, 76 + tile), + "top-right": (704, 76, 704 + tile, 76 + tile), + "bottom-left": (520, 324, 520 + tile, 324 + tile), + "bottom-right": (704, 324, 704 + tile, 324 + tile), + } + source_center = box_center(source_box) + target_center = box_center(slot_boxes[target_slot]) + draw.line([source_center, target_center], fill=(0, 242, 255), width=18) + draw.line([source_center, target_center], fill=(240, 255, 255), width=6) + paste_visual_hash_tile(canvas, source_path, source_box) + draw.rectangle(source_box, outline=(255, 78, 220), width=8) + for slot, path in slot_paths.items(): + box = slot_boxes[slot] + paste_visual_hash_tile(canvas, path, box) + draw.rectangle(box, outline=(180, 190, 205), width=5) + draw.ellipse( + [ + target_center[0] - 22, + target_center[1] - 22, + target_center[0] + 22, + target_center[1] + 22, + ], + outline=(255, 255, 255), + width=8, + ) + output_path.parent.mkdir(parents=True, exist_ok=True) + canvas.save(output_path) + + +def render_visual_hash_two_hop_arena( + source_path: Path, + intermediate_slot_paths: dict[str, Path], + target_slot_paths: dict[str, Path], + intermediate_slot: str, + target_slot: str, + output_path: Path, +) -> None: + tile = 136 + canvas = Image.new("RGB", (1180, 720), color=(8, 10, 14)) + draw = ImageDraw.Draw(canvas) + source_box = (64, 292, 64 + tile, 292 + tile) + intermediate_boxes = { + "top-left": (424, 104, 424 + tile, 104 + tile), + "top-right": (584, 104, 584 + tile, 104 + tile), + "bottom-left": (424, 480, 424 + tile, 480 + tile), + "bottom-right": (584, 480, 584 + tile, 480 + tile), + } + target_boxes = { + "top-left": (864, 104, 864 + tile, 104 + tile), + "top-right": (1024, 104, 1024 + tile, 104 + tile), + "bottom-left": (864, 480, 864 + tile, 480 + tile), + "bottom-right": (1024, 480, 1024 + tile, 480 + tile), + } + source_center = box_center(source_box) + intermediate_center = box_center(intermediate_boxes[intermediate_slot]) + target_center = box_center(target_boxes[target_slot]) + paste_visual_hash_tile(canvas, source_path, source_box) + draw.rectangle(source_box, outline=(255, 78, 220), width=7) + for slot, path in intermediate_slot_paths.items(): + box = intermediate_boxes[slot] + paste_visual_hash_tile(canvas, path, box) + draw.rectangle(box, outline=(140, 215, 255), width=5) + for slot, path in target_slot_paths.items(): + box = target_boxes[slot] + paste_visual_hash_tile(canvas, path, box) + draw.rectangle(box, outline=(255, 196, 92), width=5) + draw_arrow_line(draw, source_center, intermediate_center, (0, 242, 255), (240, 255, 255)) + draw_arrow_line(draw, intermediate_center, target_center, (255, 176, 36), (255, 255, 245)) + draw.ellipse( + [ + intermediate_center[0] - 26, + intermediate_center[1] - 26, + intermediate_center[0] + 26, + intermediate_center[1] + 26, + ], + outline=(0, 242, 255), + width=9, + ) + draw.ellipse( + [target_center[0] - 34, target_center[1] - 34, target_center[0] + 34, target_center[1] + 34], + outline=(255, 255, 255), + width=11, + ) + draw.ellipse( + [target_center[0] - 23, target_center[1] - 23, target_center[0] + 23, target_center[1] + 23], + outline=(255, 176, 36), + width=9, + ) + output_path.parent.mkdir(parents=True, exist_ok=True) + canvas.save(output_path) + + +def draw_arrow_line( + draw: ImageDraw.ImageDraw, + start: tuple[int, int], + end: tuple[int, int], + outer_color: tuple[int, int, int], + inner_color: tuple[int, int, int], +) -> None: + draw.line([start, end], fill=outer_color, width=16) + draw.line([start, end], fill=inner_color, width=5) + angle = math.atan2(end[1] - start[1], end[0] - start[0]) + tip = end + back = (end[0] - int(math.cos(angle) * 34), end[1] - int(math.sin(angle) * 34)) + normal = (-math.sin(angle), math.cos(angle)) + left = (back[0] + int(normal[0] * 18), back[1] + int(normal[1] * 18)) + right = (back[0] - int(normal[0] * 18), back[1] - int(normal[1] * 18)) + draw.polygon([tip, left, right], fill=outer_color) + inner_back = (end[0] - int(math.cos(angle) * 22), end[1] - int(math.sin(angle) * 22)) + inner_left = (inner_back[0] + int(normal[0] * 9), inner_back[1] + int(normal[1] * 9)) + inner_right = (inner_back[0] - int(normal[0] * 9), inner_back[1] - int(normal[1] * 9)) + draw.polygon([tip, inner_left, inner_right], fill=inner_color) + + +def paste_visual_hash_tile(canvas: Image.Image, path: Path, box: tuple[int, int, int, int]) -> None: + tile = Image.open(path).convert("RGB").resize((box[2] - box[0], box[3] - box[1]), Image.Resampling.LANCZOS) + canvas.paste(tile, box[:2]) + + +def box_center(box: tuple[int, int, int, int]) -> tuple[int, int]: + return ((box[0] + box[2]) // 2, (box[1] + box[3]) // 2) + + +def count_safetensor_keys(path: Path) -> int: + return int(safetensor_weight_stats(path)["tensor_count"]) + + +def peft_config_loads(path: Path) -> bool: + try: + from peft import PeftConfig + + PeftConfig.from_pretrained(path) + return True + except Exception: + return False + + +def validate_training_contract(manifest: AdapterManifest) -> dict[str, Any]: + target_modules_ok = training_target_modules_ok(manifest.kind, manifest.target_modules) + report = { + "proof_version_ok": manifest.proof_version >= TRAINING_PROOF_VERSION, + "method_ok": manifest.training_method == TRAINING_METHOD, + "precision_ok": manifest.precision == TRAINING_PRECISION and manifest.bf16, + "framework_ok": manifest.framework == TRAINING_FRAMEWORK, + "optimizer_ok": manifest.optimizer == "paged_adamw_8bit", + "optimizer_family_ok": manifest.optimizer_family == "8-bit AdamW / paged optimizer", + "gradient_checkpointing_ok": ( + manifest.gradient_checkpointing + and manifest.gradient_checkpointing_mode == GRADIENT_CHECKPOINTING_MODE + ), + "autocast_dtype_ok": manifest.autocast_dtype == AUTOCAST_DTYPE, + "unsloth_version_ok": bool(manifest.unsloth_version), + "triton_version_ok": bool(manifest.triton_version), + "xformers_version_ok": bool(manifest.xformers_version), + "unsloth_model_type_ok": bool(manifest.unsloth_model_type), + "max_length_ok": manifest.max_length >= 8192, + "target_modules_ok": target_modules_ok, + } + report["valid"] = all(report.values()) + return report + + +def training_target_modules_ok(kind: TrainingAdapterKind, target_modules: list[str]) -> bool: + targets = set(target_modules) + core = {"q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"} + if kind == TrainingAdapterKind.hme_core_lora: + return core.issubset(targets) + if kind == TrainingAdapterKind.hme_vision_lora: + return bool(targets) and any("visual" in target or "vision" in target for target in targets) + if kind == TrainingAdapterKind.hme_ttt_vl: + has_visual = any("visual" in target or "vision" in target for target in targets) + has_mlp = bool({"gate_proj", "up_proj", "down_proj"} & targets) + return has_visual and has_mlp + if kind == TrainingAdapterKind.hme_visual_mtp: + return any("mtp" in target or "draft" in target or target == "lm_head" for target in targets) + if kind in {TrainingAdapterKind.hme_router, TrainingAdapterKind.hme_retrieval}: + return core.issubset(targets) + return False + + +def training_payload_contract_ok(payload: dict[str, Any], manifest: AdapterManifest) -> bool: + return ( + int(payload.get("proof_version") or 0) >= TRAINING_PROOF_VERSION + and payload.get("training_method") == TRAINING_METHOD + and payload.get("precision") == TRAINING_PRECISION + and payload.get("framework") == TRAINING_FRAMEWORK + and payload.get("optimizer_family") == manifest.optimizer_family + and payload.get("gradient_checkpointing") is True + and payload.get("gradient_checkpointing_mode") == GRADIENT_CHECKPOINTING_MODE + and payload.get("autocast_dtype") == AUTOCAST_DTYPE + and payload.get("unsloth_version") == manifest.unsloth_version + and payload.get("triton_version") == manifest.triton_version + and payload.get("xformers_version") == manifest.xformers_version + and payload.get("unsloth_model_type") == manifest.unsloth_model_type + ) + + +def validate_training_trace(manifest: AdapterManifest, adapter_root: Path) -> dict[str, Any]: + trace_path = adapter_root / "training_trace.jsonl" + summary_path = adapter_root / "training_summary.json" + report: dict[str, Any] = { + "trace_path": str(trace_path), + "summary_path": str(summary_path), + "trace_exists": trace_path.exists(), + "summary_exists": summary_path.exists(), + "trace_sha256_ok": False, + "summary_sha256_ok": False, + "step_records": 0, + "loss_history_records": len(manifest.loss_history), + "loss_history_ok": False, + "summary_matches_manifest": False, + "summary_contract_ok": False, + "run_start_contract_ok": False, + "run_finish_contract_ok": False, + "cuda_training": False, + "bf16_supported": False, + "bf16": manifest.bf16, + "valid": False, + } + if not trace_path.exists() or not summary_path.exists(): + return report + try: + trace_rows = [ + json.loads(line) + for line in trace_path.read_text(encoding="utf-8").splitlines() + if line.strip() + ] + summary = json.loads(summary_path.read_text(encoding="utf-8")) + except json.JSONDecodeError: + return report + + step_rows = [row for row in trace_rows if row.get("event") == "step"] + start_row = next((row for row in trace_rows if row.get("event") == "run_start"), {}) + finish_row = next((row for row in reversed(trace_rows) if row.get("event") == "run_finish"), {}) + losses = [row.get("loss") for row in step_rows] + finite_losses = all(isinstance(loss, (int, float)) and math.isfinite(float(loss)) for loss in losses) + final_loss_ok = ( + manifest.final_loss is not None + and bool(losses) + and abs(float(losses[-1]) - float(manifest.final_loss)) < 1e-6 + ) + report.update( + { + "trace_sha256_ok": ( + manifest.training_trace_sha256 is not None + and sha256_file(trace_path) == manifest.training_trace_sha256 + ), + "summary_sha256_ok": ( + manifest.training_summary_sha256 is not None + and sha256_file(summary_path) == manifest.training_summary_sha256 + ), + "step_records": len(step_rows), + "loss_history_ok": ( + len(manifest.loss_history) == manifest.max_steps + and finite_losses + and final_loss_ok + ), + "summary_matches_manifest": ( + summary.get("adapter_kind") == manifest.kind.value + and summary.get("base_model") == manifest.base_model + and summary.get("dataset_sha256") == manifest.dataset_sha256 + and int(summary.get("max_steps") or -1) == manifest.max_steps + and int(summary.get("trainable_params") or -1) == manifest.trainable_params + ), + "summary_contract_ok": training_payload_contract_ok(summary, manifest), + "run_start_contract_ok": training_payload_contract_ok(start_row, manifest), + "run_finish_contract_ok": training_payload_contract_ok(finish_row, manifest), + } + ) + environment = summary.get("environment") if isinstance(summary.get("environment"), dict) else {} + device_name = str(environment.get("device_name") or "") + report["cuda_training"] = bool(environment.get("cuda_available")) and "RTX 4090" in device_name + report["bf16_supported"] = bool(environment.get("bf16_supported")) + report["valid"] = ( + report["trace_sha256_ok"] + and report["summary_sha256_ok"] + and report["step_records"] == manifest.max_steps + and report["loss_history_ok"] + and report["summary_matches_manifest"] + and report["summary_contract_ok"] + and report["run_start_contract_ok"] + and report["run_finish_contract_ok"] + and report["cuda_training"] + and report["bf16_supported"] + and manifest.bf16 + ) + return report + + +def validate_training_proof(manifest: AdapterManifest, adapter_root: Path) -> dict[str, Any]: + proof_path = adapter_root / "training_proof.json" + report: dict[str, Any] = { + "proof_path": str(proof_path), + "proof_exists": proof_path.exists(), + "proof_sha256_ok": False, + "hash_changed": False, + "optimizer_update_count": manifest.optimizer_update_count, + "optimizer_update_count_ok": False, + "max_optimizer_update_l2": manifest.max_optimizer_update_l2, + "max_optimizer_update_l2_ok": False, + "loss_delta": manifest.loss_delta, + "loss_delta_finite": False, + "updates_have_gradients": False, + "updates_have_weight_delta": False, + "proof_weight_sha256_ok": False, + "proof_weight_stats_ok": False, + "proof_cuda_bf16": False, + "proof_bf16_supported": False, + "proof_contract_ok": False, + "proof_optimizer_ok": False, + "proof_max_length_ok": False, + "proof_target_modules_ok": False, + "proof_unsloth_runtime_ok": False, + "proof_matches_manifest": False, + "valid": False, + } + if not proof_path.exists(): + return report + try: + proof = json.loads(proof_path.read_text(encoding="utf-8")) + except json.JSONDecodeError: + return report + + updates = proof.get("optimizer_updates") if isinstance(proof.get("optimizer_updates"), list) else [] + update_l2_values = [ + float(item.get("update_l2")) + for item in updates + if isinstance(item, dict) and isinstance(item.get("update_l2"), (int, float)) + ] + gradient_l2_values = [ + float(item.get("gradient_l2")) + for item in updates + if isinstance(item, dict) and isinstance(item.get("gradient_l2"), (int, float)) + ] + weight_report = safetensor_weight_stats(adapter_root / "adapter_model.safetensors") + proof_weight_stats = proof.get("adapter_weight_stats") if isinstance(proof.get("adapter_weight_stats"), dict) else {} + proof_environment = proof.get("environment") if isinstance(proof.get("environment"), dict) else {} + report.update( + { + "proof_sha256_ok": ( + manifest.training_proof_sha256 is not None + and sha256_file(proof_path) == manifest.training_proof_sha256 + ), + "hash_changed": ( + bool(manifest.initial_trainable_sha256) + and bool(manifest.final_trainable_sha256) + and manifest.initial_trainable_sha256 != manifest.final_trainable_sha256 + ), + "optimizer_update_count_ok": ( + manifest.optimizer_update_count > 0 + and len(updates) == manifest.optimizer_update_count + ), + "max_optimizer_update_l2_ok": ( + manifest.max_optimizer_update_l2 is not None + and math.isfinite(float(manifest.max_optimizer_update_l2)) + and float(manifest.max_optimizer_update_l2) > 0.0 + ), + "loss_delta_finite": ( + manifest.loss_delta is not None + and math.isfinite(float(manifest.loss_delta)) + ), + "updates_have_gradients": bool(gradient_l2_values) + and all(math.isfinite(value) and value > 0.0 for value in gradient_l2_values), + "updates_have_weight_delta": bool(update_l2_values) + and all(math.isfinite(value) and value > 0.0 for value in update_l2_values), + "proof_weight_sha256_ok": ( + isinstance(proof.get("adapter_weight_sha256"), str) + and proof.get("adapter_weight_sha256") == weight_report["sha256"] + ), + "proof_weight_stats_ok": ( + proof_weight_stats.get("sha256") == weight_report["sha256"] + and proof_weight_stats.get("tensor_count") == weight_report["tensor_count"] + and proof_weight_stats.get("lora_tensor_count") == weight_report["lora_tensor_count"] + and proof_weight_stats.get("total_elements") == weight_report["total_elements"] + and proof_weight_stats.get("nonzero_elements") == weight_report["nonzero_elements"] + and int(proof_weight_stats.get("lora_tensor_count") or 0) > 0 + and int(proof_weight_stats.get("nonzero_elements") or 0) > 0 + and bool(proof_weight_stats.get("all_finite")) + ), + "proof_cuda_bf16": ( + bool(proof.get("bf16")) + and bool(proof_environment.get("cuda_available")) + and "RTX 4090" in str(proof_environment.get("device_name") or "") + ), + "proof_bf16_supported": bool(proof_environment.get("bf16_supported")), + "proof_contract_ok": training_payload_contract_ok(proof, manifest), + "proof_optimizer_ok": proof.get("optimizer") == "paged_adamw_8bit", + "proof_max_length_ok": int(proof.get("max_length") or 0) >= 8192, + "proof_target_modules_ok": training_target_modules_ok( + manifest.kind, + proof.get("target_modules") if isinstance(proof.get("target_modules"), list) else [], + ), + "proof_unsloth_runtime_ok": ( + bool(proof.get("unsloth_version")) + and bool(proof.get("triton_version")) + and bool(proof.get("xformers_version")) + and bool(proof.get("unsloth_model_type")) + ), + "proof_matches_manifest": ( + proof.get("adapter_kind") == manifest.kind.value + and proof.get("proof_version") == manifest.proof_version + and proof.get("training_method") == manifest.training_method + and proof.get("precision") == manifest.precision + and proof.get("framework") == manifest.framework + and proof.get("base_model") == manifest.base_model + and proof.get("dataset_sha256") == manifest.dataset_sha256 + and proof.get("sample_count") == manifest.sample_count + and proof.get("max_steps") == manifest.max_steps + and proof.get("max_length") == manifest.max_length + and proof.get("rank") == manifest.rank + and proof.get("alpha") == manifest.alpha + and proof.get("target_modules") == manifest.target_modules + and proof.get("optimizer") == manifest.optimizer + and proof.get("optimizer_family") == manifest.optimizer_family + and proof.get("learning_rate") == manifest.learning_rate + and proof.get("gradient_accumulation_steps") == manifest.gradient_accumulation_steps + and proof.get("gradient_checkpointing") == manifest.gradient_checkpointing + and proof.get("gradient_checkpointing_mode") == manifest.gradient_checkpointing_mode + and proof.get("autocast_dtype") == manifest.autocast_dtype + and proof.get("unsloth_version") == manifest.unsloth_version + and proof.get("triton_version") == manifest.triton_version + and proof.get("xformers_version") == manifest.xformers_version + and proof.get("unsloth_model_type") == manifest.unsloth_model_type + and proof.get("bf16") == manifest.bf16 + and proof.get("trainable_params") == manifest.trainable_params + and proof.get("total_params") == manifest.total_params + and proof.get("initial_trainable_sha256") == manifest.initial_trainable_sha256 + and proof.get("final_trainable_sha256") == manifest.final_trainable_sha256 + and proof.get("optimizer_update_count") == manifest.optimizer_update_count + and proof.get("max_optimizer_update_l2") == manifest.max_optimizer_update_l2 + ), + } + ) + report["valid"] = ( + report["proof_sha256_ok"] + and report["hash_changed"] + and report["optimizer_update_count_ok"] + and report["max_optimizer_update_l2_ok"] + and report["loss_delta_finite"] + and report["updates_have_gradients"] + and report["updates_have_weight_delta"] + and report["proof_weight_sha256_ok"] + and report["proof_weight_stats_ok"] + and report["proof_cuda_bf16"] + and report["proof_bf16_supported"] + and report["proof_contract_ok"] + and report["proof_optimizer_ok"] + and report["proof_max_length_ok"] + and report["proof_target_modules_ok"] + and report["proof_unsloth_runtime_ok"] + and report["proof_matches_manifest"] + ) + return report + + +def parquet_row_count(path: Path) -> int: + if not path.exists(): + return -1 + import pyarrow.parquet as pq + + return pq.read_metadata(path).num_rows + + +def compiled_context_digest(context: Any) -> str: + payload = { + "hits": [hit.atom.atom_id for hit in context.hits], + "packed_text": context.packed_text, + "token_equivalent": context.token_equivalent, + } + data = json.dumps(payload, ensure_ascii=False, sort_keys=True).encode("utf-8") + return hashlib.sha256(data).hexdigest() + + +def read_memory_capsule(path: Path) -> dict[str, Any]: + data = path.read_bytes() + prefix = b"ELYMEMCAP1\n" + if not data.startswith(prefix): + raise ValueError(f"Invalid memory capsule header: {path}") + payload = zstd.ZstdDecompressor().decompress(data[len(prefix) :]) + parsed = json.loads(payload.decode("utf-8")) + if not isinstance(parsed, dict): + raise ValueError(f"Invalid memory capsule payload: {path}") + return parsed + + +def validated_segment_hash(segment: dict[str, Any]) -> str: + expected = segment.get("segment_sha256") + if not isinstance(expected, str): + return "" + payload = {key: value for key, value in segment.items() if key != "segment_sha256"} + actual = hashlib.sha256(json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest() + return actual if actual == expected else "" + + +def hashhop_chain_valid(proof: HashHopProof) -> bool: + chain_artifacts = [Path(path) for path in proof.artifacts if path.endswith(".jsonl")] + if len(chain_artifacts) != 1 or not chain_artifacts[0].exists(): + return False + rows = [ + json.loads(line) + for line in chain_artifacts[0].read_text(encoding="utf-8").splitlines() + if line.strip() + ] + if len(rows) != proof.hops: + return False + if rows[0]["from"] != proof.query_id or rows[-1]["to"] != proof.expected_target_id: + return False + for index in range(1, len(rows)): + if rows[index - 1]["to"] != rows[index]["from"]: + return False + return all(Path(path).exists() for path in proof.artifacts) + + +def extract_hashhop_target(text: str) -> str | None: + stripped = text.strip() + try: + payload = json.loads(stripped) + except json.JSONDecodeError: + payload = extract_json_object(stripped) + if isinstance(payload, dict): + for key in ("answer", "target", "target_id", "final_target_id"): + value = payload.get(key) + if isinstance(value, str): + candidate = normalize_hash_id(value) + if candidate: + return candidate + match = re.search(r"\b[a-fA-F0-9]{32}\b", stripped) + return match.group(0).lower() if match else None + + +def extract_visual_target_slot(text: str) -> str | None: + stripped = text.strip() + payload: dict[str, Any] | None + try: + parsed = json.loads(stripped) + payload = parsed if isinstance(parsed, dict) else None + except json.JSONDecodeError: + payload = extract_json_object(stripped) + if isinstance(payload, dict): + for key in ("target_slot", "final_slot", "slot", "answer"): + value = payload.get(key) + if isinstance(value, str): + slot = normalize_visual_slot(value) + if slot: + return slot + return normalize_visual_slot(stripped) + + +def extract_visual_intermediate_slot(text: str) -> str | None: + stripped = text.strip() + try: + parsed = json.loads(stripped) + payload = parsed if isinstance(parsed, dict) else None + except json.JSONDecodeError: + payload = extract_json_object(stripped) + if isinstance(payload, dict): + for key in ("intermediate_slot", "middle_slot", "first_hop_slot"): + value = payload.get(key) + if isinstance(value, str): + slot = normalize_visual_slot(value) + if slot: + return slot + return None + + +def extract_json_object(text: str) -> dict[str, Any] | None: + start = text.find("{") + end = text.rfind("}") + if start < 0 or end <= start: + return None + try: + payload = json.loads(text[start : end + 1]) + except json.JSONDecodeError: + return None + return payload if isinstance(payload, dict) else None + + +def normalize_hash_id(value: str) -> str | None: + match = re.search(r"\b[a-fA-F0-9]{32}\b", value) + return match.group(0).lower() if match else None + + +def normalize_visual_slot(value: str) -> str | None: + normalized = re.sub(r"[^a-z]+", "-", value.lower()).strip("-") + aliases = { + "top-left": "top-left", + "upper-left": "top-left", + "left-top": "top-left", + "top-right": "top-right", + "upper-right": "top-right", + "right-top": "top-right", + "bottom-left": "bottom-left", + "lower-left": "bottom-left", + "left-bottom": "bottom-left", + "bottom-right": "bottom-right", + "lower-right": "bottom-right", + "right-bottom": "bottom-right", + } + if normalized in aliases: + return aliases[normalized] + for alias, slot in aliases.items(): + if re.search(rf"\b{re.escape(alias)}\b", normalized): + return slot + return None + + +def copy_proof_artifacts(proof: HashHopProof, target: Path) -> None: + target.mkdir(parents=True, exist_ok=True) + source_roots = {Path(path).parent for path in proof.artifacts} + for source_root in source_roots: + if source_root.name == "visual_blocks": + shutil.copytree(source_root, target / "visual_blocks", dirs_exist_ok=True) + for artifact in proof.artifacts: + path = Path(artifact) + if path.is_file() and path.parent.name != "visual_blocks": + shutil.copy2(path, target / path.name) + (target / "proof.json").write_text(proof.model_dump_json(indent=2), encoding="utf-8") + + +def contradiction_atom( + atom_id: str, + source_id: str, + source: str, + text: str, + modality: Modality, + source_kind: str, + version: str | None = None, +) -> EvidenceAtom: + metadata: dict[str, Any] = {"source_kind": source_kind} + if version is not None: + metadata["version"] = version + return EvidenceAtom( + atom_id=atom_id, + modality=modality, + source_id=source_id, + source=source, + text=text, + trust=TrustScores(parser=0.95, ocr=0.9, model=0.85), + token_equivalent=max(1, len(text) // 4), + metadata=metadata, + ) + + +def contradiction_context(question: str, atoms: list[EvidenceAtom]) -> CompiledContext: + hits = [ + RetrievalHit(atom=atom, sparse_score=0.8, dense_score=0.0, graph_score=0.0, final_score=0.8) + for atom in atoms + ] + plan = ContextPlan( + question=question, + profile=RuntimeProfile.research_theater, + required_modalities=sorted({atom.modality for atom in atoms}), + evidence_budget_atoms=len(atoms), + token_budget=sum(atom.token_equivalent for atom in atoms), + retrieval_query=question, + compression_strategy="visual-contradiction-eval", + verifier_contract=["Report contradictions as contradiction_notes."], + ) + packed = "\n".join(f"[{atom.atom_id}] {atom.text}" for atom in atoms) + return CompiledContext( + plan=plan, + hits=hits, + packed_text=packed, + token_equivalent=sum(atom.token_equivalent for atom in atoms), + cache_trace_id="visual_contradiction_eval", + ) + + +def build_contradiction_scenarios() -> list[dict[str, Any]]: + """Deterministic cross-version conflict and control samples (PRD 11.15). + + Each conflict pair injects a single genuine drift across two distinct + sources; each control pair is internally consistent or scoped to different + components. The samples exercise the live ``VisualContradictionLens`` so the + proof measures real recall and false-positive rate. + """ + + scenarios: list[dict[str, Any]] = [] + + def conflict(name: str, atoms: list[EvidenceAtom]) -> None: + scenarios.append({"name": name, "context": contradiction_context(name, atoms), "expect_drift": True}) + + def control(name: str, atoms: list[EvidenceAtom]) -> None: + scenarios.append({"name": name, "context": contradiction_context(name, atoms), "expect_drift": False}) + + conflict( + "button-radius-token-drift", + [ + contradiction_atom("home_btn_v4", "home_v4", "home_v4.figma", "button.radius: 8px", Modality.ui_screenshot, "figma"), + contradiction_atom("checkout_btn_v4", "checkout_v4", "checkout_v4.figma", "button.radius: 12px", Modality.ui_screenshot, "figma"), + ], + ) + conflict( + "primary-color-token-drift", + [ + contradiction_atom("spec_color", "brand_spec", "brand_guide.pdf", "primary.color: #2563eb", Modality.pdf_page, "spec"), + contradiction_atom("impl_color", "theme_css", "src/theme.css", "primary.color: #1d4ed8", Modality.code, "css"), + ], + ) + conflict( + "heading-typography-drift", + [ + contradiction_atom("home_heading", "home_shot", "home_v3.png", "heading.font-size: 24px", Modality.ui_screenshot, "ui_screenshot"), + contradiction_atom("checkout_heading", "checkout_shot", "checkout_v3.png", "heading.font-size: 28px", Modality.ui_screenshot, "ui_screenshot"), + ], + ) + conflict( + "card-spacing-layout-drift", + [ + contradiction_atom("card_home", "card_home_shot", "home_v3.png", "card.padding: 16px", Modality.ui_screenshot, "ui_screenshot"), + contradiction_atom("card_settings", "card_settings_shot", "settings_v3.png", "card.padding: 24px", Modality.ui_screenshot, "ui_screenshot"), + ], + ) + conflict( + "cta-copy-drift", + [ + contradiction_atom("cta_v1", "shot_v1", "cart_v1.png", 'cta.label: "Checkout"', Modality.ui_screenshot, "ui_screenshot"), + contradiction_atom("cta_v4", "shot_v4", "cart_v4.png", 'cta.label: "Pay now"', Modality.ui_screenshot, "ui_screenshot"), + ], + ) + conflict( + "banner-radius-temporal-drift", + [ + contradiction_atom("banner_v1", "banner_spec_v1", "banner_v1.figma", "banner.radius: 4px", Modality.ui_screenshot, "figma", version="v1"), + contradiction_atom("banner_v4", "banner_spec_v4", "banner_v4.figma", "banner.radius: 10px", Modality.ui_screenshot, "figma", version="v4"), + ], + ) + conflict( + "toggle-accent-visual-code-drift", + [ + contradiction_atom("toggle_design", "toggle_figma", "toggle.figma", "toggle.color: #10b981", Modality.ui_screenshot, "figma"), + contradiction_atom("toggle_code", "toggle_css", "src/toggle.css", "toggle.color: #059669", Modality.code, "css"), + ], + ) + + control( + "consistent-button-radius", + [ + contradiction_atom("ctrl_btn_spec", "ctrl_spec", "spec.pdf", "button.radius: 8px", Modality.pdf_page, "spec"), + contradiction_atom("ctrl_btn_css", "ctrl_css", "src/tokens.css", "button.radius = 8px", Modality.code, "css"), + ], + ) + control( + "distinct-components-not-a-conflict", + [ + contradiction_atom("ctrl_button_radius", "ctrl_a", "a.css", "button.radius: 8px", Modality.code, "css"), + contradiction_atom("ctrl_input_radius", "ctrl_b", "b.css", "input.radius: 12px", Modality.code, "css"), + ], + ) + control( + "single-source-multiple-rules", + [ + contradiction_atom( + "ctrl_single_css", + "ctrl_single", + "src/base.css", + "header.padding: 8px; footer.padding: 24px; aside.padding: 16px", + Modality.code, + "css", + ), + ], + ) + control( + "consistent-primary-color", + [ + contradiction_atom("ctrl_color_spec", "ctrl_color_a", "brand.pdf", "primary.color: #2563eb", Modality.pdf_page, "spec"), + contradiction_atom("ctrl_color_css", "ctrl_color_b", "theme.css", "primary.color: #2563eb", Modality.code, "css"), + ], + ) + control( + "prose-without-design-facts", + [ + contradiction_atom("ctrl_prose_a", "ctrl_prose_1", "notes_a.txt", "The onboarding flow feels smooth and the colors look balanced.", Modality.text, "text"), + contradiction_atom("ctrl_prose_b", "ctrl_prose_2", "notes_b.txt", "Users reported the checkout button is easy to find.", Modality.text, "text"), + ], + ) + + return scenarios + + +def write_json(path: Path, payload: dict[str, Any]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8") + + +def read_json_or_empty(path: Path) -> dict[str, Any]: + if not path.exists(): + return {} + try: + value = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError: + return {} + return value if isinstance(value, dict) else {} + + +def format_scores(scores: dict[str, Any]) -> str: + parts: list[str] = [] + for name in sorted(scores): + score = scores[name] + if isinstance(score, (int, float)) and math.isfinite(float(score)): + parts.append(f"{name}={float(score):.2f}") + else: + parts.append(f"{name}=unscored") + return ", ".join(parts) or "none" + + +def research_module_checks(report: dict[str, Any]) -> list[dict[str, Any]]: + checks: list[dict[str, Any]] = [] + + def add(module: str, passed: bool, detail: str) -> None: + checks.append({"module": module, "passed": passed, "detail": detail}) + + v_nsa = report.get("v_nsa") if isinstance(report.get("v_nsa"), dict) else {} + sparse_blocks = v_nsa.get("sparse_blocks") if isinstance(v_nsa.get("sparse_blocks"), list) else [] + add( + "v_nsa", + int(v_nsa.get("sparse_block_count") or 0) > 0 and bool(sparse_blocks), + f"{len(sparse_blocks)} sparse blocks", + ) + + dvkv = report.get("dvkv") if isinstance(report.get("dvkv"), dict) else {} + selected = dvkv.get("selected") if isinstance(dvkv.get("selected"), list) else [] + candidate_tokens = int(dvkv.get("candidate_tokens") or 0) + budget = int(dvkv.get("budget") or 0) + dvkv_scores_valid = all( + is_finite_number(item.get("attention_importance")) and is_finite_number(item.get("evidence_value")) + for item in selected + if isinstance(item, dict) + ) + add( + "dvkv", + candidate_tokens > 0 and 0 < len(selected) <= budget and dvkv_scores_valid, + f"{len(selected)}/{candidate_tokens} visual tokens selected", + ) + + x_modal = report.get("x_modal_kv") if isinstance(report.get("x_modal_kv"), dict) else {} + shared_blocks = x_modal.get("shared_blocks") if isinstance(x_modal.get("shared_blocks"), list) else [] + x_modal_valid = bool(shared_blocks) and all( + isinstance(block, dict) and bool(block.get("block_id")) and bool(block.get("atom_ids")) + for block in shared_blocks + ) + add("x_modal_kv", x_modal_valid, f"{len(shared_blocks)} shared KV blocks") + + prefetch = report.get("speculative_prefetch") if isinstance(report.get("speculative_prefetch"), dict) else {} + prefetch_blocks = prefetch.get("prefetch_blocks") if isinstance(prefetch.get("prefetch_blocks"), list) else [] + priorities = [ + float(block.get("priority")) + for block in prefetch_blocks + if isinstance(block, dict) and is_finite_number(block.get("priority")) + ] + prefetch_valid = ( + bool(prefetch_blocks) + and len(priorities) == len(prefetch_blocks) + and priorities == sorted(priorities, reverse=True) + and all(int(block.get("bytes_estimate") or 0) > 0 for block in prefetch_blocks if isinstance(block, dict)) + ) + add("speculative_prefetch", prefetch_valid, f"{len(prefetch_blocks)} prefetch blocks") + + eviction = report.get("predictive_eviction") if isinstance(report.get("predictive_eviction"), dict) else {} + layers = eviction.get("layers") if isinstance(eviction.get("layers"), list) else [] + eviction_valid = bool(layers) and all( + isinstance(layer, dict) + and bool(layer.get("replay_token")) + and is_finite_number(layer.get("hit_rate")) + and int(layer.get("entries") or 0) >= 0 + for layer in layers + ) + add("predictive_eviction", eviction_valid, f"{len(layers)} cache layers") + + retrieval = report.get("differentiable_retrieval") if isinstance(report.get("differentiable_retrieval"), dict) else {} + probabilities = retrieval.get("probabilities") if isinstance(retrieval.get("probabilities"), list) else [] + probability_values = [ + float(item.get("probability")) + for item in probabilities + if isinstance(item, dict) and is_finite_number(item.get("probability")) + ] + probability_sum = sum(probability_values) + retrieval_valid = ( + bool(probabilities) + and len(probability_values) == len(probabilities) + and abs(probability_sum - 1.0) <= 1e-6 + and bool(retrieval.get("straight_through_atom_id")) + ) + add("differentiable_retrieval", retrieval_valid, f"probability sum {probability_sum:.6f}") + + graph = report.get("evidence_graph") if isinstance(report.get("evidence_graph"), dict) else {} + relations = graph.get("relations") if isinstance(graph.get("relations"), list) else [] + edge_count = int(graph.get("edge_count_sample") or 0) + add("evidence_graph", edge_count > 0 and "cites" in relations, f"{edge_count} sampled edges") + + sleep = report.get("sleep_consolidation") if isinstance(report.get("sleep_consolidation"), dict) else {} + durable = sleep.get("durable_candidates") if isinstance(sleep.get("durable_candidates"), list) else [] + add( + "sleep_consolidation", + int(sleep.get("candidate_count") or 0) > 0 and bool(durable) and bool(sleep.get("target_adapter")), + f"{len(durable)} durable candidates", + ) + + return checks + + +def is_finite_number(value: Any) -> bool: + return isinstance(value, (int, float)) and math.isfinite(float(value)) + + +def project_root() -> Path: + current = Path.cwd().resolve() + for candidate in (current, *current.parents): + if (candidate / "PRD.md").exists() and (candidate / "backend" / "src" / "ely_eye").exists(): + return candidate + return current + + +def discover_repository_files(root: Path) -> list[Path]: + ignored_dirs = {".git", ".ely_eye", ".venv", ".venv-linux", "__pycache__", "node_modules", "dist", "build", "target"} + files: list[Path] = [] + for current, dirs, names in os.walk(root): + dirs[:] = [name for name in dirs if name not in ignored_dirs and not name.startswith(".venv")] + current_path = Path(current) + for name in names: + path = current_path / name + try: + relative = path.relative_to(root) + except ValueError: + continue + if any(part in ignored_dirs or part.startswith(".venv") for part in relative.parts): + continue + try: + path.stat() + except OSError: + continue + files.append(path) + return files + + +def pass_rate(proofs: list[HashHopProof]) -> float: + if not proofs: + return 0.0 + return sum(1 for proof in proofs if proof.passed) / len(proofs) + + +def collect_model_visual_proofs( + evaluator: HashHopEvaluator, + hops: int, + token_equivalent: int, + min_samples: int, + target_rate: float, + max_samples: int, +) -> list[HashHopProof]: + proofs: list[HashHopProof] = [] + while len(proofs) < max_samples: + proofs.append(evaluator.generate_model_visual_proof(hops, token_equivalent, max_proofs=5)) + if len(proofs) >= min_samples and pass_rate(proofs) >= target_rate: + break + return proofs + + +def compact_generation_context(context: Any, section_hint: str, max_chars: int = 2400) -> Any: + text_hits = [hit for hit in context.hits if hit.atom.modality.value == "text"] + hits = (text_hits or context.hits)[:1] + sections: list[str] = [] + for hit in hits: + atom = hit.atom + evidence_text = extract_markdown_section(atom.text, section_hint) or atom.text + evidence_text = evidence_text.strip()[:max_chars] + citation = { + "atom_id": atom.atom_id, + "modality": atom.modality.value, + "source": atom.source, + "image_ref": atom.image_ref, + "layout": atom.layout.model_dump() if atom.layout else None, + "score": hit.final_score, + } + sections.append(f"[EVIDENCE]\n{citation}\n{evidence_text}\n[/EVIDENCE]") + packed_text = "\n\n".join(sections) + return context.model_copy( + update={ + "hits": hits, + "packed_text": packed_text, + "token_equivalent": max(1, len(packed_text) // 4), + } + ) + + +def extract_markdown_section(text: str, section_hint: str) -> str: + lines = text.splitlines() + start = None + for index, line in enumerate(lines): + stripped = line.strip() + if stripped.startswith("#") and section_hint in stripped: + start = index + break + if start is None: + return "" + end = len(lines) + start_level = len(lines[start]) - len(lines[start].lstrip("#")) + for index in range(start + 1, len(lines)): + stripped = lines[index].strip() + if stripped.startswith("#"): + level = len(lines[index]) - len(lines[index].lstrip("#")) + if level <= start_level: + end = index + break + return "\n".join(lines[start:end]) + + +def relative_artifacts(root: Path) -> dict[str, str]: + artifacts: dict[str, str] = {} + for path in sorted(root.rglob("*")): + if path.is_file(): + key = path.relative_to(root).as_posix() + artifacts[key] = key + return artifacts diff --git a/backend/src/ely_eye/evidence_graph.py b/backend/src/ely_eye/evidence_graph.py new file mode 100644 index 0000000..59e88fd --- /dev/null +++ b/backend/src/ely_eye/evidence_graph.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +import hashlib + +from .config import Settings, get_settings +from .db import Database +from .schemas import CitationReport, CompiledContext + + +class EvidenceGraphService: + def __init__(self, settings: Settings | None = None, database: Database | None = None) -> None: + self.settings = settings or get_settings() + self.db = database or Database(self.settings) + + def record_answer( + self, + question: str, + answer: str, + report: CitationReport, + context: CompiledContext, + ) -> str: + claim_id = claim_node_id(question, answer) + hit_by_id = {hit.atom.atom_id: hit for hit in context.hits} + edges: list[dict[str, object]] = [] + for atom_id in report.cited_atom_ids: + hit = hit_by_id.get(atom_id) + edges.append( + { + "from_atom_id": claim_id, + "to_atom_id": atom_id, + "relation": "cites", + "confidence": report.confidence, + "metadata": { + "question": question, + "answer_sha256": hashlib.sha256(answer.encode("utf-8")).hexdigest(), + "cache_trace_id": context.cache_trace_id, + "final_score": hit.final_score if hit else None, + }, + } + ) + for atom_id in report.missing_atom_ids: + edges.append( + { + "from_atom_id": claim_id, + "to_atom_id": atom_id, + "relation": "missing_citation", + "confidence": 0.0, + "metadata": {"question": question, "cache_trace_id": context.cache_trace_id}, + } + ) + for note in report.contradiction_notes: + note_id = "contradiction_" + hashlib.sha256(note.encode("utf-8")).hexdigest()[:16] + edges.append( + { + "from_atom_id": claim_id, + "to_atom_id": note_id, + "relation": "contradiction_note", + "confidence": report.confidence, + "metadata": {"note": note, "question": question}, + } + ) + self.db.write_evidence_edges(edges) + return claim_id + + +def claim_node_id(question: str, answer: str) -> str: + digest = hashlib.sha256(f"{question}\n{answer}".encode("utf-8")).hexdigest()[:20] + return f"claim_{digest}" diff --git a/backend/src/ely_eye/ingestion.py b/backend/src/ely_eye/ingestion.py new file mode 100644 index 0000000..4062973 --- /dev/null +++ b/backend/src/ely_eye/ingestion.py @@ -0,0 +1,657 @@ +from __future__ import annotations + +import ast +import json +import os +import re +import subprocess +import tempfile +from dataclasses import dataclass +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +import fitz +import cv2 +from PIL import Image + +from .config import Settings, get_settings +from .db import Database +from .ocr import read_text_from_image +from .schemas import ( + EvidenceAtom, + IngestResponse, + LayoutBox, + Modality, + SourceRecord, + TrustScores, +) +from .storage import ObjectStore, guess_mime, safe_source_id, sha256_bytes, sha256_file, token_equivalent + + +SUPPORTED_TEXT_EXTENSIONS = { + ".txt", + ".md", + ".json", + ".jsonl", + ".yaml", + ".yml", + ".toml", + ".csv", + ".html", + ".css", + ".xml", +} + +SUPPORTED_CODE_EXTENSIONS = { + ".py", + ".ts", + ".tsx", + ".js", + ".jsx", + ".rs", + ".go", + ".java", + ".cpp", + ".c", + ".h", + ".hpp", + ".cs", + ".swift", + ".kt", + ".sql", +} + +SUPPORTED_IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp", ".bmp", ".tif", ".tiff"} +SUPPORTED_VIDEO_EXTENSIONS = {".mp4", ".mov", ".mkv", ".avi", ".webm", ".m4v"} +REPOSITORY_INDEX_LIMIT = 5000 +REPOSITORY_COMMIT_LIMIT = 50 + + +@dataclass +class IngestBatch: + sources: list[SourceRecord] + atoms: list[EvidenceAtom] + + @property + def token_equivalent(self) -> int: + return sum(atom.token_equivalent for atom in self.atoms) + + +class IngestionService: + def __init__( + self, + settings: Settings | None = None, + database: Database | None = None, + object_store: ObjectStore | None = None, + ) -> None: + self.settings = settings or get_settings() + self.db = database or Database(self.settings) + self.objects = object_store or ObjectStore(self.settings) + + def ingest_path(self, path: Path, cartridge_name: str | None = None) -> IngestResponse: + path = path.expanduser().resolve() + if not path.exists(): + raise FileNotFoundError(path) + files = self._discover_files(path) + batch = IngestBatch(sources=[], atoms=[]) + for file_path in files: + file_batch = self._ingest_file(file_path) + batch.sources.extend(file_batch.sources) + batch.atoms.extend(file_batch.atoms) + if path.is_dir(): + repository_batch = self._ingest_repository_context(path, files) + batch.sources.extend(repository_batch.sources) + batch.atoms.extend(repository_batch.atoms) + + self.db.upsert_sources(batch.sources) + self.db.upsert_atoms(batch.atoms) + + cartridge_id = None + if cartridge_name: + from .cartridge import CartridgeService + + cartridge = CartridgeService(self.settings, self.db).materialize( + name=cartridge_name, + atoms=batch.atoms, + sources=batch.sources, + ) + cartridge_id = cartridge.cartridge_id + + return IngestResponse( + source_count=len(batch.sources), + atom_count=len(batch.atoms), + cartridge_id=cartridge_id, + token_equivalent=batch.token_equivalent, + sources=batch.sources, + ) + + def _discover_files(self, path: Path) -> list[Path]: + if path.is_file(): + return [path] + ignored_dirs = {".git", ".ely_eye", "node_modules", ".venv", ".venv-linux", "__pycache__", "dist", "build", "target"} + files: list[Path] = [] + for root, dirs, names in os.walk(path): + dirs[:] = [name for name in dirs if name not in ignored_dirs and not name.startswith(".venv")] + root_path = Path(root) + for name in names: + child = root_path / name + if any(part in ignored_dirs or part.startswith(".venv") for part in child.parts): + continue + try: + if child.is_dir(): + continue + child.stat() + except OSError: + continue + files.append(child) + return files + + def _ingest_file(self, path: Path) -> IngestBatch: + suffix = path.suffix.lower() + if suffix == ".pdf": + return self._ingest_pdf(path) + if suffix in SUPPORTED_IMAGE_EXTENSIONS: + return self._ingest_image(path) + if suffix in SUPPORTED_VIDEO_EXTENSIONS: + return self._ingest_video(path) + if suffix in SUPPORTED_CODE_EXTENSIONS: + return self._ingest_code(path) + if suffix in SUPPORTED_TEXT_EXTENSIONS: + return self._ingest_text(path, Modality.text) + return IngestBatch(sources=[], atoms=[]) + + def _source_record(self, path: Path, kind: str, metadata: dict[str, Any] | None = None) -> SourceRecord: + digest = sha256_file(path) + return SourceRecord( + source_id=safe_source_id(path, digest), + path=str(path), + kind=kind, + mime=guess_mime(path), + sha256=digest, + size_bytes=path.stat().st_size, + metadata=metadata or {}, + ) + + def _ingest_text(self, path: Path, modality: Modality) -> IngestBatch: + if path.stat().st_size > self.settings.max_text_file_bytes: + return IngestBatch(sources=[], atoms=[]) + source = self._source_record(path, modality.value) + text = path.read_text(encoding="utf-8", errors="replace") + atom = EvidenceAtom( + atom_id=f"{source.source_id}:text", + modality=modality, + source_id=source.source_id, + source=str(path), + text=text, + trust=TrustScores(parser=1.0, ocr=0.0, model=0.0), + token_equivalent=token_equivalent(text), + metadata={"extension": path.suffix.lower()}, + ) + return IngestBatch(sources=[source], atoms=[atom]) + + def _ingest_code(self, path: Path) -> IngestBatch: + batch = self._ingest_text(path, Modality.code) + if not batch.atoms: + return batch + source_text = batch.atoms[0].text + batch.atoms[0].metadata["symbols"] = extract_symbols(path, source_text) + batch.atoms[0].metadata["line_count"] = source_text.count("\n") + 1 + return batch + + def _ingest_image(self, path: Path) -> IngestBatch: + source = self._source_record(path, self._image_kind(path), metadata=image_metadata(path)) + image_ref = self.objects.put_file(path, "images") + ocr = read_text_from_image(path) + width, height = image_size(path) + atom = EvidenceAtom( + atom_id=f"{source.source_id}:image", + modality=Modality.ui_screenshot if source.kind == "ui_screenshot" else Modality.image, + source_id=source.source_id, + source=str(path), + text=ocr.text, + image_ref=image_ref, + layout=LayoutBox(x=0, y=0, w=width, h=height), + trust=TrustScores(parser=1.0, ocr=ocr.confidence, model=0.0), + token_equivalent=token_equivalent(ocr.text) + max(1, (width * height) // (224 * 224)), + metadata={"ocr_regions": ocr.regions, **image_metadata(path)}, + ) + return IngestBatch(sources=[source], atoms=[atom]) + + def _ingest_pdf(self, path: Path) -> IngestBatch: + source = self._source_record(path, "pdf") + atoms: list[EvidenceAtom] = [] + with fitz.open(path) as document: + source.metadata["page_count"] = document.page_count + for page_index, page in enumerate(document, start=1): + rect = page.rect + text = page.get_text("text").strip() + pixmap = page.get_pixmap(matrix=fitz.Matrix(1.5, 1.5), alpha=False) + image_bytes = pixmap.tobytes("png") + image_ref = self.objects.put_bytes(image_bytes, ".png", "pdf_pages") + if not text: + with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as temp_file: + temp_file.write(image_bytes) + temp_path = Path(temp_file.name) + try: + ocr = read_text_from_image(temp_path) + text = ocr.text + ocr_confidence = ocr.confidence + finally: + temp_path.unlink(missing_ok=True) + else: + ocr_confidence = 0.0 + atoms.append( + EvidenceAtom( + atom_id=f"{source.source_id}:page:{page_index:05d}", + modality=Modality.pdf_page, + source_id=source.source_id, + source=f"{path}#page={page_index}", + text=text, + image_ref=image_ref, + layout=LayoutBox(x=0, y=0, w=float(rect.width), h=float(rect.height), page=page_index), + trust=TrustScores(parser=1.0, ocr=ocr_confidence, model=0.0), + token_equivalent=token_equivalent(text) + max(1, int(rect.width * rect.height) // 50_000), + metadata={"page": page_index, "width": rect.width, "height": rect.height}, + ) + ) + atoms.extend(self._pdf_region_atoms(path, source.source_id, page_index, page)) + return IngestBatch(sources=[source], atoms=atoms) + + def _pdf_region_atoms( + self, path: Path, source_id: str, page_index: int, page: fitz.Page + ) -> list[EvidenceAtom]: + regions: list[EvidenceAtom] = [] + blocks = page.get_text("blocks") + for block_index, block in enumerate(blocks): + if len(block) < 5: + continue + x0, y0, x1, y1, text = block[:5] + text = str(text).strip() + if not text: + continue + regions.append( + EvidenceAtom( + atom_id=f"{source_id}:page:{page_index:05d}:region:{block_index:04d}", + modality=Modality.pdf_region, + source_id=source_id, + source=f"{path}#page={page_index}®ion={block_index}", + text=text, + layout=LayoutBox(x=float(x0), y=float(y0), w=float(x1 - x0), h=float(y1 - y0), page=page_index), + trust=TrustScores(parser=1.0, ocr=0.0, model=0.0), + token_equivalent=token_equivalent(text), + metadata={"page": page_index, "block": block_index}, + ) + ) + return regions + + def _ingest_video(self, path: Path) -> IngestBatch: + source = self._source_record(path, "video") + capture = cv2.VideoCapture(str(path)) + if not capture.isOpened(): + raise RuntimeError(f"Cannot open video file: {path}") + + fps = capture.get(cv2.CAP_PROP_FPS) + frame_count = int(capture.get(cv2.CAP_PROP_FRAME_COUNT)) + duration = frame_count / fps if fps > 0 else 0.0 + source.metadata.update({"fps": fps, "frame_count": frame_count, "duration_seconds": duration}) + + atoms: list[EvidenceAtom] = [] + step = max(1, int(fps * self.settings.video_sample_seconds)) if fps > 0 else 1 + frame_index = 0 + while frame_index < frame_count: + capture.set(cv2.CAP_PROP_POS_FRAMES, frame_index) + ok, frame = capture.read() + if not ok: + break + second = frame_index / fps if fps > 0 else 0.0 + success, encoded = cv2.imencode(".png", frame) + if not success: + frame_index += step + continue + image_bytes = encoded.tobytes() + image_ref = self.objects.put_bytes(image_bytes, ".png", "video_frames") + with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as temp_file: + temp_file.write(image_bytes) + temp_path = Path(temp_file.name) + try: + ocr = read_text_from_image(temp_path) + finally: + temp_path.unlink(missing_ok=True) + height, width = frame.shape[:2] + atoms.append( + EvidenceAtom( + atom_id=f"{source.source_id}:frame:{frame_index:08d}", + modality=Modality.video_frame, + source_id=source.source_id, + source=f"{path}#t={second:.3f}", + text=ocr.text, + image_ref=image_ref, + layout=LayoutBox(x=0, y=0, w=float(width), h=float(height), frame_second=second), + trust=TrustScores(parser=1.0, ocr=ocr.confidence, model=0.0), + token_equivalent=token_equivalent(ocr.text) + max(1, (width * height) // (224 * 224)), + metadata={"frame_index": frame_index, "second": second, "ocr_regions": ocr.regions}, + ) + ) + frame_index += step + capture.release() + return IngestBatch(sources=[source], atoms=atoms) + + def _image_kind(self, path: Path) -> str: + name = path.name.lower() + ui_markers = ("screenshot", "screen", "figma", "ui", "wireframe", "mockup") + return "ui_screenshot" if any(marker in name for marker in ui_markers) else "image" + + def _ingest_repository_context(self, root: Path, files: list[Path]) -> IngestBatch: + code_files = [path for path in files if path.suffix.lower() in SUPPORTED_CODE_EXTENSIONS] + git_root = find_git_root(root) + if not code_files and git_root is None: + return IngestBatch(sources=[], atoms=[]) + + context = build_repository_context(root, files, code_files, git_root) + payload = json.dumps(context, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8") + digest = sha256_bytes(payload) + source = SourceRecord( + source_id=safe_source_id(root, digest), + path=str(root), + kind="code_repository", + mime="application/vnd.ely-eye.repository+json", + sha256=digest, + size_bytes=len(payload), + metadata={ + "file_count": context["structure"]["file_count"], + "indexed_file_count": context["structure"]["indexed_file_count"], + "code_file_count": context["code"]["file_count"], + "git_available": context["git"]["available"], + }, + ) + atom = EvidenceAtom( + atom_id=f"{source.source_id}:text", + modality=Modality.code, + source_id=source.source_id, + source=str(root), + text=json.dumps(context, ensure_ascii=False, indent=2), + trust=TrustScores(parser=1.0, ocr=0.0, model=0.0), + token_equivalent=token_equivalent(json.dumps(context, ensure_ascii=False)), + metadata=context, + ) + return IngestBatch(sources=[source], atoms=[atom]) + + +def image_size(path: Path) -> tuple[int, int]: + with Image.open(path) as image: + return image.size + + +def image_metadata(path: Path) -> dict[str, Any]: + with Image.open(path) as image: + return {"width": image.width, "height": image.height, "mode": image.mode, "format": image.format} + + +def build_repository_context( + root: Path, + files: list[Path], + code_files: list[Path], + git_root: Path | None, +) -> dict[str, Any]: + structure_entries = [file_structure_entry(root, path) for path in sorted(files)[:REPOSITORY_INDEX_LIMIT]] + code_entries = [code_context_entry(root, path) for path in sorted(code_files)[:REPOSITORY_INDEX_LIMIT]] + git_context = collect_git_context(git_root) if git_root is not None else {"available": False} + return { + "format": "ely-eye-repository-context-v1", + "root": str(root), + "structure": { + "file_count": len(files), + "indexed_file_count": len(structure_entries), + "limit": REPOSITORY_INDEX_LIMIT, + "files": structure_entries, + }, + "code": { + "file_count": len(code_files), + "indexed_file_count": len(code_entries), + "files": code_entries, + "call_graph": repository_call_graph(code_entries), + }, + "git": git_context, + "issue_pr_history": extract_issue_pr_history(git_context), + } + + +def file_structure_entry(root: Path, path: Path) -> dict[str, Any]: + return { + "path": relative_path(root, path), + "suffix": path.suffix.lower(), + "kind": file_kind(path), + "size_bytes": path.stat().st_size, + } + + +def code_context_entry(root: Path, path: Path) -> dict[str, Any]: + text = path.read_text(encoding="utf-8", errors="replace") + symbols = extract_symbols(path, text) + return { + "path": relative_path(root, path), + "language": path.suffix.lower().lstrip("."), + "line_count": text.count("\n") + 1, + "symbols": symbols, + } + + +def repository_call_graph(code_entries: list[dict[str, Any]]) -> list[dict[str, Any]]: + edges: list[dict[str, Any]] = [] + for entry in code_entries: + symbols = entry.get("symbols") if isinstance(entry.get("symbols"), dict) else {} + for call in symbols.get("calls", []): + if not isinstance(call, dict): + continue + caller = call.get("caller") + callee = call.get("callee") + if caller and callee: + edges.append({"path": entry["path"], "caller": caller, "callee": callee}) + return edges + + +def collect_git_context(root: Path) -> dict[str, Any]: + commits = [ + parse_git_log_line(line) + for line in run_git(root, ["log", f"--max-count={REPOSITORY_COMMIT_LIMIT}", "--pretty=format:%H%x09%an%x09%aI%x09%s"]).splitlines() + if line.strip() + ] + commits = [commit for commit in commits if commit] + return { + "available": True, + "root": str(root), + "head": run_git(root, ["rev-parse", "HEAD"]).strip() or None, + "branch": run_git(root, ["branch", "--show-current"]).strip() or None, + "remotes": parse_git_remotes(run_git(root, ["remote", "-v"])), + "recent_commits": commits, + } + + +def parse_git_log_line(line: str) -> dict[str, str] | None: + parts = line.split("\t", 3) + if len(parts) != 4: + return None + commit_hash, author, authored_at, subject = parts + return {"hash": commit_hash, "author": author, "authored_at": authored_at, "subject": subject} + + +def parse_git_remotes(output: str) -> list[dict[str, str]]: + remotes: list[dict[str, str]] = [] + for line in output.splitlines(): + parts = line.split() + if len(parts) >= 3: + remotes.append({"name": parts[0], "url": parts[1], "kind": parts[2].strip("()")}) + return remotes + + +def extract_issue_pr_history(git_context: dict[str, Any]) -> dict[str, Any]: + references: dict[str, set[int]] = {"issues": set(), "pull_requests": set()} + for commit in git_context.get("recent_commits", []): + if not isinstance(commit, dict): + continue + subject = str(commit.get("subject") or "") + for number in re.findall(r"(?:fixes|closes|resolves)\s+#(\d+)", subject, flags=re.IGNORECASE): + references["issues"].add(int(number)) + for number in re.findall(r"(?:pull request|pr)\s*#?(\d+)", subject, flags=re.IGNORECASE): + references["pull_requests"].add(int(number)) + for number in re.findall(r"\(#(\d+)\)", subject): + references["pull_requests"].add(int(number)) + return { + "source": "local_git_commit_history", + "issues": sorted(references["issues"]), + "pull_requests": sorted(references["pull_requests"]), + } + + +def run_git(root: Path, args: list[str]) -> str: + try: + completed = subprocess.run( + ["git", *args], + cwd=root, + check=False, + capture_output=True, + text=True, + timeout=10, + ) + except (OSError, subprocess.SubprocessError): + return "" + return completed.stdout if completed.returncode == 0 else "" + + +def find_git_root(path: Path) -> Path | None: + current = path if path.is_dir() else path.parent + while True: + if (current / ".git").exists(): + return current + if current.parent == current: + return None + current = current.parent + + +def file_kind(path: Path) -> str: + suffix = path.suffix.lower() + if suffix in SUPPORTED_CODE_EXTENSIONS: + return "code" + if suffix in SUPPORTED_TEXT_EXTENSIONS: + return "text" + if suffix in SUPPORTED_IMAGE_EXTENSIONS: + return "image" + if suffix in SUPPORTED_VIDEO_EXTENSIONS: + return "video" + if suffix == ".pdf": + return "pdf" + return "binary" + + +def relative_path(root: Path, path: Path) -> str: + try: + return path.relative_to(root).as_posix() + except ValueError: + return path.as_posix() + + +def extract_symbols(path: Path, text: str) -> dict[str, Any]: + suffix = path.suffix.lower() + if suffix == ".py": + return extract_python_symbols(text) + symbols = re.findall( + r"\b(?:class|function|interface|type|enum|struct|fn|func)\s+([A-Za-z_][A-Za-z0-9_]*)", + text, + ) + constants = re.findall(r"\b(?:const|let|var)\s+([A-Za-z_][A-Za-z0-9_]*)\s*=", text) + calls = extract_generic_calls(text) + return {"symbols": sorted(set(symbols)), "bindings": sorted(set(constants)), "calls": calls} + + +def extract_python_symbols(text: str) -> dict[str, Any]: + try: + tree = ast.parse(text) + except SyntaxError as exc: + return { + "classes": [], + "functions": [], + "imports": [], + "calls": extract_generic_calls(text), + "parse_error": f"{exc.msg} at line {exc.lineno}", + } + classes: list[str] = [] + functions: list[str] = [] + imports: list[str] = [] + calls: list[dict[str, str]] = [] + for node in ast.walk(tree): + if isinstance(node, ast.ClassDef): + classes.append(node.name) + elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + functions.append(node.name) + elif isinstance(node, ast.Import): + imports.extend(alias.name for alias in node.names) + elif isinstance(node, ast.ImportFrom) and node.module: + imports.append(node.module) + call_visitor = PythonCallGraphVisitor() + call_visitor.visit(tree) + calls.extend(call_visitor.calls) + return { + "classes": sorted(set(classes)), + "functions": sorted(set(functions)), + "imports": sorted(set(imports)), + "calls": calls, + } + + +class PythonCallGraphVisitor(ast.NodeVisitor): + def __init__(self) -> None: + self.current: str | None = None + self.calls: list[dict[str, str]] = [] + + def visit_FunctionDef(self, node: ast.FunctionDef) -> Any: + previous = self.current + self.current = node.name + self.generic_visit(node) + self.current = previous + + def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> Any: + previous = self.current + self.current = node.name + self.generic_visit(node) + self.current = previous + + def visit_Call(self, node: ast.Call) -> Any: + if self.current: + callee = python_call_name(node.func) + if callee: + self.calls.append({"caller": self.current, "callee": callee}) + self.generic_visit(node) + + +def python_call_name(node: ast.AST) -> str | None: + if isinstance(node, ast.Name): + return node.id + if isinstance(node, ast.Attribute): + parent = python_call_name(node.value) + return f"{parent}.{node.attr}" if parent else node.attr + return None + + +def extract_generic_calls(text: str) -> list[dict[str, str]]: + calls: list[dict[str, str]] = [] + current = "" + function_pattern = re.compile(r"\b(?:function|fn|func)\s+([A-Za-z_][A-Za-z0-9_]*)|([A-Za-z_][A-Za-z0-9_]*)\s*=\s*\([^)]*\)\s*=>") + call_pattern = re.compile(r"\b([A-Za-z_][A-Za-z0-9_.]*)\s*\(") + keywords = {"if", "for", "while", "switch", "catch", "return", "function"} + for line in text.splitlines(): + function_match = function_pattern.search(line) + if function_match: + current = next(group for group in function_match.groups() if group) + for callee in call_pattern.findall(line): + root = callee.split(".", 1)[0] + if root not in keywords and callee != current: + calls.append({"caller": current, "callee": callee}) + return calls + + +def write_sources_json(path: Path, sources: list[SourceRecord]) -> None: + path.write_text( + json.dumps([source.model_dump(mode="json") for source in sources], ensure_ascii=False, indent=2), + encoding="utf-8", + ) diff --git a/backend/src/ely_eye/ocr.py b/backend/src/ely_eye/ocr.py new file mode 100644 index 0000000..aa7232b --- /dev/null +++ b/backend/src/ely_eye/ocr.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +from functools import lru_cache +from pathlib import Path +from typing import Any + + +class OcrResult: + def __init__(self, text: str, confidence: float, regions: list[dict[str, Any]]) -> None: + self.text = text + self.confidence = confidence + self.regions = regions + + +@lru_cache(maxsize=1) +def _rapid_ocr() -> Any: + from rapidocr_onnxruntime import RapidOCR + + return RapidOCR() + + +def read_text_from_image(path: Path) -> OcrResult: + engine = _rapid_ocr() + result = engine(str(path)) + raw = result[0] if isinstance(result, tuple) else result + if raw is None: + return OcrResult(text="", confidence=0.0, regions=[]) + + texts: list[str] = [] + confidences: list[float] = [] + regions: list[dict[str, Any]] = [] + for item in raw: + if len(item) < 3: + continue + box, text, confidence = item[0], str(item[1]), float(item[2]) + texts.append(text) + confidences.append(confidence) + regions.append({"box": box, "text": text, "confidence": confidence}) + + average_confidence = sum(confidences) / len(confidences) if confidences else 0.0 + return OcrResult(text="\n".join(texts), confidence=average_confidence, regions=regions) diff --git a/backend/src/ely_eye/profiles.py b/backend/src/ely_eye/profiles.py new file mode 100644 index 0000000..f91fe9a --- /dev/null +++ b/backend/src/ely_eye/profiles.py @@ -0,0 +1,420 @@ +from __future__ import annotations + +import json +import os +import shutil +import subprocess +from pathlib import Path + +from .schemas import RuntimeLaunchProfile, RuntimeProfileReport, RuntimeProfileValidation + + +EXTREME_YARN_OVERRIDE = { + "text_config": { + "rope_parameters": { + "mrope_interleaved": True, + "mrope_section": [11, 11, 10], + "rope_type": "yarn", + "rope_theta": 10_000_000, + "partial_rotary_factor": 0.25, + "factor": 4.0, + "original_max_position_embeddings": 262_144, + } + } +} + + +def runtime_profile_report(project_root: Path | None = None) -> RuntimeProfileReport: + root = project_root or Path.cwd() + profiles = [ + RuntimeLaunchProfile( + name="sglang-live", + runtime="SGLang", + role="Main VLM service with MTP speculative decoding and Qwen tool/parser support.", + context_tokens=262_144, + script="scripts/launch-sglang-live.ps1", + command=[ + "python", + "-m", + "sglang.launch_server", + "--model-path", + "Qwen/Qwen3.5-9B", + "--port", + "8000", + "--tp-size", + "1", + "--mem-fraction-static", + "0.82", + "--context-length", + "262144", + "--reasoning-parser", + "qwen3", + "--speculative-algo", + "NEXTN", + "--speculative-num-steps", + "3", + "--speculative-num-draft-tokens", + "4", + ], + required_features=["VLM", "MTP", "long-context", "tool-parser"], + ), + RuntimeLaunchProfile( + name="sglang-extreme-yarn", + runtime="SGLang", + role="1.01M YaRN extreme-context service.", + context_tokens=1_010_000, + script="scripts/launch-sglang-extreme.ps1", + command=[ + "python", + "-m", + "sglang.launch_server", + "--model-path", + "Qwen/Qwen3.5-9B", + "--port", + "8000", + "--tp-size", + "1", + "--mem-fraction-static", + "0.88", + "--context-length", + "1010000", + "--json-model-override-args", + "EXTREME_YARN_OVERRIDE", + ], + environment={"SGLANG_ALLOW_OVERWRITE_LONGER_CONTEXT_LEN": "1"}, + required_features=["YaRN", "long-context", "CPU-offload-compatible"], + ), + RuntimeLaunchProfile( + name="vllm-live", + runtime="vLLM", + role="PagedAttention and Automatic Prefix Caching service for live document QA.", + context_tokens=262_144, + script="scripts/launch-vllm-live.ps1", + command=[ + "python", + "-m", + "vllm.entrypoints.openai.api_server", + "--model", + "Qwen/Qwen3.5-9B", + "--host", + "127.0.0.1", + "--port", + "8001", + "--max-model-len", + "262144", + "--enable-prefix-caching", + "--gpu-memory-utilization", + "0.82", + "--trust-remote-code", + ], + required_features=["PagedAttention", "Automatic Prefix Caching", "OpenAI API"], + ), + RuntimeLaunchProfile( + name="vllm-extreme-yarn", + runtime="vLLM", + role="1.01M YaRN profile with prefix cache and quantized KV-compatible launch args.", + context_tokens=1_010_000, + script="scripts/launch-vllm-extreme.ps1", + command=[ + "python", + "-m", + "vllm.entrypoints.openai.api_server", + "--model", + "Qwen/Qwen3.5-9B", + "--host", + "127.0.0.1", + "--port", + "8001", + "--max-model-len", + "1010000", + "--enable-prefix-caching", + "--gpu-memory-utilization", + "0.88", + "--trust-remote-code", + "--rope-scaling", + "EXTREME_YARN_OVERRIDE", + ], + required_features=["YaRN", "PagedAttention", "Automatic Prefix Caching"], + ), + RuntimeLaunchProfile( + name="ktransformers-extreme", + runtime="KTransformers", + role="SGLang-KT CPU-GPU heterogeneous ultra-long-context experiment service.", + context_tokens=1_010_000, + script="scripts/launch-ktransformers-extreme.ps1", + command=[ + "python", + "-m", + "sglang.launch_server", + "--model-path", + "Qwen/Qwen3.5-9B", + "--port", + "8002", + "--context-length", + "1010000", + "--kt-cpuinfer", + "16", + "--cpu-offload-gb", + "48", + ], + required_features=["CPU-GPU heterogeneous execution", "ultra-long-context experiment"], + ), + RuntimeLaunchProfile( + name="llamacpp-offline", + runtime="llama.cpp", + role="GGUF offline and portable local demo service.", + context_tokens=262_144, + script="scripts/launch-llamacpp-offline.ps1", + command=[ + "llama-server", + "--model", + "$env:ELY_EYE_GGUF_MODEL", + "--host", + "127.0.0.1", + "--port", + "8003", + "--ctx-size", + "262144", + "--flash-attn", + "--cache-type-k", + "q4_0", + "--cache-type-v", + "q4_0", + ], + environment={"ELY_EYE_GGUF_MODEL": "required absolute GGUF path"}, + required_features=["GGUF", "offline demo", "quantized KV"], + ), + ] + validations = validate_runtime_profiles(root, profiles) + ready = all(validation.status == "ready" for validation in validations) + return RuntimeProfileReport( + status="ready" if ready else "incomplete", + profiles=profiles, + validations=validations, + ) + + +def validate_runtime_profiles( + root: Path, + profiles: list[RuntimeLaunchProfile], +) -> list[RuntimeProfileValidation]: + wsl_gpu = wsl_gpu_available() + package_cache = { + ".venv-linux": linux_package_versions(root, ".venv-linux"), + ".venv-linux-vllm": linux_package_versions(root, ".venv-linux-vllm"), + } + probe_cache: dict[tuple[str, str, str | None], dict[str, object]] = {} + llama_server = resolve_llama_server() + validations: list[RuntimeProfileValidation] = [] + for profile in profiles: + script_exists = (root / profile.script).exists() + package = package_for_runtime(profile.runtime) + env_name = linux_env_for_runtime(profile.runtime) + linux_packages = package_cache.get(env_name, {}) + package_version = linux_packages.get(package) if package else None + kt_sglang_version = linux_packages.get("sglang") if profile.runtime == "KTransformers" else None + executable = llama_server if profile.runtime == "llama.cpp" else None + probe_key = (profile.runtime, env_name, executable) + if probe_key not in probe_cache: + probe_cache[probe_key] = runtime_launcher_probe(root, profile.runtime, env_name, executable) + probe = probe_cache[probe_key] + probe_status = str(probe.get("status", "skipped")) + probe_passed = probe_status == "passed" + if profile.runtime in {"SGLang", "vLLM", "KTransformers"}: + ready = script_exists and bool(package_version) and wsl_gpu and probe_passed + if profile.runtime == "KTransformers": + ready = ready and bool(kt_sglang_version) + detail = ( + ( + f"WSL GPU ready; {package} {package_version}; " + f"sglang-kt entry {kt_sglang_version}" + ) + if profile.runtime == "KTransformers" and ready + else f"WSL GPU ready; {package} {package_version}; env {env_name}" + if ready + else ( + f"script={script_exists}, wsl_gpu={wsl_gpu}, env={env_name}, " + f"{package}={package_version}, sglang={kt_sglang_version}" + ) + ) + elif profile.runtime == "llama.cpp": + ready = script_exists and bool(executable) and probe_passed + detail = f"llama-server at {executable}" if ready else "llama-server executable missing" + else: + ready = script_exists + detail = f"script={script_exists}" + detail = f"{detail}; launcher_probe={probe_status}" + validations.append( + RuntimeProfileValidation( + name=profile.name, + runtime=profile.runtime, + status="ready" if ready else "missing", + script_exists=script_exists, + package=package, + package_version=package_version, + executable=executable, + wsl_gpu=wsl_gpu, + probe_command=[str(item) for item in probe.get("command", [])], + probe_status=probe_status, + probe_exit_code=probe.get("exit_code") if isinstance(probe.get("exit_code"), int) else None, + probe_output=probe.get("output") if isinstance(probe.get("output"), str) else None, + detail=detail, + ) + ) + return validations + + +def package_for_runtime(runtime: str) -> str | None: + return { + "SGLang": "sglang", + "vLLM": "vllm", + "KTransformers": "ktransformers", + }.get(runtime) + + +def linux_env_for_runtime(runtime: str) -> str: + if runtime == "vLLM": + return ".venv-linux-vllm" + return ".venv-linux" + + +def linux_package_versions(root: Path, env_name: str) -> dict[str, str]: + script = ( + "import importlib.metadata as md, json;" + "names=['sglang','vllm','ktransformers','torch','torchvision','qwen-vl-utils'];" + "out={};" + "\nfor name in names:\n" + " try: out[name]=md.version(name)\n" + " except Exception: pass\n" + "print(json.dumps(out))" + ) + try: + output = subprocess.check_output( + [ + "wsl.exe", + "-d", + "Ubuntu", + "bash", + "-lc", + ( + f"cd {wsl_quote(windows_path_to_wsl(root))} " + f"&& test -x {env_name}/bin/python " + f"&& {env_name}/bin/python -c {wsl_quote(script)}" + ), + ], + text=True, + stderr=subprocess.DEVNULL, + timeout=20, + ) + data = json.loads(output.strip()) + return {str(key): str(value) for key, value in data.items()} + except Exception: + return {} + + +def runtime_launcher_probe( + root: Path, + runtime: str, + env_name: str, + executable: str | None, +) -> dict[str, object]: + if runtime == "SGLang": + probe_script = ( + "import importlib.util, sys; " + "spec=importlib.util.find_spec('sglang.launch_server'); " + "print(spec.origin if spec else 'missing'); " + "sys.exit(0 if spec else 1)" + ) + command = wsl_python_command(root, env_name, f"-c {wsl_quote(probe_script)}") + elif runtime == "vLLM": + probe_script = ( + "import importlib.util, sys; " + "spec=importlib.util.find_spec('vllm.entrypoints.openai.api_server'); " + "print(spec.origin if spec else 'missing'); " + "sys.exit(0 if spec else 1)" + ) + command = wsl_python_command(root, env_name, f"-c {wsl_quote(probe_script)}") + elif runtime == "KTransformers": + probe_script = "import ktransformers, sglang; print('ktransformers and sglang import ok')" + command = wsl_python_command(root, env_name, f"-c {wsl_quote(probe_script)}") + elif runtime == "llama.cpp" and executable: + command = [executable, "--help"] + else: + return {"status": "skipped", "command": [], "output": "no launcher probe configured"} + try: + completed = subprocess.run( + command, + text=True, + capture_output=True, + timeout=30, + check=False, + ) + except Exception as exc: + return { + "status": "failed", + "command": command, + "exit_code": None, + "output": str(exc).splitlines()[0], + } + output = compact_output(completed.stdout, completed.stderr) + return { + "status": "passed" if completed.returncode == 0 else "failed", + "command": command, + "exit_code": completed.returncode, + "output": output, + } + + +def wsl_python_command(root: Path, env_name: str, python_args: str) -> list[str]: + shell_command = ( + f"cd {wsl_quote(windows_path_to_wsl(root))} " + f"&& test -x {env_name}/bin/python " + f"&& {env_name}/bin/python {python_args}" + ) + return ["wsl.exe", "-d", "Ubuntu", "bash", "-lc", shell_command] + + +def compact_output(stdout: str, stderr: str, limit: int = 800) -> str: + text = "\n".join(part.strip() for part in (stdout, stderr) if part.strip()) + return text[:limit] + + +def wsl_gpu_available() -> bool: + if shutil.which("wsl.exe") is None: + return False + try: + output = subprocess.check_output( + ["wsl.exe", "-d", "Ubuntu", "nvidia-smi", "-L"], + text=True, + stderr=subprocess.DEVNULL, + timeout=10, + ) + return "GPU" in output + except Exception: + return False + + +def resolve_llama_server() -> str | None: + configured = os.environ.get("LLAMA_CPP_SERVER") + if configured and Path(configured).exists(): + return configured + direct = shutil.which("llama-server") + if direct: + return direct + local_app_data = os.environ.get("LOCALAPPDATA") + if not local_app_data: + return None + package_root = Path(local_app_data) / "Microsoft" / "WinGet" / "Packages" + matches = sorted(package_root.glob("ggml.llamacpp_*/*llama-server.exe")) + return str(matches[-1]) if matches else None + + +def windows_path_to_wsl(path: Path) -> str: + resolved = path.resolve() + drive = resolved.drive.rstrip(":").lower() + parts = resolved.parts[1:] + return "/mnt/" + drive + "/" + "/".join(parts) + + +def wsl_quote(value: str) -> str: + return "'" + value.replace("'", "'\"'\"'") + "'" diff --git a/backend/src/ely_eye/research_modules.py b/backend/src/ely_eye/research_modules.py new file mode 100644 index 0000000..1c6a922 --- /dev/null +++ b/backend/src/ely_eye/research_modules.py @@ -0,0 +1,221 @@ +from __future__ import annotations + +import hashlib +import json +import math +from pathlib import Path +from typing import Any + +import numpy as np + +from .cache_fabric import CacheFabric +from .config import Settings, get_settings +from .db import Database +from .dvkv import VisualToken, select_dvkv_tokens +from .schemas import RetrievalHit + + +class ResearchModuleService: + def __init__(self, settings: Settings | None = None, database: Database | None = None) -> None: + self.settings = settings or get_settings() + self.db = database or Database(self.settings) + + def build_report(self, query: str, hits: list[RetrievalHit], output_path: Path | None = None) -> dict[str, Any]: + report = { + "v_nsa": self.v_nsa_plan(hits), + "dvkv": self.dvkv_plan(hits), + "x_modal_kv": self.x_modal_kv_plan(hits), + "speculative_prefetch": self.speculative_prefetch_plan(query, hits), + "predictive_eviction": self.predictive_eviction_plan(), + "differentiable_retrieval": self.differentiable_retrieval_distribution(hits), + "evidence_graph": self.evidence_graph_status(), + "sleep_consolidation": self.sleep_consolidation_plan(hits), + } + if output_path is not None: + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8") + return report + + def v_nsa_plan(self, hits: list[RetrievalHit]) -> dict[str, Any]: + heads = { + "visual_anchor_heads": [], + "text_retrieval_heads": [], + "streaming_heads": [], + } + for index, hit in enumerate(hits): + target = { + "atom_id": hit.atom.atom_id, + "modality": hit.atom.modality.value, + "score": hit.final_score, + "layout": hit.atom.layout.model_dump() if hit.atom.layout else None, + } + if hit.atom.image_ref or hit.atom.modality.value in {"image", "pdf_page", "pdf_region", "video_frame", "ui_screenshot"}: + heads["visual_anchor_heads"].append(target) + elif hit.final_score >= 0.45: + heads["text_retrieval_heads"].append(target) + else: + heads["streaming_heads"].append(target) + sparse_blocks = sorted( + heads["visual_anchor_heads"] + heads["text_retrieval_heads"], + key=lambda item: float(item["score"]), + reverse=True, + ) + return { + "head_roles": heads, + "sparse_block_count": len(sparse_blocks), + "sparse_blocks": sparse_blocks[:32], + } + + def dvkv_plan(self, hits: list[RetrievalHit], budget: int = 16) -> dict[str, Any]: + tokens = [ + VisualToken( + token_id=hit.atom.atom_id, + vector=stable_vector(hit.atom.atom_id + hit.atom.text, 64), + attention_importance=max(0.0, hit.final_score), + evidence_value=hit.atom.trust.parser * 0.45 + hit.atom.trust.ocr * 0.35 + hit.atom.trust.model * 0.20, + ) + for hit in hits + if hit.atom.image_ref or hit.atom.layout is not None + ] + if not tokens: + visual_atoms = [atom for atom in self.db.list_atoms() if atom.image_ref] + tokens = [ + VisualToken( + token_id=atom.atom_id, + vector=stable_vector(atom.atom_id + atom.source, 64), + attention_importance=1.0 / (index + 1), + evidence_value=atom.trust.parser * 0.45 + atom.trust.ocr * 0.35 + atom.trust.model * 0.20, + ) + for index, atom in enumerate(visual_atoms[:budget]) + ] + selected = select_dvkv_tokens(tokens, min(budget, max(1, len(tokens)))) if tokens else [] + return { + "candidate_tokens": len(tokens), + "budget": budget, + "selected": [ + { + "token_id": token.token_id, + "attention_importance": token.attention_importance, + "evidence_value": token.evidence_value, + } + for token in selected + ], + } + + def x_modal_kv_plan(self, hits: list[RetrievalHit]) -> dict[str, Any]: + groups: dict[str, list[dict[str, Any]]] = {} + for hit in hits: + source_key = hashlib.sha256(hit.atom.source_id.encode("utf-8")).hexdigest()[:16] + groups.setdefault(source_key, []).append( + { + "atom_id": hit.atom.atom_id, + "modality": hit.atom.modality.value, + "has_image": bool(hit.atom.image_ref), + "score": hit.final_score, + } + ) + shared_blocks = [ + { + "block_id": f"xkv_{source_key}", + "source_key": source_key, + "modalities": sorted({item["modality"] for item in items}), + "atom_ids": [item["atom_id"] for item in sorted(items, key=lambda value: value["score"], reverse=True)], + } + for source_key, items in groups.items() + ] + return {"shared_block_count": len(shared_blocks), "shared_blocks": shared_blocks} + + def speculative_prefetch_plan(self, query: str, hits: list[RetrievalHit]) -> dict[str, Any]: + query_terms = {term for term in query.lower().split() if len(term) > 2} + candidates = [] + for hit in hits: + text_terms = set(hit.atom.text.lower().split()) + lexical_overlap = len(query_terms & text_terms) / max(1, len(query_terms)) + priority = hit.final_score * 0.75 + lexical_overlap * 0.25 + candidates.append( + { + "cache_key": hashlib.sha256(hit.atom.atom_id.encode("utf-8")).hexdigest()[:16], + "atom_id": hit.atom.atom_id, + "priority": priority, + "bytes_estimate": max(128, hit.atom.token_equivalent * 16), + } + ) + candidates.sort(key=lambda item: item["priority"], reverse=True) + return {"prefetch_blocks": candidates[:8], "planner_model": self.settings.planner_model_id} + + def predictive_eviction_plan(self) -> dict[str, Any]: + layers = CacheFabric(self.settings, self.db).statuses() + rows = [] + for layer in layers: + total = max(1, layer.hit_count + layer.miss_count) + hit_rate = layer.hit_count / total + rows.append( + { + "layer": layer.layer, + "entries": layer.entries, + "hit_rate": hit_rate, + "evict": hit_rate < 0.25 and layer.entries > 0, + "replay_token": hashlib.sha256(f"{layer.layer}:{layer.entries}".encode("utf-8")).hexdigest()[:16], + } + ) + return {"policy": "hit-rate weighted replay buffer", "layers": rows} + + def differentiable_retrieval_distribution(self, hits: list[RetrievalHit], temperature: float = 0.05) -> dict[str, Any]: + if not hits: + return {"temperature": temperature, "probabilities": []} + scores = np.array([hit.final_score for hit in hits], dtype=np.float64) + scaled = scores / max(temperature, 1e-6) + scaled -= float(np.max(scaled)) + probs = np.exp(scaled) + probs /= probs.sum() + top_index = int(np.argmax(probs)) + return { + "temperature": temperature, + "estimator": "softmax distribution with straight-through top-1 selection", + "straight_through_atom_id": hits[top_index].atom.atom_id, + "probabilities": [ + {"atom_id": hit.atom.atom_id, "probability": float(prob)} + for hit, prob in zip(hits, probs, strict=True) + ], + } + + def evidence_graph_status(self) -> dict[str, Any]: + edges = self.db.list_evidence_edges(limit=64) + return { + "edge_count_sample": len(edges), + "relations": sorted({edge["relation"] for edge in edges}), + "recent_edges": edges[:16], + } + + def sleep_consolidation_plan(self, hits: list[RetrievalHit]) -> dict[str, Any]: + durable = [ + { + "atom_id": hit.atom.atom_id, + "reason": "high score and cited-ready evidence", + "score": hit.final_score, + } + for hit in hits + if hit.final_score >= 0.25 + ] + return { + "target_adapter": "hme_ttt_vl", + "target_cartridge": "ely_eye_prd", + "candidate_count": len(durable), + "durable_candidates": durable[:32], + } + + +def stable_vector(seed: str, dims: int) -> np.ndarray: + digest = hashlib.sha256(seed.encode("utf-8")).digest() + values = [] + while len(values) < dims: + for byte in digest: + values.append((byte / 255.0) * 2.0 - 1.0) + if len(values) == dims: + break + digest = hashlib.sha256(digest).digest() + vector = np.array(values, dtype=np.float32) + norm = float(math.sqrt(float(vector @ vector))) + if norm > 0: + vector /= norm + return vector diff --git a/backend/src/ely_eye/retrieval.py b/backend/src/ely_eye/retrieval.py new file mode 100644 index 0000000..27daa02 --- /dev/null +++ b/backend/src/ely_eye/retrieval.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +import re +from collections import defaultdict + +from rank_bm25 import BM25Okapi + +from .config import Settings, get_settings +from .db import Database +from .embeddings import EmbeddingService +from .schemas import EvidenceAtom, RetrievalHit + + +class RetrievalService: + def __init__(self, settings: Settings | None = None, database: Database | None = None) -> None: + self.settings = settings or get_settings() + self.db = database or Database(self.settings) + self.embeddings = EmbeddingService(self.settings, self.db) + + def search(self, query: str, top_k: int | None = None) -> list[RetrievalHit]: + limit = top_k or self.settings.retrieval_top_k + atoms = self.db.list_atoms() + if not atoms: + return [] + + sparse_scores = self._sparse_search(query, atoms, limit * 3) + dense_scores: dict[str, float] = {} + if self.embeddings.enabled(): + dense_scores = self._dense_search(query, limit * 3) + + all_ids = set(sparse_scores) | set(dense_scores) + atom_map = {atom.atom_id: atom for atom in atoms} + hits: list[RetrievalHit] = [] + for atom_id in all_ids: + atom = atom_map.get(atom_id) + if atom is None: + continue + sparse_score = sparse_scores.get(atom_id, 0.0) + dense_score = dense_scores.get(atom_id, 0.0) + graph_score = self._graph_score(atom, query) + final_score = sparse_score * 0.55 + dense_score * 0.35 + graph_score * 0.10 + hits.append( + RetrievalHit( + atom=atom, + sparse_score=sparse_score, + dense_score=dense_score, + graph_score=graph_score, + final_score=final_score, + ) + ) + hits.sort(key=lambda hit: hit.final_score, reverse=True) + return hits[:limit] + + def _sparse_search(self, query: str, atoms: list[EvidenceAtom], limit: int) -> dict[str, float]: + tokenized = [tokenize(atom.text + " " + atom.source) for atom in atoms] + bm25 = BM25Okapi(tokenized) + query_tokens = tokenize(query) + scores = bm25.get_scores(query_tokens) + lexical_scores = [lexical_score(query_tokens, tokens, atoms[index]) for index, tokens in enumerate(tokenized)] + combined = [max(float(scores[index]), lexical_scores[index]) for index in range(len(atoms))] + order = sorted(range(len(combined)), key=lambda index: combined[index], reverse=True)[:limit] + if not order: + return {} + max_score = max(float(combined[index]) for index in order) or 1.0 + return {atoms[index].atom_id: float(combined[index]) / max_score for index in order if combined[index] > 0} + + def _dense_search(self, query: str, limit: int) -> dict[str, float]: + index = self.embeddings.load_index() + if index is None: + index = self.embeddings.build_index() + query_vector = self.embeddings.embed_query(query) + return dict(index.search(query_vector, limit)) + + def _graph_score(self, atom: EvidenceAtom, query: str) -> float: + query_tokens = set(tokenize(query)) + relation_tokens = set() + for relation in atom.relations: + relation_tokens.update(tokenize(relation)) + if not query_tokens or not relation_tokens: + return 0.0 + return len(query_tokens & relation_tokens) / len(query_tokens) + + +def tokenize(text: str) -> list[str]: + return [token.lower() for token in re.findall(r"[\w\u4e00-\u9fff]+", text)] + + +def lexical_score(query_tokens: list[str], document_tokens: list[str], atom: EvidenceAtom) -> float: + if not query_tokens or not document_tokens: + return 0.0 + document_set = set(document_tokens) + overlap = sum(1 for token in query_tokens if token in document_set) + phrase_hits = sum(1 for token in query_tokens if token and token in atom.text.lower()) + return (overlap + 0.25 * phrase_hits) / len(query_tokens) + + +def group_hits_by_source(hits: list[RetrievalHit]) -> dict[str, list[RetrievalHit]]: + grouped: dict[str, list[RetrievalHit]] = defaultdict(list) + for hit in hits: + grouped[hit.atom.source].append(hit) + return dict(grouped) diff --git a/backend/src/ely_eye/runtime.py b/backend/src/ely_eye/runtime.py new file mode 100644 index 0000000..b851ee8 --- /dev/null +++ b/backend/src/ely_eye/runtime.py @@ -0,0 +1,454 @@ +from __future__ import annotations + +import base64 +import json +import subprocess +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +import httpx + +from .config import Settings, get_settings +from .schemas import AdapterManifest, CompiledContext, RuntimeStatus, TrainingAdapterKind +from .storage import ObjectStore + + +class RuntimeUnavailableError(RuntimeError): + pass + + +@dataclass +class RuntimeGeneration: + text: str + backend: str + adapter_path: str | None = None + adapter_kind: str | None = None + adapter_id: str | None = None + adapter_sha256: str | None = None + input_tokens: int | None = None + output_tokens: int | None = None + + +class QwenRuntime: + def __init__(self, settings: Settings | None = None) -> None: + self.settings = settings or get_settings() + self.objects = ObjectStore(self.settings) + self._processor: Any | None = None + self._model: Any | None = None + + def status(self) -> RuntimeStatus: + gpu = gpu_status() + adapter = resolve_runtime_adapter(self.settings) + if self.settings.runtime_backend == "sglang": + available, detail = self._sglang_status() + else: + available, detail = self._transformers_status() + return RuntimeStatus( + backend=self.settings.runtime_backend, + model_id=self.settings.model_id, + available=available, + detail=detail, + adapter_path=str(adapter.path) if adapter else None, + adapter_kind=adapter.manifest.kind if adapter else None, + adapter_id=adapter.manifest.adapter_id if adapter else None, + adapter_sha256=adapter.manifest.sha256 if adapter else None, + gpu_name=gpu.get("name"), + cuda=gpu.get("cuda"), + vram_total_mb=gpu.get("total_mb"), + vram_used_mb=gpu.get("used_mb"), + ) + + def generate(self, question: str, context: CompiledContext) -> RuntimeGeneration: + if self.settings.runtime_backend == "sglang": + return self._generate_sglang(question, context) + return self._generate_transformers(question, context) + + def generate_prompt(self, prompt: str, image_paths: list[Path] | None = None) -> RuntimeGeneration: + if self.settings.runtime_backend == "sglang": + return self._generate_sglang_prompt(prompt, image_paths or []) + return self._generate_transformers_prompt(prompt, image_paths or []) + + def _sglang_status(self) -> tuple[bool, str]: + try: + with httpx.Client(timeout=2.0) as client: + response = client.get(f"{self.settings.sglang_base_url}/models") + if response.status_code == 200: + return True, "SGLang OpenAI-compatible endpoint is reachable." + return False, f"SGLang endpoint returned HTTP {response.status_code}." + except httpx.HTTPError as exc: + return False, f"SGLang endpoint is not reachable: {exc}" + + def _transformers_status(self) -> tuple[bool, str]: + try: + import torch + + adapter_detail = "" + adapter = resolve_runtime_adapter(self.settings) + if adapter is not None: + adapter_detail = f" Adapter selected: {adapter.path}." + elif self.settings.adapter_dir is not None: + return False, f"Configured adapter path is missing: {self.settings.adapter_dir}" + if torch.cuda.is_available(): + return True, f"Transformers backend can use CUDA.{adapter_detail}" + return True, f"Transformers backend is available on CPU.{adapter_detail}" + except Exception as exc: + return False, f"Transformers backend import failed: {exc}" + + def _generate_sglang(self, question: str, context: CompiledContext) -> RuntimeGeneration: + prompt = build_prompt(question, context) + content: list[dict[str, Any]] = [{"type": "text", "text": prompt}] + for image_path in self._context_images(context)[:6]: + content.append( + { + "type": "image_url", + "image_url": {"url": image_data_url(image_path)}, + } + ) + payload = { + "model": self.settings.model_id, + "messages": [{"role": "user", "content": content}], + "temperature": self.settings.temperature, + "max_tokens": self.settings.max_new_tokens, + } + try: + with httpx.Client(timeout=None) as client: + response = client.post( + f"{self.settings.sglang_base_url}/chat/completions", + headers={"Authorization": f"Bearer {self.settings.sglang_api_key}"}, + json=payload, + ) + response.raise_for_status() + except httpx.HTTPError as exc: + raise RuntimeUnavailableError(f"SGLang generation failed: {exc}") from exc + data = response.json() + adapter = resolve_runtime_adapter(self.settings) + usage = data.get("usage") if isinstance(data, dict) else None + return RuntimeGeneration( + text=data["choices"][0]["message"]["content"], + backend="sglang", + adapter_path=str(adapter.path) if adapter else None, + adapter_kind=adapter.manifest.kind.value if adapter else None, + adapter_id=adapter.manifest.adapter_id if adapter else None, + adapter_sha256=adapter.manifest.sha256 if adapter else None, + input_tokens=usage.get("prompt_tokens") if isinstance(usage, dict) else None, + output_tokens=usage.get("completion_tokens") if isinstance(usage, dict) else None, + ) + + def _generate_sglang_prompt(self, prompt: str, image_paths: list[Path]) -> RuntimeGeneration: + content: list[dict[str, Any]] = [{"type": "text", "text": prompt}] + for image_path in image_paths[:6]: + content.append( + { + "type": "image_url", + "image_url": {"url": image_data_url(image_path)}, + } + ) + payload = { + "model": self.settings.model_id, + "messages": [{"role": "user", "content": content}], + "temperature": self.settings.temperature, + "max_tokens": self.settings.max_new_tokens, + } + try: + with httpx.Client(timeout=None) as client: + response = client.post( + f"{self.settings.sglang_base_url}/chat/completions", + headers={"Authorization": f"Bearer {self.settings.sglang_api_key}"}, + json=payload, + ) + response.raise_for_status() + except httpx.HTTPError as exc: + raise RuntimeUnavailableError(f"SGLang generation failed: {exc}") from exc + data = response.json() + adapter = resolve_runtime_adapter(self.settings) + usage = data.get("usage") if isinstance(data, dict) else None + return RuntimeGeneration( + text=data["choices"][0]["message"]["content"], + backend="sglang", + adapter_path=str(adapter.path) if adapter else None, + adapter_kind=adapter.manifest.kind.value if adapter else None, + adapter_id=adapter.manifest.adapter_id if adapter else None, + adapter_sha256=adapter.manifest.sha256 if adapter else None, + input_tokens=usage.get("prompt_tokens") if isinstance(usage, dict) else None, + output_tokens=usage.get("completion_tokens") if isinstance(usage, dict) else None, + ) + + def _generate_transformers(self, question: str, context: CompiledContext) -> RuntimeGeneration: + model, processor = self._load_transformers() + prompt = build_prompt(question, context) + images = [str(path) for path in self._context_images(context)[:6]] + messages: list[dict[str, Any]] = [ + { + "role": "user", + "content": [{"type": "text", "text": prompt}] + + [{"type": "image", "image": image_path} for image_path in images], + } + ] + import torch + + text = processor.apply_chat_template( + messages, + tokenize=False, + add_generation_prompt=True, + enable_thinking=False, + ) + image_inputs: list[Any] | None = None + video_inputs: list[Any] | None = None + if images: + from qwen_vl_utils import process_vision_info + + image_inputs, video_inputs = process_vision_info(messages) + inputs = processor( + text=[text], + images=image_inputs, + videos=video_inputs, + return_tensors="pt", + ) + inputs = {key: value.to(model.device) if hasattr(value, "to") else value for key, value in inputs.items()} + generate_kwargs: dict[str, Any] = { + **inputs, + "max_new_tokens": self.settings.max_new_tokens, + "do_sample": self.settings.temperature > 0, + } + if self.settings.temperature > 0: + generate_kwargs["temperature"] = self.settings.temperature + with torch.inference_mode(): + generated = model.generate(**generate_kwargs) + input_length = int(inputs["input_ids"].shape[-1]) + generated = generated[:, input_length:] + decoded = processor.batch_decode(generated, skip_special_tokens=True)[0] + adapter = resolve_runtime_adapter(self.settings) + return RuntimeGeneration( + text=decoded, + backend="transformers", + adapter_path=str(adapter.path) if adapter else None, + adapter_kind=adapter.manifest.kind.value if adapter else None, + adapter_id=adapter.manifest.adapter_id if adapter else None, + adapter_sha256=adapter.manifest.sha256 if adapter else None, + input_tokens=input_length, + output_tokens=int(generated.shape[-1]), + ) + + def _generate_transformers_prompt(self, prompt: str, image_paths: list[Path]) -> RuntimeGeneration: + model, processor = self._load_transformers() + images = [str(path) for path in image_paths[:6]] + messages: list[dict[str, Any]] = [ + { + "role": "user", + "content": [{"type": "text", "text": prompt}] + + [{"type": "image", "image": image_path} for image_path in images], + } + ] + import torch + + text = processor.apply_chat_template( + messages, + tokenize=False, + add_generation_prompt=True, + enable_thinking=False, + ) + image_inputs: list[Any] | None = None + video_inputs: list[Any] | None = None + if images: + from qwen_vl_utils import process_vision_info + + image_inputs, video_inputs = process_vision_info(messages) + inputs = processor( + text=[text], + images=image_inputs, + videos=video_inputs, + return_tensors="pt", + ) + inputs = {key: value.to(model.device) if hasattr(value, "to") else value for key, value in inputs.items()} + generate_kwargs: dict[str, Any] = { + **inputs, + "max_new_tokens": self.settings.max_new_tokens, + "do_sample": self.settings.temperature > 0, + } + if self.settings.temperature > 0: + generate_kwargs["temperature"] = self.settings.temperature + with torch.inference_mode(): + generated = model.generate(**generate_kwargs) + input_length = int(inputs["input_ids"].shape[-1]) + generated = generated[:, input_length:] + decoded = processor.batch_decode(generated, skip_special_tokens=True)[0] + adapter = resolve_runtime_adapter(self.settings) + return RuntimeGeneration( + text=decoded, + backend="transformers", + adapter_path=str(adapter.path) if adapter else None, + adapter_kind=adapter.manifest.kind.value if adapter else None, + adapter_id=adapter.manifest.adapter_id if adapter else None, + adapter_sha256=adapter.manifest.sha256 if adapter else None, + input_tokens=input_length, + output_tokens=int(generated.shape[-1]), + ) + + def _load_transformers(self) -> tuple[Any, Any]: + if self._model is not None and self._processor is not None: + return self._model, self._processor + import torch + from transformers import AutoProcessor + + try: + from transformers import AutoModelForImageTextToText + + model_cls = AutoModelForImageTextToText + except ImportError: + from transformers import AutoModelForCausalLM + + model_cls = AutoModelForCausalLM + + self._processor = AutoProcessor.from_pretrained(self.settings.model_id, trust_remote_code=True) + self._model = model_cls.from_pretrained( + self.settings.model_id, + dtype=torch.bfloat16 if torch.cuda.is_available() else torch.float32, + device_map="auto", + trust_remote_code=True, + ) + adapter = resolve_runtime_adapter(self.settings) + if adapter is not None: + from .training import bitsandbytes_gaudi_probe_guard + + with bitsandbytes_gaudi_probe_guard(): + from peft import PeftModel + + self._model = PeftModel.from_pretrained(self._model, adapter.path) + self._model.eval() + return self._model, self._processor + + def unload(self) -> None: + self._model = None + self._processor = None + try: + import torch + + if torch.cuda.is_available(): + torch.cuda.empty_cache() + except Exception: + return + + def _context_images(self, context: CompiledContext) -> list[Path]: + paths: list[Path] = [] + for hit in context.hits: + image_ref = hit.atom.image_ref + if image_ref: + paths.append(self.objects.resolve(image_ref)) + return paths + + +@dataclass(frozen=True) +class RuntimeAdapter: + path: Path + manifest: AdapterManifest + + +def resolve_runtime_adapter( + settings: Settings, + kind: TrainingAdapterKind = TrainingAdapterKind.hme_core_lora, +) -> RuntimeAdapter | None: + if settings.adapter_dir is not None: + path = settings.adapter_dir.expanduser().resolve() + manifest_path = path / "adapter_manifest.json" + if path.exists() and manifest_path.exists(): + return RuntimeAdapter( + path=path, + manifest=AdapterManifest.model_validate_json(manifest_path.read_text(encoding="utf-8")), + ) + return None + + candidates: list[RuntimeAdapter] = [] + for manifest_path in settings.adapters_dir.glob("*/adapter_manifest.json"): + manifest = AdapterManifest.model_validate_json(manifest_path.read_text(encoding="utf-8")) + if manifest.kind == kind and manifest.base_model == settings.model_id: + candidates.append(RuntimeAdapter(path=manifest_path.parent.resolve(), manifest=manifest)) + candidates.sort( + key=lambda item: ( + item.manifest.cartridge_id is not None, + (item.path / "adapter_model.safetensors").stat().st_mtime + if (item.path / "adapter_model.safetensors").exists() + else 0.0, + ), + reverse=True, + ) + return candidates[0] if candidates else None + + +def build_prompt(question: str, context: CompiledContext) -> str: + contract = "\n".join(f"- {item}" for item in context.plan.verifier_contract) + return f"""You are Ely-Eye, a local multimodal context operating system. + +Use /no_think mode. Return final JSON only. Use only the provided evidence. Include atom ids in citations. + +Verifier contract: +{contract} + +Context plan: +{context.plan.model_dump_json(indent=2)} + +Packed evidence: +{context.packed_text} + +Question: +{question} + +Return compact JSON only: +{{ + "answer": "120 words or fewer", + "citations": ["atom_id strings only"], + "contradiction_notes": ["strings"], + "confidence": 0.0 +}} +""" + + +def image_data_url(path: Path) -> str: + suffix = path.suffix.lower().lstrip(".") or "png" + mime = "jpeg" if suffix in {"jpg", "jpeg"} else suffix + encoded = base64.b64encode(path.read_bytes()).decode("ascii") + return f"data:image/{mime};base64,{encoded}" + + +def gpu_status() -> dict[str, Any]: + try: + output = subprocess.check_output( + [ + "nvidia-smi", + "--query-gpu=name,memory.total,memory.used,driver_version", + "--format=csv,noheader,nounits", + ], + text=True, + stderr=subprocess.STDOUT, + timeout=5, + ).strip() + except (subprocess.SubprocessError, FileNotFoundError): + return {} + if not output: + return {} + first = output.splitlines()[0] + parts = [part.strip() for part in first.split(",")] + if len(parts) < 4: + return {} + return { + "name": parts[0], + "total_mb": int(float(parts[1])), + "used_mb": int(float(parts[2])), + "cuda": f"driver {parts[3]}", + } + + +def parse_runtime_json(text: str) -> dict[str, Any]: + stripped = text.strip() + if stripped.startswith("```"): + stripped = stripped.strip("`") + if stripped.lower().startswith("json"): + stripped = stripped[4:].strip() + try: + return json.loads(stripped) + except json.JSONDecodeError: + start = stripped.find("{") + end = stripped.rfind("}") + if start >= 0 and end > start: + return json.loads(stripped[start : end + 1]) + raise diff --git a/backend/src/ely_eye/schemas.py b/backend/src/ely_eye/schemas.py new file mode 100644 index 0000000..c7c7919 --- /dev/null +++ b/backend/src/ely_eye/schemas.py @@ -0,0 +1,479 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from enum import StrEnum +from pathlib import Path +from typing import Any, Literal + +from pydantic import BaseModel, Field + + +def utc_now() -> datetime: + return datetime.now(timezone.utc) + + +class RuntimeProfile(StrEnum): + live_demo = "live_demo" + extreme_context = "extreme_context" + library_100m = "library_100m" + research_theater = "research_theater" + + +class TrainingAdapterKind(StrEnum): + hme_core_lora = "hme_core_lora" + hme_vision_lora = "hme_vision_lora" + hme_ttt_vl = "hme_ttt_vl" + hme_visual_mtp = "hme_visual_mtp" + hme_router = "hme_router" + hme_retrieval = "hme_retrieval" + + +class Modality(StrEnum): + text = "text" + code = "code" + image = "image" + pdf_page = "pdf_page" + pdf_region = "pdf_region" + video_frame = "video_frame" + ui_screenshot = "ui_screenshot" + mixed = "mixed" + + +class LayoutBox(BaseModel): + x: float + y: float + w: float + h: float + page: int | None = None + frame_second: float | None = None + + +class TrustScores(BaseModel): + parser: float = Field(ge=0.0, le=1.0) + ocr: float = Field(default=0.0, ge=0.0, le=1.0) + model: float = Field(default=0.0, ge=0.0, le=1.0) + + +class EvidenceAtom(BaseModel): + atom_id: str + modality: Modality + source_id: str + source: str + time: datetime = Field(default_factory=utc_now) + text: str = "" + image_ref: str | None = None + embedding_refs: list[str] = Field(default_factory=list) + layout: LayoutBox | None = None + relations: list[str] = Field(default_factory=list) + trust: TrustScores + token_equivalent: int = Field(default=0, ge=0) + metadata: dict[str, Any] = Field(default_factory=dict) + + +class SourceRecord(BaseModel): + source_id: str + path: str + kind: str + mime: str + sha256: str + size_bytes: int + created_at: datetime = Field(default_factory=utc_now) + metadata: dict[str, Any] = Field(default_factory=dict) + + +class IngestResponse(BaseModel): + source_count: int + atom_count: int + cartridge_id: str | None + token_equivalent: int + sources: list[SourceRecord] + + +class RetrievalHit(BaseModel): + atom: EvidenceAtom + sparse_score: float + dense_score: float = 0.0 + graph_score: float = 0.0 + final_score: float + + +class ContextPlan(BaseModel): + question: str + profile: RuntimeProfile + required_modalities: list[Modality] + evidence_budget_atoms: int + token_budget: int + retrieval_query: str + compression_strategy: str + verifier_contract: list[str] + + +class CompiledContext(BaseModel): + plan: ContextPlan + hits: list[RetrievalHit] + packed_text: str + token_equivalent: int + cache_trace_id: str + + +class CitationReport(BaseModel): + cited_atom_ids: list[str] + missing_atom_ids: list[str] + citation_accuracy: float + contradiction_notes: list[str] + confidence: float + + +class AnswerResponse(BaseModel): + answer: str + citations: list[str] + context: CompiledContext + verifier: CitationReport + runtime_backend: str + + +class CacheLayerStatus(BaseModel): + layer: str + entries: int + bytes_estimate: int + hit_count: int + miss_count: int + last_event_at: datetime | None + + +class MemoryStatus(BaseModel): + active_atoms: int + active_token_equivalent: int + cartridges: int + library_token_equivalent: int + layers: list[CacheLayerStatus] + + +class RuntimeStatus(BaseModel): + backend: str + model_id: str + available: bool + detail: str + adapter_path: str | None = None + adapter_kind: TrainingAdapterKind | None = None + adapter_id: str | None = None + adapter_sha256: str | None = None + gpu_name: str | None = None + cuda: str | None = None + vram_total_mb: int | None = None + vram_used_mb: int | None = None + + +class SystemStatus(BaseModel): + app: str = "Ely-Eye" + version: str + runtime: RuntimeStatus + memory: MemoryStatus + profiles: dict[str, dict[str, Any]] + data_home: str + + +class CartridgeManifest(BaseModel): + cartridge_id: str + name: str + created_at: datetime + model_base: str + source_count: int + atom_count: int + token_equivalent: int + artifacts: dict[str, str] + dna: str | None = None + + +class AdapterManifest(BaseModel): + adapter_id: str + kind: TrainingAdapterKind + created_at: datetime + proof_version: int = 1 + training_method: str = "BF16 LoRA" + precision: str = "bf16" + framework: str = "unsloth_patched_transformers_peft" + base_model: str + dataset_path: str + dataset_sha256: str + sample_count: int + max_steps: int + max_length: int + rank: int + alpha: int + target_modules: list[str] + trainable_params: int + total_params: int + optimizer: str + optimizer_family: str | None = None + learning_rate: float + gradient_accumulation_steps: int + gradient_checkpointing: bool = False + gradient_checkpointing_mode: str | None = None + autocast_dtype: str | None = None + unsloth_version: str | None = None + triton_version: str | None = None + xformers_version: str | None = None + unsloth_model_type: str | None = None + bf16: bool + final_loss: float | None + loss_history: list[float] = Field(default_factory=list) + initial_trainable_sha256: str | None = None + final_trainable_sha256: str | None = None + initial_trainable_l2: float | None = None + final_trainable_l2: float | None = None + optimizer_update_count: int = 0 + max_optimizer_update_l2: float | None = None + loss_delta: float | None = None + training_proof_path: str | None = None + training_proof_sha256: str | None = None + training_trace_path: str | None = None + training_trace_sha256: str | None = None + training_summary_path: str | None = None + training_summary_sha256: str | None = None + training_started_at: datetime | None = None + training_finished_at: datetime | None = None + training_wall_seconds: float | None = None + training_device: str | None = None + torch_version: str | None = None + cuda_version: str | None = None + adapter_weight_sha256: str | None = None + adapter_tensor_count: int = 0 + adapter_lora_tensor_count: int = 0 + adapter_total_elements: int = 0 + adapter_nonzero_elements: int = 0 + adapter_max_abs: float | None = None + adapter_weights_finite: bool = False + artifacts: dict[str, str] + sha256: str + cartridge_id: str | None = None + + +class AdapterStatus(BaseModel): + name: str + kind: TrainingAdapterKind + proof_version: int = 1 + training_method: str = "BF16 LoRA" + precision: str = "bf16" + framework: str = "unsloth_patched_transformers_peft" + base_model: str + path: str + adapter_id: str + sample_count: int + max_steps: int + trainable_params: int + final_loss: float | None + sha256: str + weight_bytes: int + safetensor_keys: int + cartridge_id: str | None = None + cartridge_bound: bool + training_trace_path: str | None = None + training_trace_sha256: str | None = None + training_trace_steps: int = 0 + training_proof_path: str | None = None + training_proof_sha256: str | None = None + optimizer: str | None = None + optimizer_family: str | None = None + gradient_checkpointing: bool = False + gradient_checkpointing_mode: str | None = None + autocast_dtype: str | None = None + unsloth_version: str | None = None + triton_version: str | None = None + xformers_version: str | None = None + unsloth_model_type: str | None = None + optimizer_update_count: int = 0 + max_optimizer_update_l2: float | None = None + loss_delta: float | None = None + training_summary_path: str | None = None + training_wall_seconds: float | None = None + training_device: str | None = None + bf16: bool = False + torch_version: str | None = None + cuda_version: str | None = None + adapter_weight_sha256: str | None = None + adapter_tensor_count: int = 0 + adapter_lora_tensor_count: int = 0 + adapter_total_elements: int = 0 + adapter_nonzero_elements: int = 0 + adapter_max_abs: float | None = None + adapter_weights_finite: bool = False + + +class CartridgeAssetStatus(BaseModel): + name: str + path: str + sha256: str + size_bytes: int + item_count: int + dtype: str + status: Literal["ready", "missing"] + + +class CartridgeAssetReport(BaseModel): + cartridge_id: str + status: Literal["ready", "incomplete"] + created_at: datetime = Field(default_factory=utc_now) + assets: list[CartridgeAssetStatus] + + +class BenchmarkSpec(BaseModel): + name: str + target: str + dataset: str + task_type: str + runner: str + metric: str + + +class BenchmarkRegistryReport(BaseModel): + status: Literal["ready", "incomplete"] + created_at: datetime = Field(default_factory=utc_now) + benchmarks: list[BenchmarkSpec] + + +class BenchmarkSourceStatus(BaseModel): + name: str + dataset: str + status: Literal["ready", "unavailable"] + sha: str | None = None + gated: bool | str | None = None + private: bool | None = None + sibling_count: int = 0 + error: str | None = None + + +class BenchmarkSourceReport(BaseModel): + status: Literal["ready", "incomplete"] + created_at: datetime = Field(default_factory=utc_now) + sources: list[BenchmarkSourceStatus] + + +class BenchmarkSampleStatus(BaseModel): + name: str + dataset: str + source_dataset: str | None = None + source_sha: str | None = None + source_note: str | None = None + status: Literal["loaded", "gated", "failed"] + config: str | None = None + split: str | None = None + sample_id: str | None = None + fields: list[str] = Field(default_factory=list) + question_preview: str | None = None + answer_preview: str | None = None + sample_sha256: str | None = None + media_artifacts: list[str] = Field(default_factory=list) + error: str | None = None + + +class BenchmarkSampleReport(BaseModel): + status: Literal["ready", "partial", "failed"] + created_at: datetime = Field(default_factory=utc_now) + samples: list[BenchmarkSampleStatus] + + +class BenchmarkPredictionStatus(BaseModel): + name: str + dataset: str + status: Literal["predicted", "gated", "failed"] + config: str | None = None + split: str | None = None + sample_id: str | None = None + task_type: str + metric: str + prompt_sha256: str | None = None + sample_sha256: str | None = None + prediction: str | None = None + answer: str | None = None + score: float | None = None + runtime_backend: str | None = None + adapter_id: str | None = None + adapter_sha256: str | None = None + input_tokens: int | None = None + output_tokens: int | None = None + media_artifacts: list[str] = Field(default_factory=list) + error: str | None = None + + +class BenchmarkPredictionReport(BaseModel): + status: Literal["ready", "partial", "failed"] + created_at: datetime = Field(default_factory=utc_now) + predictions: list[BenchmarkPredictionStatus] + + +class RuntimeLaunchProfile(BaseModel): + name: str + runtime: str + role: str + context_tokens: int + script: str + command: list[str] + environment: dict[str, str] = Field(default_factory=dict) + required_features: list[str] = Field(default_factory=list) + + +class RuntimeProfileValidation(BaseModel): + name: str + runtime: str + status: Literal["ready", "missing"] + script_exists: bool + package: str | None = None + package_version: str | None = None + executable: str | None = None + wsl_gpu: bool = False + probe_command: list[str] = Field(default_factory=list) + probe_status: Literal["passed", "failed", "skipped"] = "skipped" + probe_exit_code: int | None = None + probe_output: str | None = None + detail: str + + +class RuntimeProfileReport(BaseModel): + status: Literal["ready", "incomplete"] + created_at: datetime = Field(default_factory=utc_now) + profiles: list[RuntimeLaunchProfile] + validations: list[RuntimeProfileValidation] = Field(default_factory=list) + + +class HashHopProof(BaseModel): + proof_id: str + kind: Literal["text_hashhop", "visual_hashhop"] + hops: int + token_equivalent: int + query_id: str + expected_target_id: str + model_target_id: str | None = None + passed: bool | None = None + artifacts: list[str] + created_at: datetime = Field(default_factory=utc_now) + + +class ProofCheck(BaseModel): + name: str + requirement: str + status: Literal["passed", "failed"] + evidence: list[str] + detail: str + + +class ProofSuiteReport(BaseModel): + proof_id: str + cartridge_id: str + status: Literal["passed", "failed"] + created_at: datetime = Field(default_factory=utc_now) + checks: list[ProofCheck] + artifacts: dict[str, str] + dna_before: str | None = None + dna_after: str | None = None + + +class UploadIngestRequest(BaseModel): + cartridge_name: str | None = None + dense_embeddings: bool | None = None + + +class PathIngestRequest(BaseModel): + path: Path + cartridge_name: str | None = None + dense_embeddings: bool | None = None diff --git a/backend/src/ely_eye/storage.py b/backend/src/ely_eye/storage.py new file mode 100644 index 0000000..158a191 --- /dev/null +++ b/backend/src/ely_eye/storage.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +import hashlib +import mimetypes +import shutil +from pathlib import Path + +from .config import Settings, get_settings + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def sha256_bytes(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + +def guess_mime(path: Path) -> str: + mime, _ = mimetypes.guess_type(path.name) + return mime or "application/octet-stream" + + +def safe_source_id(path: Path, content_hash: str) -> str: + stem = "".join(char if char.isalnum() else "_" for char in path.stem).strip("_") + prefix = stem[:48] or "source" + return f"{prefix}_{content_hash[:16]}" + + +def token_equivalent(text: str) -> int: + if not text: + return 0 + ascii_chars = sum(1 for char in text if ord(char) < 128) + non_ascii_chars = len(text) - ascii_chars + return max(1, ascii_chars // 4 + non_ascii_chars // 2) + + +class ObjectStore: + def __init__(self, settings: Settings | None = None) -> None: + self.settings = settings or get_settings() + self.settings.ensure_dirs() + + def put_file(self, path: Path, namespace: str) -> str: + content_hash = sha256_file(path) + suffix = path.suffix.lower() + target_dir = self.settings.object_dir / namespace / content_hash[:2] + target_dir.mkdir(parents=True, exist_ok=True) + target = target_dir / f"{content_hash}{suffix}" + if not target.exists(): + shutil.copy2(path, target) + return self.to_object_uri(target) + + def put_bytes(self, data: bytes, suffix: str, namespace: str) -> str: + content_hash = sha256_bytes(data) + target_dir = self.settings.object_dir / namespace / content_hash[:2] + target_dir.mkdir(parents=True, exist_ok=True) + target = target_dir / f"{content_hash}{suffix}" + if not target.exists(): + target.write_bytes(data) + return self.to_object_uri(target) + + def resolve(self, object_uri: str) -> Path: + prefix = "object://" + if not object_uri.startswith(prefix): + raise ValueError(f"Invalid object URI: {object_uri}") + relative = object_uri[len(prefix) :] + path = (self.settings.object_dir / relative).resolve() + root = self.settings.object_dir.resolve() + if root not in path.parents and path != root: + raise ValueError(f"Object URI escapes object store: {object_uri}") + return path + + def to_object_uri(self, path: Path) -> str: + return "object://" + path.resolve().relative_to(self.settings.object_dir.resolve()).as_posix() diff --git a/backend/src/ely_eye/training.py b/backend/src/ely_eye/training.py new file mode 100644 index 0000000..4c81aef --- /dev/null +++ b/backend/src/ely_eye/training.py @@ -0,0 +1,1640 @@ +from __future__ import annotations + +import hashlib +import json +import math +import platform +import re +import shutil +import subprocess +import time +from contextlib import contextmanager +from datetime import datetime, timezone +from importlib.metadata import PackageNotFoundError, version +from pathlib import Path +from typing import Any, TextIO + +from pydantic import BaseModel, Field + +from .config import Settings, get_settings +from .db import Database +from .schemas import AdapterManifest, EvidenceAtom, TrainingAdapterKind + + +SYSTEM_PROMPT = ( + "You are Ely-Eye. Answer from supplied evidence, return compact JSON, " + "and cite atom ids exactly." +) +TRAINING_PROOF_VERSION = 2 +TRAINING_METHOD = "BF16 LoRA" +TRAINING_PRECISION = "bf16" +TRAINING_FRAMEWORK = "unsloth_patched_transformers_peft" +GRADIENT_CHECKPOINTING_MODE = "unsloth_patched_torch_non_reentrant" +AUTOCAST_DTYPE = "bf16" + + +class TrainingSample(BaseModel): + id: str + task: str + inputs: list[dict[str, Any]] + memory: dict[str, Any] = Field(default_factory=dict) + output: dict[str, Any] + + +class TrainingService: + def __init__(self, settings: Settings | None = None, database: Database | None = None) -> None: + self.settings = settings or get_settings() + self.db = database or Database(self.settings) + + def validate_dataset(self, path: Path) -> list[TrainingSample]: + samples: list[TrainingSample] = [] + with path.open("r", encoding="utf-8") as handle: + for line_number, line in enumerate(handle, start=1): + if not line.strip(): + continue + try: + samples.append(TrainingSample.model_validate_json(line)) + except Exception as exc: + raise ValueError(f"Invalid training sample at line {line_number}: {exc}") from exc + if not samples: + raise ValueError(f"Training dataset is empty: {path}") + return samples + + def build_prd_dataset( + self, + prd_path: Path, + output_path: Path | None = None, + cartridge_id: str | None = None, + max_sections: int = 48, + ) -> Path: + prd_path = prd_path.resolve() + text = prd_path.read_text(encoding="utf-8") + atoms = self._atoms_for_path(prd_path) + atom_id = atoms[0].atom_id if atoms else f"{prd_path.stem}:{sha256_bytes(text.encode('utf-8'))[:12]}" + cartridge_id = cartridge_id or self._latest_cartridge_id() + sections = split_markdown_sections(text) + if not sections: + raise ValueError(f"No markdown sections found in {prd_path}") + output_path = output_path or (self.settings.training_dir / "hme_prd_sft.jsonl") + output_path.parent.mkdir(parents=True, exist_ok=True) + + samples: list[TrainingSample] = [] + for index, section in enumerate(sections[:max_sections], start=1): + title = section["title"] + body = compact_text(section["body"], limit=5200) + if len(body) < 160: + continue + citation = f"{atom_id}#section={slugify(title)}" + answer = compact_text(body, limit=900) + samples.append( + TrainingSample( + id=f"hme_prd_{index:04d}", + task="hme_core_lora_evidence_json", + inputs=[ + { + "type": "text", + "text": ( + f"Section title: {title}\n" + f"Evidence atom: {atom_id}\n" + f"Evidence:\n{body}\n\n" + "Question: State the Ely-Eye requirement in this section. " + "Return JSON with answer, citations, contradiction_notes, confidence." + ), + } + ], + memory={ + "cartridge": cartridge_id, + "required_atoms": [atom_id], + "source": str(prd_path), + }, + output={ + "answer": answer, + "citations": [citation], + "contradiction_notes": [], + "confidence": 1.0, + "reasoning_policy": "evidence-first", + }, + ) + ) + + if not samples: + raise ValueError(f"No usable PRD training sections found in {prd_path}") + with output_path.open("w", encoding="utf-8", newline="\n") as handle: + for sample in samples: + handle.write(sample.model_dump_json() + "\n") + return output_path + + def build_visual_dataset( + self, + prd_path: Path, + output_path: Path | None = None, + image_paths: list[Path] | None = None, + cartridge_id: str | None = None, + ) -> Path: + prd_path = prd_path.resolve() + text = prd_path.read_text(encoding="utf-8") + atom_id = self._atom_id_for_path(prd_path, text) + cartridge_id = cartridge_id or self._latest_cartridge_id() + media_dir = self.settings.training_dir / "media" + media_dir.mkdir(parents=True, exist_ok=True) + + prepared_images: list[Path] = [] + for image_path in image_paths or []: + if image_path.exists(): + prepared_images.append(prepare_training_image(image_path, media_dir)) + prd_card = render_prd_training_card(text, atom_id, media_dir / "prd_training_card.png") + prepared_images.append(prd_card) + if not prepared_images: + raise ValueError("No visual training images are available") + + output_path = output_path or (self.settings.training_dir / "hme_visual_sft.jsonl") + samples: list[TrainingSample] = [] + visual_tasks = [ + ( + "hme_vision_lora_visual_grounding", + "12.1 训练目标", + ( + "Ely-Eye trains HME-Vision-LoRA on Qwen3.5-9B vision projector " + "and selected vision layers so UI, PDF, OCR, and chart evidence can be " + "grounded into cited multimodal memory." + ), + ), + ( + "hme_ttt_vl_visual_memory", + "11.2 X — TTT-VL:视觉语言测试时学习", + ( + "TTT-VL keeps Qwen3.5-9B frozen while mutable MLP and visual LoRA " + "adapters learn low-step visual memory updates that can be consolidated " + "into cartridge-local adapter deltas." + ), + ), + ( + "hme_visual_hashhop_grounding", + "11.3 X — Visual HashHop", + ( + "Visual HashHop converts random chain ids into non-text geometric image " + "blocks, then evaluates long-range visual addressing without OCR shortcuts." + ), + ), + ( + "hme_dvkv_visual_cache", + "11.4 S/H — DVKV:Diversity-Aware Visual KV Compression", + ( + "DVKV keeps visual KV tokens by combining attention importance, visual " + "diversity, and evidence value so OCR, layout, chart, and coordinate details " + "survive compression." + ), + ), + ( + "hme_visual_contradiction_lens", + "11.15 X — Visual Contradiction Lens", + ( + "The Visual Contradiction Lens compares visual versions and cites layout, " + "design-token, copy, temporal, and visual-code drift as evidence graph facts." + ), + ), + ] + for index, image_path in enumerate(prepared_images, start=1): + for task_index, (task, section_title, answer) in enumerate(visual_tasks, start=1): + requirement = section_by_title(text, section_title) + samples.append( + TrainingSample( + id=f"hme_visual_{index:02d}_{task_index:02d}", + task=task, + inputs=[ + { + "type": "ui_screenshot", + "path": str(image_path.resolve()), + "max_pixels": 262144, + }, + { + "type": "text", + "text": ( + f"Evidence atom: {atom_id}\n" + f"Visual source: {image_path.name}\n" + f"PRD visual requirement:\n" + f"{compact_text(requirement, 2200)}\n\n" + "Question: Map this visual evidence to the Ely-Eye PRD module. " + "Return JSON with answer, citations, contradiction_notes, confidence." + ), + }, + ], + memory={ + "cartridge": cartridge_id, + "required_atoms": [atom_id], + "source": str(prd_path), + }, + output={ + "answer": answer, + "citations": [f"{atom_id}#section={slugify(section_title)}"], + "contradiction_notes": [], + "confidence": 1.0, + "reasoning_policy": "evidence-first", + }, + ) + ) + write_jsonl(output_path, samples) + return output_path + + def build_router_dataset( + self, + prd_path: Path, + output_path: Path | None = None, + cartridge_id: str | None = None, + ) -> Path: + prd_path = prd_path.resolve() + text = prd_path.read_text(encoding="utf-8") + atom_id = self._atom_id_for_path(prd_path, text) + cartridge_id = cartridge_id or self._latest_cartridge_id() + output_path = output_path or (self.settings.training_dir / "hme_router_sft.jsonl") + scenarios = [ + ( + "Summarize a stable live-demo answer from local PRD evidence.", + "live_demo", + 262144, + ["mixed"], + "Prefix cache + sparse evidence packing", + ), + ( + "Plan a 1.01M extreme context run with YaRN and parked context.", + "extreme_context", + 1010000, + ["mixed"], + "YaRN + V-NSA sparse blocks + DVKV visual KV compression + CPU/NVMe parked context", + ), + ( + "Route a 100M library question through Context Cartridge memory.", + "library_100m", + 100000000, + ["mixed"], + "Context Cartridge retrieval + ColQwen-compatible dense vectors + BM25 + evidence graph pack", + ), + ( + "Audit UI screenshot and PRD differences for the research demo.", + "research_theater", + 262144, + ["image", "ui_screenshot"], + "Visual patch cache + DVKV evidence-biased region retention", + ), + ( + "Find visual token compression methods across a 100M library cartridge.", + "library_100m", + 100000000, + ["pdf_page", "image", "video_frame"], + "ColQwen-compatible retrieval + graph grouping + cited evidence pack", + ), + ( + "Compare UI versions and cite coordinates plus code evidence.", + "research_theater", + 262144, + ["ui_screenshot", "code", "text"], + "Visual Contradiction Lens + evidence graph writeback", + ), + ( + "Run Visual HashHop at 262K with a two-hop random image chain.", + "research_theater", + 262144, + ["image"], + "Visual HashHop arena + verifier contract + no OCR-dependent evidence", + ), + ( + "Answer from a TTT-VL cartridge after original images are absent.", + "library_100m", + 100000000, + ["mixed"], + "Load cartridge adapters + learned memory recall + cited adapter proof", + ), + ] + samples = [ + TrainingSample( + id=f"hme_router_{index:04d}", + task="hme_router_context_plan", + inputs=[ + { + "type": "text", + "text": ( + f"Evidence atom: {atom_id}\n" + f"PRD routing source:\n{compact_text(section_by_title(text, '1. 产品定义'), 1800)}\n\n" + f"User request: {request}\n" + "Return the Ely-Eye routing plan as compact JSON." + ), + } + ], + memory={ + "cartridge": cartridge_id, + "required_atoms": [atom_id], + "source": str(prd_path), + }, + output={ + "profile": profile, + "token_budget": token_budget, + "required_modalities": modalities, + "retrieval_query": request, + "compression_strategy": compression, + "citations": [f"{atom_id}#section=1-产品定义"], + "confidence": 1.0, + }, + ) + for index, (request, profile, token_budget, modalities, compression) in enumerate(scenarios, start=1) + ] + write_jsonl(output_path, samples) + return output_path + + def build_retrieval_dataset( + self, + prd_path: Path, + output_path: Path | None = None, + cartridge_id: str | None = None, + max_sections: int = 32, + ) -> Path: + prd_path = prd_path.resolve() + text = prd_path.read_text(encoding="utf-8") + atom_id = self._atom_id_for_path(prd_path, text) + cartridge_id = cartridge_id or self._latest_cartridge_id() + sections = [section for section in split_markdown_sections(text) if len(section["body"]) >= 160] + output_path = output_path or (self.settings.training_dir / "hme_retrieval_pairs.jsonl") + samples: list[TrainingSample] = [] + for index, section in enumerate(sections[:max_sections], start=1): + title = section["title"] + positive = compact_text(section["body"], 1600) + samples.append( + TrainingSample( + id=f"hme_retrieval_{index:04d}", + task="hme_retrieval_contrastive_pair", + inputs=[ + { + "type": "text", + "text": f"Find the PRD section that answers: {title}", + } + ], + memory={ + "cartridge": cartridge_id, + "required_atoms": [atom_id], + "source": str(prd_path), + }, + output={ + "positive": positive, + "positive_title": title, + "citations": [f"{atom_id}#section={slugify(title)}"], + "confidence": 1.0, + }, + ) + ) + if len(samples) < 2: + raise ValueError("Retrieval contrastive training needs at least two PRD sections") + write_jsonl(output_path, samples) + return output_path + + def write_lora_config( + self, + output_dir: Path, + kind: TrainingAdapterKind = TrainingAdapterKind.hme_core_lora, + rank: int = 16, + alpha: int = 32, + model_id: str | None = None, + ) -> Path: + output_dir.mkdir(parents=True, exist_ok=True) + config = { + "base_model": model_id or self.settings.model_id, + "adapter_kind": kind.value, + "method": "BF16 LoRA", + "framework": TRAINING_FRAMEWORK, + "rank": rank, + "alpha": alpha, + "gradient_checkpointing": True, + "gradient_checkpointing_mode": GRADIENT_CHECKPOINTING_MODE, + "precision": "bf16", + "optimizer": "paged_adamw_8bit", + "ensure_weight_tying": kind == TrainingAdapterKind.hme_visual_mtp, + } + path = output_dir / "lora_config.json" + path.write_text(json.dumps(config, ensure_ascii=False, indent=2), encoding="utf-8") + return path + + def train_lora( + self, + dataset_path: Path, + output_dir: Path, + max_steps: int, + kind: TrainingAdapterKind = TrainingAdapterKind.hme_core_lora, + model_id: str | None = None, + rank: int = 16, + alpha: int = 32, + max_length: int | None = None, + gradient_accumulation_steps: int | None = None, + learning_rate: float | None = None, + cartridge_id: str | None = None, + ) -> AdapterManifest: + samples = self.validate_dataset(dataset_path) + output_dir.mkdir(parents=True, exist_ok=True) + model_id = model_id or self._model_for_kind(kind) + max_length = max_length or self.settings.train_max_length + gradient_accumulation_steps = ( + gradient_accumulation_steps or self.settings.train_gradient_accumulation_steps + ) + learning_rate = learning_rate or self.settings.train_learning_rate + started_at = datetime.now(timezone.utc) + wall_start = time.perf_counter() + trace_path = output_dir / "training_trace.jsonl" + + import torch + + activate_unsloth_training() + from peft import LoraConfig, get_peft_model + from transformers import AutoProcessor + + try: + from transformers import AutoModelForImageTextToText + + model_cls = AutoModelForImageTextToText + except ImportError: + from transformers import AutoModelForCausalLM + + model_cls = AutoModelForCausalLM + + require_cuda_bf16(torch) + processor = AutoProcessor.from_pretrained(model_id, trust_remote_code=True) + dtype = torch.bfloat16 + device_map: dict[str, int] = {"": 0} + model = model_cls.from_pretrained( + model_id, + dtype=dtype, + device_map=device_map, + trust_remote_code=True, + ) + model.config.use_cache = False + input_require_grads = False + if hasattr(model, "enable_input_require_grads"): + model.enable_input_require_grads() + input_require_grads = True + + target_modules = infer_lora_targets(model, kind) + gradient_checkpointing = enable_gradient_checkpointing(model) + peft_config_options: dict[str, Any] = { + "r": rank, + "lora_alpha": alpha, + "target_modules": target_modules, + "lora_dropout": 0.0, + "bias": "none", + "task_type": "CAUSAL_LM", + } + if kind == TrainingAdapterKind.hme_visual_mtp: + peft_config_options["ensure_weight_tying"] = True + with bitsandbytes_gaudi_probe_guard(): + model = get_peft_model(model, LoraConfig(**peft_config_options)) + unsloth_model_type = model_type_for_proof(model) + trainable_params, total_params = model.get_nb_trainable_parameters() + optimizer, optimizer_name = make_optimizer(model, learning_rate) + model.train() + + losses: list[float] = [] + optimizer_updates: list[dict[str, Any]] = [] + trainable_tensors = trainable_named_parameters(model) + initial_trainable_sha = trainable_state_sha256(trainable_tensors) + initial_trainable_l2 = trainable_l2_norm(trainable_tensors) + optimizer.zero_grad(set_to_none=True) + environment = training_environment(torch) + contract = training_contract( + gradient_checkpointing=gradient_checkpointing, + input_require_grads=input_require_grads, + optimizer_name=optimizer_name, + unsloth_model_type=unsloth_model_type, + ) + with trace_path.open("w", encoding="utf-8", newline="\n") as trace: + write_trace( + trace, + { + "event": "run_start", + **contract, + "started_at": started_at.isoformat(), + "adapter_kind": kind.value, + "base_model": model_id, + "dataset_path": str(dataset_path.resolve()), + "dataset_sha256": sha256_file(dataset_path), + "sample_count": len(samples), + "max_steps": max_steps, + "max_length": max_length, + "rank": rank, + "alpha": alpha, + "gradient_accumulation_steps": gradient_accumulation_steps, + "learning_rate": learning_rate, + "target_modules": target_modules, + "trainable_params": int(trainable_params), + "total_params": int(total_params), + "optimizer": optimizer_name, + "environment": environment, + }, + ) + for step in range(max_steps): + sample = samples[step % len(samples)] + batch = encode_training_sample(sample, processor, model.device, max_length) + with torch.autocast( + device_type="cuda", + dtype=torch.bfloat16, + enabled=torch.cuda.is_available(), + ): + outputs = model(**batch) + loss = outputs.loss / gradient_accumulation_steps + loss.backward() + optimizer_step = (step + 1) % gradient_accumulation_steps == 0 or (step + 1) == max_steps + grad_l2 = gradient_l2_norm(trainable_tensors) + update_l2 = None + if optimizer_step: + before_update = trainable_state_cpu(trainable_tensors) + optimizer.step() + update_l2 = trainable_update_l2(before_update, trainable_tensors) + optimizer_updates.append( + { + "step": step + 1, + "gradient_l2": grad_l2, + "update_l2": update_l2, + } + ) + optimizer.zero_grad(set_to_none=True) + actual_loss = float(loss.detach().cpu()) * gradient_accumulation_steps + losses.append(actual_loss) + write_trace( + trace, + { + "event": "step", + "step": step + 1, + "sample_id": sample.id, + "task": sample.task, + "loss": actual_loss, + "optimizer_step": optimizer_step, + "gradient_l2": grad_l2, + "update_l2": update_l2, + }, + ) + + final_trainable_sha = trainable_state_sha256(trainable_tensors) + final_trainable_l2 = trainable_l2_norm(trainable_tensors) + max_update_l2 = max((item["update_l2"] for item in optimizer_updates), default=None) + loss_delta = losses[0] - losses[-1] if len(losses) >= 2 else None + model.save_pretrained(output_dir) + processor.save_pretrained(output_dir) + self.write_lora_config(output_dir, kind, rank, alpha, model_id) + weight_stats = safetensor_weight_stats(output_dir / "adapter_model.safetensors") + finished_at = datetime.now(timezone.utc) + wall_seconds = time.perf_counter() - wall_start + summary_path = output_dir / "training_summary.json" + proof_path = output_dir / "training_proof.json" + training_proof = { + **contract, + "adapter_kind": kind.value, + "base_model": model_id, + "dataset_path": str(dataset_path.resolve()), + "dataset_sha256": sha256_file(dataset_path), + "sample_count": len(samples), + "max_steps": max_steps, + "max_length": max_length, + "rank": rank, + "alpha": alpha, + "target_modules": target_modules, + "trainable_params": int(trainable_params), + "total_params": int(total_params), + "optimizer": optimizer_name, + "learning_rate": learning_rate, + "gradient_accumulation_steps": gradient_accumulation_steps, + "bf16": bool(torch.cuda.is_available()), + "environment": environment, + "initial_trainable_sha256": initial_trainable_sha, + "final_trainable_sha256": final_trainable_sha, + "initial_trainable_l2": initial_trainable_l2, + "final_trainable_l2": final_trainable_l2, + "optimizer_update_count": len(optimizer_updates), + "max_optimizer_update_l2": max_update_l2, + "loss_delta": loss_delta, + "loss_history": losses, + "optimizer_updates": optimizer_updates, + "adapter_weight_sha256": weight_stats["sha256"], + "adapter_weight_stats": weight_stats, + } + write_training_summary(proof_path, training_proof) + write_training_summary( + summary_path, + { + **contract, + "adapter_kind": kind.value, + "base_model": model_id, + "dataset_path": str(dataset_path.resolve()), + "dataset_sha256": sha256_file(dataset_path), + "sample_count": len(samples), + "max_steps": max_steps, + "max_length": max_length, + "rank": rank, + "alpha": alpha, + "target_modules": target_modules, + "trainable_params": int(trainable_params), + "total_params": int(total_params), + "optimizer": optimizer_name, + "learning_rate": learning_rate, + "gradient_accumulation_steps": gradient_accumulation_steps, + "bf16": bool(torch.cuda.is_available()), + "loss_history": losses, + "final_loss": losses[-1] if losses else None, + "initial_trainable_sha256": initial_trainable_sha, + "final_trainable_sha256": final_trainable_sha, + "initial_trainable_l2": initial_trainable_l2, + "final_trainable_l2": final_trainable_l2, + "optimizer_update_count": len(optimizer_updates), + "max_optimizer_update_l2": max_update_l2, + "loss_delta": loss_delta, + "started_at": started_at.isoformat(), + "finished_at": finished_at.isoformat(), + "wall_seconds": wall_seconds, + "environment": environment, + "peak_cuda_memory_mb": cuda_peak_memory_mb(torch), + "adapter_weight_sha256": weight_stats["sha256"], + "adapter_weight_stats": weight_stats, + }, + ) + with trace_path.open("a", encoding="utf-8", newline="\n") as trace: + write_trace( + trace, + { + "event": "run_finish", + **contract, + "finished_at": finished_at.isoformat(), + "wall_seconds": wall_seconds, + "final_loss": losses[-1] if losses else None, + "initial_trainable_sha256": initial_trainable_sha, + "final_trainable_sha256": final_trainable_sha, + "optimizer_update_count": len(optimizer_updates), + "max_optimizer_update_l2": max_update_l2, + "loss_delta": loss_delta, + "peak_cuda_memory_mb": cuda_peak_memory_mb(torch), + "adapter_weight_sha256": weight_stats["sha256"], + }, + ) + adapter_sha = sha256_tree(output_dir) + manifest = AdapterManifest( + adapter_id=adapter_id(kind, dataset_path, model_id), + kind=kind, + created_at=datetime.now(timezone.utc), + proof_version=TRAINING_PROOF_VERSION, + training_method=TRAINING_METHOD, + precision=TRAINING_PRECISION, + framework=TRAINING_FRAMEWORK, + base_model=model_id, + dataset_path=str(dataset_path.resolve()), + dataset_sha256=sha256_file(dataset_path), + sample_count=len(samples), + max_steps=max_steps, + max_length=max_length, + rank=rank, + alpha=alpha, + target_modules=target_modules, + trainable_params=int(trainable_params), + total_params=int(total_params), + optimizer=optimizer_name, + optimizer_family=optimizer_family(optimizer_name), + learning_rate=learning_rate, + gradient_accumulation_steps=gradient_accumulation_steps, + gradient_checkpointing=gradient_checkpointing, + gradient_checkpointing_mode=GRADIENT_CHECKPOINTING_MODE if gradient_checkpointing else None, + autocast_dtype=AUTOCAST_DTYPE, + bf16=torch.cuda.is_available(), + unsloth_version=package_version("unsloth"), + triton_version=package_version("triton-windows") or package_version("triton"), + xformers_version=package_version("xformers"), + unsloth_model_type=unsloth_model_type, + final_loss=losses[-1] if losses else None, + loss_history=losses, + initial_trainable_sha256=initial_trainable_sha, + final_trainable_sha256=final_trainable_sha, + initial_trainable_l2=initial_trainable_l2, + final_trainable_l2=final_trainable_l2, + optimizer_update_count=len(optimizer_updates), + max_optimizer_update_l2=max_update_l2, + loss_delta=loss_delta, + training_proof_path=str(proof_path.resolve()), + training_proof_sha256=sha256_file(proof_path), + training_trace_path=str(trace_path.resolve()), + training_trace_sha256=sha256_file(trace_path), + training_summary_path=str(summary_path.resolve()), + training_summary_sha256=sha256_file(summary_path), + training_started_at=started_at, + training_finished_at=finished_at, + training_wall_seconds=wall_seconds, + training_device=str(environment.get("device_name") or environment.get("device") or ""), + torch_version=str(environment.get("torch_version") or ""), + cuda_version=( + str(environment["torch_cuda_version"]) + if environment.get("torch_cuda_version") is not None + else None + ), + adapter_weight_sha256=str(weight_stats["sha256"] or ""), + adapter_tensor_count=int(weight_stats["tensor_count"]), + adapter_lora_tensor_count=int(weight_stats["lora_tensor_count"]), + adapter_total_elements=int(weight_stats["total_elements"]), + adapter_nonzero_elements=int(weight_stats["nonzero_elements"]), + adapter_max_abs=weight_stats["max_abs"], + adapter_weights_finite=bool(weight_stats["all_finite"]), + artifacts=relative_artifacts(output_dir), + sha256=adapter_sha, + cartridge_id=cartridge_id, + ) + (output_dir / "adapter_manifest.json").write_text( + manifest.model_dump_json(indent=2), + encoding="utf-8", + ) + return manifest + + def train_retrieval_lora( + self, + dataset_path: Path, + output_dir: Path, + max_steps: int, + rank: int = 8, + alpha: int = 16, + max_length: int | None = None, + learning_rate: float | None = None, + cartridge_id: str | None = None, + ) -> AdapterManifest: + samples = self.validate_dataset(dataset_path) + output_dir.mkdir(parents=True, exist_ok=True) + max_length = max_length or self.settings.train_max_length + learning_rate = learning_rate or self.settings.train_learning_rate + started_at = datetime.now(timezone.utc) + wall_start = time.perf_counter() + trace_path = output_dir / "training_trace.jsonl" + + import torch + import torch.nn.functional as F + + activate_unsloth_training() + from peft import LoraConfig, TaskType, get_peft_model + from transformers import AutoModel, AutoProcessor + + model_id = self.settings.embedding_model_id + require_cuda_bf16(torch) + processor = AutoProcessor.from_pretrained(model_id, trust_remote_code=True) + model = AutoModel.from_pretrained( + model_id, + dtype=torch.bfloat16, + device_map={"": 0}, + trust_remote_code=True, + ) + if hasattr(model.config, "use_cache"): + model.config.use_cache = False + target_modules = infer_lora_targets(model, TrainingAdapterKind.hme_retrieval) + gradient_checkpointing = enable_gradient_checkpointing(model) + with bitsandbytes_gaudi_probe_guard(): + model = get_peft_model( + model, + LoraConfig( + r=rank, + lora_alpha=alpha, + target_modules=target_modules, + lora_dropout=0.0, + bias="none", + task_type=TaskType.FEATURE_EXTRACTION, + ), + ) + unsloth_model_type = model_type_for_proof(model) + trainable_params, total_params = model.get_nb_trainable_parameters() + optimizer, optimizer_name = make_optimizer(model, learning_rate) + model.train() + + losses: list[float] = [] + optimizer_updates: list[dict[str, Any]] = [] + trainable_tensors = trainable_named_parameters(model) + initial_trainable_sha = trainable_state_sha256(trainable_tensors) + initial_trainable_l2 = trainable_l2_norm(trainable_tensors) + batch_size = min(4, len(samples)) + environment = training_environment(torch) + contract = training_contract( + gradient_checkpointing=gradient_checkpointing, + input_require_grads=False, + optimizer_name=optimizer_name, + unsloth_model_type=unsloth_model_type, + ) + with trace_path.open("w", encoding="utf-8", newline="\n") as trace: + write_trace( + trace, + { + "event": "run_start", + **contract, + "started_at": started_at.isoformat(), + "adapter_kind": TrainingAdapterKind.hme_retrieval.value, + "base_model": model_id, + "dataset_path": str(dataset_path.resolve()), + "dataset_sha256": sha256_file(dataset_path), + "sample_count": len(samples), + "max_steps": max_steps, + "max_length": max_length, + "rank": rank, + "alpha": alpha, + "target_modules": target_modules, + "trainable_params": int(trainable_params), + "total_params": int(total_params), + "optimizer": optimizer_name, + "learning_rate": learning_rate, + "environment": environment, + }, + ) + for step in range(max_steps): + window = [samples[(step + offset) % len(samples)] for offset in range(batch_size)] + queries = [str(sample.inputs[0]["text"]) for sample in window] + documents = [str(sample.output["positive"]) for sample in window] + query_batch = encode_retrieval_batch(queries, processor, model.device, max_length) + document_batch = encode_retrieval_batch(documents, processor, model.device, max_length) + query_embeddings = pool_embeddings(model(**query_batch), query_batch["attention_mask"]) + document_embeddings = pool_embeddings(model(**document_batch), document_batch["attention_mask"]) + logits = query_embeddings @ document_embeddings.T / 0.05 + labels = torch.arange(logits.shape[0], device=logits.device) + loss = F.cross_entropy(logits.float(), labels) + loss.backward() + grad_l2 = gradient_l2_norm(trainable_tensors) + before_update = trainable_state_cpu(trainable_tensors) + optimizer.step() + update_l2 = trainable_update_l2(before_update, trainable_tensors) + optimizer_updates.append( + { + "step": step + 1, + "gradient_l2": grad_l2, + "update_l2": update_l2, + } + ) + optimizer.zero_grad(set_to_none=True) + actual_loss = float(loss.detach().cpu()) + losses.append(actual_loss) + write_trace( + trace, + { + "event": "step", + "step": step + 1, + "sample_ids": [sample.id for sample in window], + "task": "hme_retrieval_contrastive_pair", + "loss": actual_loss, + "optimizer_step": True, + "gradient_l2": grad_l2, + "update_l2": update_l2, + }, + ) + + final_trainable_sha = trainable_state_sha256(trainable_tensors) + final_trainable_l2 = trainable_l2_norm(trainable_tensors) + max_update_l2 = max((item["update_l2"] for item in optimizer_updates), default=None) + loss_delta = losses[0] - losses[-1] if len(losses) >= 2 else None + model.save_pretrained(output_dir) + processor.save_pretrained(output_dir) + self.write_lora_config(output_dir, TrainingAdapterKind.hme_retrieval, rank, alpha, model_id) + weight_stats = safetensor_weight_stats(output_dir / "adapter_model.safetensors") + finished_at = datetime.now(timezone.utc) + wall_seconds = time.perf_counter() - wall_start + summary_path = output_dir / "training_summary.json" + proof_path = output_dir / "training_proof.json" + training_proof = { + **contract, + "adapter_kind": TrainingAdapterKind.hme_retrieval.value, + "base_model": model_id, + "dataset_path": str(dataset_path.resolve()), + "dataset_sha256": sha256_file(dataset_path), + "sample_count": len(samples), + "max_steps": max_steps, + "max_length": max_length, + "rank": rank, + "alpha": alpha, + "target_modules": target_modules, + "trainable_params": int(trainable_params), + "total_params": int(total_params), + "optimizer": optimizer_name, + "learning_rate": learning_rate, + "gradient_accumulation_steps": 1, + "bf16": bool(torch.cuda.is_available()), + "environment": environment, + "initial_trainable_sha256": initial_trainable_sha, + "final_trainable_sha256": final_trainable_sha, + "initial_trainable_l2": initial_trainable_l2, + "final_trainable_l2": final_trainable_l2, + "optimizer_update_count": len(optimizer_updates), + "max_optimizer_update_l2": max_update_l2, + "loss_delta": loss_delta, + "loss_history": losses, + "optimizer_updates": optimizer_updates, + "adapter_weight_sha256": weight_stats["sha256"], + "adapter_weight_stats": weight_stats, + } + write_training_summary(proof_path, training_proof) + write_training_summary( + summary_path, + { + **contract, + "adapter_kind": TrainingAdapterKind.hme_retrieval.value, + "base_model": model_id, + "dataset_path": str(dataset_path.resolve()), + "dataset_sha256": sha256_file(dataset_path), + "sample_count": len(samples), + "max_steps": max_steps, + "max_length": max_length, + "rank": rank, + "alpha": alpha, + "target_modules": target_modules, + "trainable_params": int(trainable_params), + "total_params": int(total_params), + "optimizer": optimizer_name, + "learning_rate": learning_rate, + "gradient_accumulation_steps": 1, + "bf16": bool(torch.cuda.is_available()), + "loss_history": losses, + "final_loss": losses[-1] if losses else None, + "initial_trainable_sha256": initial_trainable_sha, + "final_trainable_sha256": final_trainable_sha, + "initial_trainable_l2": initial_trainable_l2, + "final_trainable_l2": final_trainable_l2, + "optimizer_update_count": len(optimizer_updates), + "max_optimizer_update_l2": max_update_l2, + "loss_delta": loss_delta, + "started_at": started_at.isoformat(), + "finished_at": finished_at.isoformat(), + "wall_seconds": wall_seconds, + "environment": environment, + "peak_cuda_memory_mb": cuda_peak_memory_mb(torch), + "adapter_weight_sha256": weight_stats["sha256"], + "adapter_weight_stats": weight_stats, + }, + ) + with trace_path.open("a", encoding="utf-8", newline="\n") as trace: + write_trace( + trace, + { + "event": "run_finish", + **contract, + "finished_at": finished_at.isoformat(), + "wall_seconds": wall_seconds, + "final_loss": losses[-1] if losses else None, + "initial_trainable_sha256": initial_trainable_sha, + "final_trainable_sha256": final_trainable_sha, + "optimizer_update_count": len(optimizer_updates), + "max_optimizer_update_l2": max_update_l2, + "loss_delta": loss_delta, + "peak_cuda_memory_mb": cuda_peak_memory_mb(torch), + "adapter_weight_sha256": weight_stats["sha256"], + }, + ) + manifest = AdapterManifest( + adapter_id=adapter_id(TrainingAdapterKind.hme_retrieval, dataset_path, model_id), + kind=TrainingAdapterKind.hme_retrieval, + created_at=datetime.now(timezone.utc), + proof_version=TRAINING_PROOF_VERSION, + training_method=TRAINING_METHOD, + precision=TRAINING_PRECISION, + framework=TRAINING_FRAMEWORK, + base_model=model_id, + dataset_path=str(dataset_path.resolve()), + dataset_sha256=sha256_file(dataset_path), + sample_count=len(samples), + max_steps=max_steps, + max_length=max_length, + rank=rank, + alpha=alpha, + target_modules=target_modules, + trainable_params=int(trainable_params), + total_params=int(total_params), + optimizer=optimizer_name, + optimizer_family=optimizer_family(optimizer_name), + learning_rate=learning_rate, + gradient_accumulation_steps=1, + gradient_checkpointing=gradient_checkpointing, + gradient_checkpointing_mode=GRADIENT_CHECKPOINTING_MODE if gradient_checkpointing else None, + autocast_dtype=AUTOCAST_DTYPE, + bf16=torch.cuda.is_available(), + unsloth_version=package_version("unsloth"), + triton_version=package_version("triton-windows") or package_version("triton"), + xformers_version=package_version("xformers"), + unsloth_model_type=unsloth_model_type, + final_loss=losses[-1] if losses else None, + loss_history=losses, + initial_trainable_sha256=initial_trainable_sha, + final_trainable_sha256=final_trainable_sha, + initial_trainable_l2=initial_trainable_l2, + final_trainable_l2=final_trainable_l2, + optimizer_update_count=len(optimizer_updates), + max_optimizer_update_l2=max_update_l2, + loss_delta=loss_delta, + training_proof_path=str(proof_path.resolve()), + training_proof_sha256=sha256_file(proof_path), + training_trace_path=str(trace_path.resolve()), + training_trace_sha256=sha256_file(trace_path), + training_summary_path=str(summary_path.resolve()), + training_summary_sha256=sha256_file(summary_path), + training_started_at=started_at, + training_finished_at=finished_at, + training_wall_seconds=wall_seconds, + training_device=str(environment.get("device_name") or environment.get("device") or ""), + torch_version=str(environment.get("torch_version") or ""), + cuda_version=( + str(environment["torch_cuda_version"]) + if environment.get("torch_cuda_version") is not None + else None + ), + adapter_weight_sha256=str(weight_stats["sha256"] or ""), + adapter_tensor_count=int(weight_stats["tensor_count"]), + adapter_lora_tensor_count=int(weight_stats["lora_tensor_count"]), + adapter_total_elements=int(weight_stats["total_elements"]), + adapter_nonzero_elements=int(weight_stats["nonzero_elements"]), + adapter_max_abs=weight_stats["max_abs"], + adapter_weights_finite=bool(weight_stats["all_finite"]), + artifacts=relative_artifacts(output_dir), + sha256=sha256_tree(output_dir), + cartridge_id=cartridge_id, + ) + (output_dir / "adapter_manifest.json").write_text( + manifest.model_dump_json(indent=2), + encoding="utf-8", + ) + return manifest + + def _atoms_for_path(self, path: Path) -> list[EvidenceAtom]: + resolved = path.resolve() + atoms: list[EvidenceAtom] = [] + for atom in self.db.list_atoms(): + try: + if Path(atom.source).resolve() == resolved: + atoms.append(atom) + except OSError: + if atom.source == str(path): + atoms.append(atom) + return atoms + + def _latest_cartridge_id(self) -> str | None: + rows = self.db.list_cartridges() + return str(rows[0]["cartridge_id"]) if rows else None + + def _model_for_kind(self, kind: TrainingAdapterKind) -> str: + if kind == TrainingAdapterKind.hme_router: + return self.settings.planner_model_id + return self.settings.model_id + + def _atom_id_for_path(self, path: Path, text: str) -> str: + atoms = self._atoms_for_path(path) + return atoms[0].atom_id if atoms else f"{path.stem}:{sha256_bytes(text.encode('utf-8'))[:12]}" + + +def encode_training_sample( + sample: TrainingSample, + processor: Any, + device: Any, + max_length: int, +) -> dict[str, Any]: + messages = messages_for_sample(sample, include_answer=True) + prompt_messages = messages_for_sample(sample, include_answer=False) + image_inputs, video_inputs = vision_inputs(messages) + text = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=False) + prompt_text = processor.apply_chat_template( + prompt_messages, + tokenize=False, + add_generation_prompt=True, + enable_thinking=False, + ) + inputs = processor( + text=[text], + images=image_inputs, + videos=video_inputs, + return_tensors="pt", + ) + prompt_inputs = processor( + text=[prompt_text], + images=image_inputs, + videos=video_inputs, + return_tensors="pt", + ) + labels = inputs["input_ids"].clone() + prompt_length = min(int(prompt_inputs["input_ids"].shape[-1]), int(labels.shape[-1])) + labels[:, :prompt_length] = -100 + pad_token_id = getattr(processor, "tokenizer", processor).pad_token_id + if pad_token_id is not None: + labels[labels == pad_token_id] = -100 + if int((labels != -100).sum().item()) == 0: + raise ValueError(f"Sample {sample.id} produced no trainable tokens") + inputs["labels"] = labels + if int(inputs["input_ids"].shape[-1]) > max_length: + for key in ("input_ids", "attention_mask", "labels"): + if key in inputs: + inputs[key] = inputs[key][:, -max_length:] + return {key: value.to(device) if hasattr(value, "to") else value for key, value in inputs.items()} + + +def encode_retrieval_batch( + texts: list[str], + processor: Any, + device: Any, + max_length: int, +) -> dict[str, Any]: + tokenizer = getattr(processor, "tokenizer", processor) + inputs = tokenizer( + texts, + padding=True, + truncation=True, + max_length=max_length, + return_tensors="pt", + ) + return {key: value.to(device) if hasattr(value, "to") else value for key, value in inputs.items()} + + +def pool_embeddings(outputs: Any, attention_mask: Any) -> Any: + import torch.nn.functional as F + + hidden = outputs.last_hidden_state + mask = attention_mask.unsqueeze(-1).to(hidden.dtype) + pooled = (hidden * mask).sum(dim=1) / mask.sum(dim=1).clamp_min(1.0) + return F.normalize(pooled, dim=-1) + + +def messages_for_sample(sample: TrainingSample, include_answer: bool) -> list[dict[str, Any]]: + user_content: list[dict[str, Any]] = [] + for item in sample.inputs: + item_type = item.get("type") + if item_type in {"image", "pdf_page", "ui_screenshot"} and item.get("path"): + path = str(item["path"]) + if path.startswith("object://"): + user_content.append({"type": "text", "text": f"Image object reference: {path}"}) + else: + image_part: dict[str, Any] = {"type": "image", "image": path} + for key in ("min_pixels", "max_pixels"): + if key in item: + image_part[key] = item[key] + user_content.append(image_part) + elif item_type == "text" and item.get("text"): + user_content.append({"type": "text", "text": str(item["text"])}) + else: + user_content.append({"type": "text", "text": json.dumps(item, ensure_ascii=False)}) + messages: list[dict[str, Any]] = [ + {"role": "system", "content": SYSTEM_PROMPT}, + {"role": "user", "content": user_content}, + ] + if include_answer: + messages.append( + { + "role": "assistant", + "content": json.dumps(sample.output, ensure_ascii=False, separators=(",", ":")), + } + ) + return messages + + +def vision_inputs(messages: list[dict[str, Any]]) -> tuple[list[Any] | None, list[Any] | None]: + has_media = any( + isinstance(content, list) + and any(part.get("type") in {"image", "video"} for part in content if isinstance(part, dict)) + for content in (message.get("content") for message in messages) + ) + if not has_media: + return None, None + from qwen_vl_utils import process_vision_info + + return process_vision_info(messages) + + +def infer_lora_targets(model: Any, kind: TrainingAdapterKind) -> list[str]: + import torch + + core_leafs = {"q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"} + mlp_leafs = {"gate_proj", "up_proj", "down_proj"} + found_leafs: set[str] = set() + exact: set[str] = set() + for name, module in model.named_modules(): + if not isinstance(module, torch.nn.Linear): + continue + leaf = name.rsplit(".", 1)[-1] + lower = name.lower() + if kind == TrainingAdapterKind.hme_core_lora and leaf in core_leafs: + found_leafs.add(leaf) + elif kind == TrainingAdapterKind.hme_vision_lora and ("visual" in lower or "vision" in lower): + exact.add(name) + elif kind == TrainingAdapterKind.hme_ttt_vl and (leaf in mlp_leafs or "visual" in lower): + exact.add(name if "visual" in lower else leaf) + elif kind == TrainingAdapterKind.hme_visual_mtp and ( + "mtp" in lower or "draft" in lower or name == "lm_head" + ): + exact.add(name) + elif kind in {TrainingAdapterKind.hme_router, TrainingAdapterKind.hme_retrieval} and leaf in core_leafs: + found_leafs.add(leaf) + targets = sorted(exact or found_leafs) + if not targets: + for name, module in model.named_modules(): + if isinstance(module, torch.nn.Linear) and not name.endswith("lm_head"): + targets.append(name.rsplit(".", 1)[-1]) + targets = sorted(set(targets)) + if not targets: + raise ValueError(f"No LoRA target modules found for {kind.value}") + return targets + + +def trainable_named_parameters(model: Any) -> list[tuple[str, Any]]: + return [(name, param) for name, param in model.named_parameters() if param.requires_grad] + + +def trainable_state_cpu(named_parameters: list[tuple[str, Any]]) -> dict[str, Any]: + return {name: param.detach().float().cpu().clone() for name, param in named_parameters} + + +def trainable_state_sha256(named_parameters: list[tuple[str, Any]]) -> str: + digest = hashlib.sha256() + for name, param in named_parameters: + tensor = param.detach().float().cpu().contiguous() + digest.update(name.encode("utf-8")) + digest.update(str(tuple(tensor.shape)).encode("utf-8")) + digest.update(tensor.numpy().tobytes()) + return digest.hexdigest() + + +def trainable_l2_norm(named_parameters: list[tuple[str, Any]]) -> float: + total = 0.0 + for _, param in named_parameters: + tensor = param.detach().float() + total += float(tensor.square().sum().detach().cpu()) + return math.sqrt(total) + + +def gradient_l2_norm(named_parameters: list[tuple[str, Any]]) -> float: + total = 0.0 + for _, param in named_parameters: + if param.grad is None: + continue + grad = param.grad.detach().float() + total += float(grad.square().sum().detach().cpu()) + return math.sqrt(total) + + +def trainable_update_l2(before_update: dict[str, Any], named_parameters: list[tuple[str, Any]]) -> float: + total = 0.0 + for name, param in named_parameters: + delta = param.detach().float().cpu() - before_update[name] + total += float(delta.square().sum()) + return math.sqrt(total) + + +def require_cuda_bf16(torch_module: Any) -> None: + if not torch_module.cuda.is_available(): + raise RuntimeError("PRD 12.2 training requires CUDA BF16 on the local RTX 4090.") + bf16_supported = getattr(torch_module.cuda, "is_bf16_supported", lambda: False) + if not bool(bf16_supported()): + raise RuntimeError("PRD 12.2 training requires BF16-capable CUDA hardware.") + + +def activate_unsloth_training() -> None: + required = { + "unsloth": package_version("unsloth"), + "unsloth_zoo": package_version("unsloth_zoo"), + "triton": package_version("triton-windows") or package_version("triton"), + "xformers": package_version("xformers"), + } + missing = [name for name, installed in required.items() if not installed] + if missing: + raise RuntimeError( + "PRD 12.2 training requires installed Unsloth dependencies: " + ", ".join(missing) + ) + with bitsandbytes_gaudi_probe_guard(): + __import__("unsloth") + + +def enable_gradient_checkpointing(model: Any) -> bool: + if not hasattr(model, "gradient_checkpointing_enable"): + return False + try: + model.gradient_checkpointing_enable(gradient_checkpointing_kwargs={"use_reentrant": False}) + except TypeError: + model.gradient_checkpointing_enable() + return True + + +def model_type_for_proof(model: Any) -> str | None: + current = model + seen: set[int] = set() + while current is not None and id(current) not in seen: + seen.add(id(current)) + config = getattr(current, "config", None) + model_type = getattr(config, "model_type", None) + if isinstance(model_type, str) and model_type: + return model_type + current = getattr(current, "model", None) + return None + + +def gradient_checkpointing_enabled(model: Any) -> bool: + if bool(getattr(model, "is_gradient_checkpointing", False)): + return True + config = getattr(model, "config", None) + if bool(getattr(config, "gradient_checkpointing", False)): + return True + return bool(getattr(model, "_gradient_checkpointing_func", None)) + + +def make_optimizer(model: Any, learning_rate: float) -> tuple[Any, str]: + params = [param for param in model.parameters() if param.requires_grad] + try: + with bitsandbytes_gaudi_probe_guard(): + import bitsandbytes as bnb + except Exception as exc: + raise RuntimeError("PRD 12.2 training requires bitsandbytes PagedAdamW8bit.") from exc + try: + return bnb.optim.PagedAdamW8bit(params, lr=learning_rate), "paged_adamw_8bit" + except Exception as exc: + raise RuntimeError("bitsandbytes PagedAdamW8bit failed to initialize.") from exc + + +def optimizer_family(optimizer_name: str) -> str: + if optimizer_name == "paged_adamw_8bit": + return "8-bit AdamW / paged optimizer" + return optimizer_name + + +@contextmanager +def bitsandbytes_gaudi_probe_guard() -> Any: + original_run = subprocess.run + + def guarded_run(*args: Any, **kwargs: Any) -> Any: + command = args[0] if args else kwargs.get("args") + if isinstance(command, str) and "habana-torch-plugin" in command: + return subprocess.CompletedProcess(command, 0, "", "") + return original_run(*args, **kwargs) + + subprocess.run = guarded_run + try: + yield + finally: + subprocess.run = original_run + + +def training_contract( + *, + gradient_checkpointing: bool, + input_require_grads: bool, + optimizer_name: str, + unsloth_model_type: str | None, +) -> dict[str, Any]: + return { + "proof_version": TRAINING_PROOF_VERSION, + "training_method": TRAINING_METHOD, + "precision": TRAINING_PRECISION, + "framework": TRAINING_FRAMEWORK, + "optimizer_family": optimizer_family(optimizer_name), + "gradient_checkpointing": gradient_checkpointing, + "gradient_checkpointing_mode": GRADIENT_CHECKPOINTING_MODE if gradient_checkpointing else None, + "input_require_grads": input_require_grads, + "autocast_dtype": AUTOCAST_DTYPE, + "unsloth_version": package_version("unsloth"), + "triton_version": package_version("triton-windows") or package_version("triton"), + "xformers_version": package_version("xformers"), + "unsloth_model_type": unsloth_model_type, + } + + +def training_environment(torch_module: Any) -> dict[str, Any]: + cuda_available = bool(torch_module.cuda.is_available()) + device_name = torch_module.cuda.get_device_name(0) if cuda_available else platform.processor() + capability = torch_module.cuda.get_device_capability(0) if cuda_available else None + bf16_supported = ( + bool(getattr(torch_module.cuda, "is_bf16_supported", lambda: False)()) + if cuda_available + else False + ) + return { + "python": platform.python_version(), + "platform": platform.platform(), + "device": "cuda:0" if cuda_available else "cpu", + "device_name": device_name, + "cuda_available": cuda_available, + "bf16_supported": bf16_supported, + "cuda_capability": list(capability) if capability else None, + "torch_version": getattr(torch_module, "__version__", None), + "torch_cuda_version": getattr(torch_module.version, "cuda", None), + "transformers_version": package_version("transformers"), + "peft_version": package_version("peft"), + "bitsandbytes_version": package_version("bitsandbytes"), + "unsloth_version": package_version("unsloth"), + "unsloth_zoo_version": package_version("unsloth_zoo"), + "triton_version": package_version("triton-windows") or package_version("triton"), + "xformers_version": package_version("xformers"), + } + + +def package_version(package_name: str) -> str | None: + try: + return version(package_name) + except PackageNotFoundError: + return None + + +def cuda_peak_memory_mb(torch_module: Any) -> float | None: + if not torch_module.cuda.is_available(): + return None + return float(torch_module.cuda.max_memory_allocated() / 1024 / 1024) + + +def safetensor_weight_stats(path: Path) -> dict[str, Any]: + report: dict[str, Any] = { + "path": str(path), + "exists": path.exists(), + "sha256": sha256_file(path) if path.exists() else None, + "tensor_count": 0, + "lora_tensor_count": 0, + "total_elements": 0, + "nonzero_elements": 0, + "nonzero_tensor_count": 0, + "finite_tensor_count": 0, + "all_finite": False, + "max_abs": None, + "mean_abs": None, + "dtype_counts": {}, + "key_sample": [], + "valid": False, + } + if not path.exists(): + return report + + import torch + from safetensors import safe_open + + total_abs = 0.0 + max_abs = 0.0 + dtype_counts: dict[str, int] = {} + with safe_open(path, framework="pt", device="cpu") as handle: + keys = list(handle.keys()) + report["tensor_count"] = len(keys) + report["key_sample"] = keys[:12] + for key in keys: + tensor = handle.get_tensor(key) + numel = int(tensor.numel()) + dtype = str(tensor.dtype).replace("torch.", "") + dtype_counts[dtype] = dtype_counts.get(dtype, 0) + 1 + report["total_elements"] += numel + if "lora_" in key: + report["lora_tensor_count"] += 1 + if numel == 0: + continue + tensor_float = tensor.detach().float() + finite = bool(torch.isfinite(tensor_float).all().item()) + if finite: + report["finite_tensor_count"] += 1 + nonzero = int(torch.count_nonzero(tensor).item()) + report["nonzero_elements"] += nonzero + if nonzero > 0: + report["nonzero_tensor_count"] += 1 + abs_tensor = tensor_float.abs() + tensor_max_abs = float(abs_tensor.max().item()) + max_abs = max(max_abs, tensor_max_abs) + total_abs += float(abs_tensor.sum().item()) + + report["dtype_counts"] = dtype_counts + report["all_finite"] = ( + report["tensor_count"] > 0 and report["finite_tensor_count"] == report["tensor_count"] + ) + if int(report["total_elements"]) > 0: + report["max_abs"] = max_abs + report["mean_abs"] = total_abs / int(report["total_elements"]) + report["valid"] = ( + report["exists"] + and int(report["tensor_count"]) > 0 + and int(report["lora_tensor_count"]) > 0 + and int(report["total_elements"]) > 0 + and int(report["nonzero_elements"]) > 0 + and bool(report["all_finite"]) + and report["max_abs"] is not None + and float(report["max_abs"]) > 0.0 + ) + return report + + +def write_trace(handle: TextIO, payload: dict[str, Any]) -> None: + handle.write(json.dumps(payload, ensure_ascii=False, separators=(",", ":")) + "\n") + handle.flush() + + +def write_training_summary(path: Path, payload: dict[str, Any]) -> None: + path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8") + + +def split_markdown_sections(text: str) -> list[dict[str, str]]: + matches = list(re.finditer(r"^(#{2,3})\s+(.+?)\s*$", text, flags=re.MULTILINE)) + sections: list[dict[str, str]] = [] + for index, match in enumerate(matches): + start = match.end() + end = matches[index + 1].start() if index + 1 < len(matches) else len(text) + sections.append({"title": match.group(2).strip(), "body": text[start:end].strip()}) + return sections + + +def section_by_title(text: str, title_fragment: str) -> str: + for section in split_markdown_sections(text): + if title_fragment in section["title"]: + return section["body"] + return "" + + +def write_jsonl(path: Path, samples: list[TrainingSample]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w", encoding="utf-8", newline="\n") as handle: + for sample in samples: + handle.write(sample.model_dump_json() + "\n") + + +def prepare_training_image(source: Path, media_dir: Path, max_side: int = 896) -> Path: + from PIL import Image + + image = Image.open(source).convert("RGB") + image.thumbnail((max_side, max_side)) + target = media_dir / f"{source.stem}_{sha256_file(source)[:10]}.png" + image.save(target) + return target + + +def render_prd_training_card(text: str, atom_id: str, target: Path) -> Path: + from PIL import Image, ImageDraw, ImageFont + + target.parent.mkdir(parents=True, exist_ok=True) + image = Image.new("RGB", (1024, 768), "#f7f5ef") + draw = ImageDraw.Draw(image) + font_path = Path("C:/Windows/Fonts/msyh.ttc") + title_font = ImageFont.truetype(str(font_path), 34) if font_path.exists() else ImageFont.load_default() + body_font = ImageFont.truetype(str(font_path), 21) if font_path.exists() else ImageFont.load_default() + small_font = ImageFont.truetype(str(font_path), 18) if font_path.exists() else ImageFont.load_default() + draw.rectangle((28, 28, 996, 740), outline="#1f5f4a", width=3) + draw.text((52, 48), "Ely-Eye PRD Training Card", fill="#09201b", font=title_font) + draw.text((52, 100), f"Evidence Atom: {atom_id}", fill="#1f5f4a", font=small_font) + source = ( + section_by_title(text, "12.1 训练目标") + + "\n" + + section_by_title(text, "12.2 训练策略") + + "\n" + + section_by_title(text, "12.3 训练数据结构") + ) + y = 148 + for line in wrap_text(compact_text(source, 1500), width=42): + draw.text((52, y), line, fill="#09201b", font=body_font) + y += 31 + if y > 700: + break + image.save(target) + return target + + +def wrap_text(text: str, width: int) -> list[str]: + lines: list[str] = [] + current = "" + for char in text: + current += char + if len(current) >= width or char == "\n": + lines.append(current.strip()) + current = "" + if current.strip(): + lines.append(current.strip()) + return lines + + +def compact_text(text: str, limit: int) -> str: + compacted = re.sub(r"\s+", " ", text).strip() + if len(compacted) <= limit: + return compacted + return compacted[: limit - 3].rstrip() + "..." + + +def slugify(value: str) -> str: + slug = re.sub(r"[^a-zA-Z0-9\u4e00-\u9fff]+", "-", value).strip("-").lower() + return slug[:80] or "section" + + +def adapter_id(kind: TrainingAdapterKind, dataset_path: Path, model_id: str) -> str: + raw = f"{kind.value}:{model_id}:{dataset_path.resolve()}:{datetime.now(timezone.utc).isoformat()}" + return f"{kind.value}_{sha256_bytes(raw.encode('utf-8'))[:12]}" + + +def relative_artifacts(root: Path) -> dict[str, str]: + artifacts: dict[str, str] = {} + for path in sorted(root.rglob("*")): + if path.is_file() and path.name != "adapter_manifest.json": + artifacts[path.stem] = path.relative_to(root).as_posix() + return artifacts + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + shutil.copyfileobj(handle, _DigestWriter(digest)) + return digest.hexdigest() + + +def sha256_tree(root: Path) -> str: + digest = hashlib.sha256() + for path in sorted(root.rglob("*")): + if path.is_file() and path.name != "adapter_manifest.json": + digest.update(path.relative_to(root).as_posix().encode("utf-8")) + with path.open("rb") as handle: + shutil.copyfileobj(handle, _DigestWriter(digest)) + return digest.hexdigest() + + +def sha256_bytes(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + +class _DigestWriter: + def __init__(self, digest: "hashlib._Hash") -> None: + self.digest = digest + + def write(self, data: bytes) -> int: + self.digest.update(data) + return len(data) diff --git a/backend/src/ely_eye/verifier.py b/backend/src/ely_eye/verifier.py new file mode 100644 index 0000000..9447245 --- /dev/null +++ b/backend/src/ely_eye/verifier.py @@ -0,0 +1,232 @@ +from __future__ import annotations + +import json +import re +from dataclasses import dataclass +from typing import Any + +from .schemas import CitationReport, CompiledContext + + +ATOM_PATTERN = re.compile(r"[A-Za-z0-9_./\\:-]+:(?:text|image|page|frame|region)[A-Za-z0-9_./\\:#=-]*") + + +class Verifier: + def verify_answer(self, raw_answer: str, context: CompiledContext) -> tuple[str, list[str], CitationReport]: + parsed = self._parse_answer(raw_answer) + answer = parsed.get("answer", raw_answer) + citations = parsed.get("citations") or self._extract_citations(raw_answer) + citations = normalize_citations(citations) + available = {hit.atom.atom_id for hit in context.hits} + cited_available, missing = resolve_citations(citations, available) + accuracy = len(cited_available) / len(citations) if citations else 0.0 + contradiction_notes = parsed.get("contradiction_notes") or [] + if isinstance(contradiction_notes, str): + contradiction_notes = [contradiction_notes] + confidence = float(parsed.get("confidence", accuracy)) + return ( + str(answer), + citations, + CitationReport( + cited_atom_ids=cited_available, + missing_atom_ids=missing, + citation_accuracy=accuracy, + contradiction_notes=[str(note) for note in contradiction_notes], + confidence=max(0.0, min(1.0, confidence)), + ), + ) + + def _parse_answer(self, raw_answer: str) -> dict[str, Any]: + stripped = raw_answer.strip() + if not stripped: + return {"answer": ""} + candidates = [stripped] + if "```" in stripped: + parts = stripped.split("```") + candidates.extend(part.removeprefix("json").strip() for part in parts) + start = stripped.find("{") + end = stripped.rfind("}") + if start >= 0 and end > start: + candidates.append(stripped[start : end + 1]) + for candidate in candidates: + try: + value = json.loads(candidate) + except json.JSONDecodeError: + continue + if isinstance(value, dict): + return value + return {"answer": raw_answer, "citations": self._extract_citations(raw_answer)} + + def _extract_citations(self, raw_answer: str) -> list[str]: + return sorted(set(ATOM_PATTERN.findall(raw_answer))) + + +def normalize_citations(value: Any) -> list[str]: + if value is None: + return [] + if isinstance(value, str): + return [value] + normalized: list[str] = [] + if isinstance(value, list): + for item in value: + if isinstance(item, str): + normalized.append(item) + elif isinstance(item, dict) and "atom_id" in item: + normalized.append(str(item["atom_id"])) + return normalized + + +def resolve_citations(citations: list[str], available: set[str]) -> tuple[list[str], list[str]]: + cited_available: list[str] = [] + missing: list[str] = [] + for citation in citations: + atom_id = citation.split("#", 1)[0] + if citation in available: + cited_available.append(citation) + elif atom_id in available: + cited_available.append(atom_id) + else: + missing.append(citation) + return list(dict.fromkeys(cited_available)), missing + + +@dataclass(frozen=True) +class DriftRule: + """One declarative extractor for a design fact (PRD 11.15 taxonomy). + + ``pattern`` exposes an optional ``scope`` group (component qualifier) and a + mandatory ``value`` group. The same multi-value-across-sources mechanism + surfaces every drift kind; ``category`` is the PRD baseline label. + """ + + name: str + category: str + pattern: re.Pattern[str] + value_kind: str + + +class VisualContradictionLens: + """Detect cross-version visual contradictions (PRD 11.15). + + A single mechanism backs the whole taxonomy: extract scoped ``(attribute, + value)`` design facts from each Evidence Atom, group identical attributes + across atoms, and flag any attribute carrying more than one distinct value + across at least two distinct sources. Requiring two sources prevents a + single CSS file with many rules from reading as a contradiction. Source + kind and version metadata refine each note into design-token, layout, + typography, copy, visual-code, or temporal drift. + """ + + RULES: tuple[DriftRule, ...] = ( + DriftRule( + "radius", + "design-token drift", + re.compile( + r"(?P[A-Za-z][\w .-]*?)?\.?(?:border-)?radius\s*[:=]\s*(?P\d+(?:\.\d+)?)\s*px", + re.IGNORECASE, + ), + "length", + ), + DriftRule( + "color", + "design-token drift", + re.compile( + r"(?P[A-Za-z][\w .-]*?)?\.?(?:colou?r|background|fill)\s*[:=]\s*(?P#[0-9a-fA-F]{3,8})", + re.IGNORECASE, + ), + "color", + ), + DriftRule( + "font-size", + "typography drift", + re.compile( + r"(?P[A-Za-z][\w .-]*?)?\.?font-size\s*[:=]\s*(?P\d+(?:\.\d+)?)\s*(?:px|rem|pt)", + re.IGNORECASE, + ), + "length", + ), + DriftRule( + "spacing", + "layout drift", + re.compile( + r"(?P[A-Za-z][\w .-]*?)?\.?(?:padding|margin|gap|spacing)\s*[:=]\s*(?P\d+(?:\.\d+)?)\s*px", + re.IGNORECASE, + ), + "length", + ), + DriftRule( + "label", + "copy drift", + re.compile( + r"(?P[A-Za-z][\w .-]*?)?\.?(?:label|copy|cta|caption)\s*[:=]\s*[\"'](?P[^\"']+)[\"']", + re.IGNORECASE, + ), + "text", + ), + ) + + def detect(self, context: CompiledContext) -> list[str]: + observations: dict[str, dict[str, set[str]]] = {} + raw_display: dict[str, dict[str, str]] = {} + category_of: dict[str, str] = {} + source_kind: dict[str, str] = {} + source_version: dict[str, str] = {} + for hit in context.hits: + atom = hit.atom + source_kind[atom.source_id] = str(atom.metadata.get("source_kind") or atom.modality.value) + version = atom.metadata.get("version") + if version is not None: + source_version[atom.source_id] = str(version) + for rule in self.RULES: + for match in rule.pattern.finditer(atom.text): + scope = (match.groupdict().get("scope") or "").strip().lower().rstrip(".") + key = f"{scope}.{rule.name}" if scope else rule.name + normalized = normalize_design_value(match.group("value"), rule.value_kind) + if not normalized: + continue + observations.setdefault(key, {}).setdefault(normalized, set()).add(atom.source_id) + raw_display.setdefault(key, {})[normalized] = match.group("value").strip() + category_of[key] = rule.category + notes: list[str] = [] + for key in sorted(observations): + value_sources = observations[key] + sources = set().union(*value_sources.values()) + if len(value_sources) > 1 and len(sources) > 1: + category = self._classify(category_of[key], sources, source_kind, source_version) + values = ", ".join(raw_display[key][value] for value in sorted(value_sources)) + notes.append(f"{key} {category}: {values}") + return notes + + @staticmethod + def _classify( + baseline: str, + sources: set[str], + source_kind: dict[str, str], + source_version: dict[str, str], + ) -> str: + kinds = {source_kind.get(source, "") for source in sources} + design_kinds = {"figma", "spec", "design", "ui_screenshot", "image", "pdf_page", "pdf_region"} + code_kinds = {"code", "css"} + if kinds & design_kinds and kinds & code_kinds: + return "visual-code drift" + versions = {source_version[source] for source in sources if source in source_version} + if len(versions) > 1: + return "temporal drift" + return baseline + + +def normalize_design_value(value: str, value_kind: str) -> str: + cleaned = value.strip() + if not cleaned: + return "" + if value_kind == "length": + try: + return f"{float(cleaned):g}" + except ValueError: + return "" + if value_kind == "color": + hex_value = cleaned.lower().lstrip("#") + if len(hex_value) in {3, 4}: + hex_value = "".join(component * 2 for component in hex_value) + return f"#{hex_value}" + return " ".join(cleaned.lower().split()) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..762e72a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +services: + ely-eye: + build: . + ports: + - "8765:8765" + volumes: + - ./.ely_eye:/data/ely-eye + environment: + ELY_EYE_HOME: /data/ely-eye + ELY_EYE_RUNTIME_BACKEND: sglang + ELY_EYE_SGLANG_BASE_URL: http://host.docker.internal:8000/v1 + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: 1 + capabilities: [gpu] diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..8e5fcec --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,32 @@ +import js from '@eslint/js'; +import globals from 'globals'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import tseslint from 'typescript-eslint'; + +export default [ + { ignores: ['dist'] }, + ...tseslint.configs.recommended, + { + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2022, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...js.configs.recommended.rules, + ...reactHooks.configs.recommended.rules, + 'no-undef': 'off', + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + }, + }, +]; diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..73e815e --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + Ely-Eye + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..3586fcc --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3019 @@ +{ + "name": "ely-eye-frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ely-eye-frontend", + "version": "0.1.0", + "dependencies": { + "@vitejs/plugin-react": "^6.0.2", + "lucide-react": "^0.562.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "typescript": "^5.9.0", + "vite": "^8.0.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.0", + "@types/node": "^24.10.0", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "eslint": "^9.39.0", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "playwright": "^1.60.0", + "typescript-eslint": "^8.50.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz", + "integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz", + "integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz", + "integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz", + "integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz", + "integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz", + "integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz", + "integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz", + "integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz", + "integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz", + "integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz", + "integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz", + "integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz", + "integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz", + "integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz", + "integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz", + "integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.12.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz", + "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.15", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", + "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.0.tgz", + "integrity": "sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.60.0", + "@typescript-eslint/type-utils": "8.60.0", + "@typescript-eslint/utils": "8.60.0", + "@typescript-eslint/visitor-keys": "8.60.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.60.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.0.tgz", + "integrity": "sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.60.0", + "@typescript-eslint/types": "8.60.0", + "@typescript-eslint/typescript-estree": "8.60.0", + "@typescript-eslint/visitor-keys": "8.60.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.0.tgz", + "integrity": "sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.60.0", + "@typescript-eslint/types": "^8.60.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.0.tgz", + "integrity": "sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.0", + "@typescript-eslint/visitor-keys": "8.60.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.0.tgz", + "integrity": "sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.0.tgz", + "integrity": "sha512-SX46wEUtitCpq7AN38HkUU/+zvUpdKf7ephtWAFgckH8O7PQIyL5gvrhQgBLuEYgLfuKWOVvWVskMbuFHAz5xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.0", + "@typescript-eslint/typescript-estree": "8.60.0", + "@typescript-eslint/utils": "8.60.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.0.tgz", + "integrity": "sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.0.tgz", + "integrity": "sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.60.0", + "@typescript-eslint/tsconfig-utils": "8.60.0", + "@typescript-eslint/types": "8.60.0", + "@typescript-eslint/visitor-keys": "8.60.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.0.tgz", + "integrity": "sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.60.0", + "@typescript-eslint/types": "8.60.0", + "@typescript-eslint/typescript-estree": "8.60.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.0.tgz", + "integrity": "sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", + "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==", + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.32", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz", + "integrity": "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.363", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.363.tgz", + "integrity": "sha512-VjUKPyWzGnT1fujlkEGC/BvN70Hh70KXtAqcmniXviYlJC/ivcT+BWGPyxWVbJZLfvtKR6dqg1L7T7pgAMBtWA==", + "dev": true, + "license": "ISC" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.562.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz", + "integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz", + "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.6" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rolldown": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz", + "integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.132.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.2", + "@rolldown/binding-darwin-arm64": "1.0.2", + "@rolldown/binding-darwin-x64": "1.0.2", + "@rolldown/binding-freebsd-x64": "1.0.2", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", + "@rolldown/binding-linux-arm64-gnu": "1.0.2", + "@rolldown/binding-linux-arm64-musl": "1.0.2", + "@rolldown/binding-linux-ppc64-gnu": "1.0.2", + "@rolldown/binding-linux-s390x-gnu": "1.0.2", + "@rolldown/binding-linux-x64-gnu": "1.0.2", + "@rolldown/binding-linux-x64-musl": "1.0.2", + "@rolldown/binding-openharmony-arm64": "1.0.2", + "@rolldown/binding-wasm32-wasi": "1.0.2", + "@rolldown/binding-win32-arm64-msvc": "1.0.2", + "@rolldown/binding-win32-x64-msvc": "1.0.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.60.0.tgz", + "integrity": "sha512-9f65qWLZdAW9m1JaxBDUHcqRUfL8bkxxXL7XxEfI+F09q56PkBvIfCjLF3yInsDM/BBmwkqmCQdCZe/RYlIWEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.60.0", + "@typescript-eslint/parser": "8.60.0", + "@typescript-eslint/typescript-estree": "8.60.0", + "@typescript-eslint/utils": "8.60.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "8.0.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz", + "integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==", + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.2", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..85ca7f2 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,33 @@ +{ + "name": "ely-eye-frontend", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --host 127.0.0.1 --port 5173", + "build": "tsc -b && vite build", + "preview": "vite preview --host 127.0.0.1 --port 5173", + "lint": "eslint .", + "verify:ui": "node scripts/verify-ui.mjs" + }, + "dependencies": { + "@vitejs/plugin-react": "^6.0.2", + "lucide-react": "^0.562.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "typescript": "^5.9.0", + "vite": "^8.0.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.0", + "@types/node": "^24.10.0", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "eslint": "^9.39.0", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "playwright": "^1.60.0", + "typescript-eslint": "^8.50.0" + } +} diff --git a/frontend/scripts/verify-ui.mjs b/frontend/scripts/verify-ui.mjs new file mode 100644 index 0000000..313a781 --- /dev/null +++ b/frontend/scripts/verify-ui.mjs @@ -0,0 +1,32 @@ +import { chromium } from 'playwright'; +import { mkdir } from 'node:fs/promises'; +import path from 'node:path'; + +const root = path.resolve(process.cwd(), '..'); +const outputDir = path.join(root, '.ely_eye', 'proofs'); +await mkdir(outputDir, { recursive: true }); + +const browser = await chromium.launch(); +const page = await browser.newPage({ viewport: { width: 1440, height: 1100 } }); +const consoleErrors = []; +page.on('console', (message) => { + if (message.type() === 'error') { + consoleErrors.push(message.text()); + } +}); + +await page.goto('http://127.0.0.1:5173/', { waitUntil: 'networkidle' }); +await page.getByRole('heading', { name: 'Local VLM Context OS' }).waitFor(); +await page.getByLabel('system status').getByText('Qwen/Qwen3.5-9B').waitFor(); +await page.getByRole('heading', { name: 'Adapter Matrix' }).waitFor(); +await page.getByRole('heading', { name: 'PRD Proof Suite' }).waitFor(); +await page.getByText(/\d+\/\d+ passed/).waitFor(); +await page.screenshot({ path: path.join(outputDir, 'ely-eye-dashboard.png'), fullPage: true }); +const status = await page.locator('.kpi-grid').innerText(); +await browser.close(); + +if (consoleErrors.length > 0) { + throw new Error(consoleErrors.join('\n')); +} + +console.log(JSON.stringify({ screenshot: path.join(outputDir, 'ely-eye-dashboard.png'), status }, null, 2)); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..a70ea15 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,517 @@ +import { + Activity, + Archive, + Braces, + CheckCircle, + Cpu, + Database, + FileSearch, + Gauge, + GitBranch, + Image as ImageIcon, + Play, + RefreshCw, + Send, + Server, + ShieldCheck, + Upload, +} from 'lucide-react'; +import { FormEvent, ReactNode, useEffect, useMemo, useState } from 'react'; +import { + askQuestion, + getAdapters, + getLatestProofSuite, + getMemoryMap, + getStatus, + ingestPath, + runHashHop, + runProofSuite, + uploadFiles, +} from './api'; +import type { + AdapterStatus, + AnswerResponse, + HashHopProof, + IngestResponse, + MemoryMap, + ProofSuiteReport, + RuntimeProfile, + SystemStatus, +} from './types'; + +const profiles: Array<{ id: RuntimeProfile; label: string }> = [ + { id: 'live_demo', label: 'Live Demo' }, + { id: 'extreme_context', label: 'Extreme' }, + { id: 'library_100m', label: '100M Library' }, + { id: 'research_theater', label: 'Research' }, +]; + +export function App() { + const [status, setStatus] = useState(null); + const [memoryMap, setMemoryMap] = useState(null); + const [adapters, setAdapters] = useState([]); + const [profile, setProfile] = useState('library_100m'); + const [path, setPath] = useState(''); + const [cartridgeName, setCartridgeName] = useState('ely-eye-cartridge'); + const [files, setFiles] = useState([]); + const [question, setQuestion] = useState(''); + const [answer, setAnswer] = useState(null); + const [hashHop, setHashHop] = useState(null); + const [proofSuite, setProofSuite] = useState(null); + const [lastIngest, setLastIngest] = useState(null); + const [busy, setBusy] = useState(null); + const [error, setError] = useState(null); + + const refresh = async () => { + setError(null); + const [nextStatus, nextMap, nextAdapters, nextProofSuite] = await Promise.all([ + getStatus(), + getMemoryMap(), + getAdapters(), + getLatestProofSuite().catch(() => null), + ]); + setStatus(nextStatus); + setMemoryMap(nextMap); + setAdapters(nextAdapters); + setProofSuite(nextProofSuite); + }; + + useEffect(() => { + let cancelled = false; + async function loadInitial() { + try { + const [nextStatus, nextMap, nextAdapters, nextProofSuite] = await Promise.all([ + getStatus(), + getMemoryMap(), + getAdapters(), + getLatestProofSuite().catch(() => null), + ]); + if (!cancelled) { + setStatus(nextStatus); + setMemoryMap(nextMap); + setAdapters(nextAdapters); + setProofSuite(nextProofSuite); + } + } catch (err) { + if (!cancelled) { + setError(errorMessage(err)); + } + } + } + void loadInitial(); + return () => { + cancelled = true; + }; + }, []); + + const memoryLayers = useMemo(() => status?.memory.layers ?? [], [status]); + const boundAdapters = useMemo(() => adapters.filter((adapter) => adapter.cartridge_bound).length, [adapters]); + const passedProofChecks = useMemo( + () => proofSuite?.checks.filter((check) => check.status === 'passed').length ?? 0, + [proofSuite], + ); + + async function submitPath(event: FormEvent) { + event.preventDefault(); + setBusy('ingest-path'); + setError(null); + try { + const result = await ingestPath(path, cartridgeName); + setLastIngest(result); + await refresh(); + } catch (err) { + setError(errorMessage(err)); + } finally { + setBusy(null); + } + } + + async function submitUpload(event: FormEvent) { + event.preventDefault(); + setBusy('upload'); + setError(null); + try { + const result = await uploadFiles(files, cartridgeName); + setLastIngest(result); + await refresh(); + } catch (err) { + setError(errorMessage(err)); + } finally { + setBusy(null); + } + } + + async function submitQuestion(event: FormEvent) { + event.preventDefault(); + setBusy('chat'); + setError(null); + try { + const result = await askQuestion(question, profile); + setAnswer(result); + await refresh(); + } catch (err) { + setError(errorMessage(err)); + } finally { + setBusy(null); + } + } + + async function submitHashHop(kind: 'text' | 'visual') { + setBusy(`hashhop-${kind}`); + setError(null); + try { + const result = await runHashHop(kind, kind === 'visual' ? 2 : 2, kind === 'visual' ? 262144 : 1010000); + setHashHop(result); + await refresh(); + } catch (err) { + setError(errorMessage(err)); + } finally { + setBusy(null); + } + } + + async function submitProofSuite() { + setBusy('proof-suite'); + setError(null); + try { + const cartridgeId = memoryMap?.cartridges[0]?.cartridge_id; + const result = await runProofSuite(cartridgeId); + setProofSuite(result); + await refresh(); + } catch (err) { + setError(errorMessage(err)); + } finally { + setBusy(null); + } + } + + return ( +
+
+
+

Ely-Eye v4.0

+

Local VLM Context OS

+
+ +
+ + {error &&
{error}
} + +
+ } label="Runtime" value={status?.runtime.available ? 'ready' : 'offline'} detail={formatRuntimeDetail(status)} /> + } label="GPU" value={status?.runtime.gpu_name ?? 'unknown'} detail={formatVram(status)} /> + } label="Library" value={formatNumber(status?.memory.library_token_equivalent ?? 0)} detail="token eq" /> + } label="Cartridges" value={String(status?.memory.cartridges ?? 0)} detail={`${boundAdapters} adapters bound`} /> +
+ +
+ }> +
+ + + +
+
+ + +
+ {lastIngest && ( +
+ {lastIngest.source_count} sources + {lastIngest.atom_count} atoms + {formatNumber(lastIngest.token_equivalent)} tokens +
+ )} +
+ + }> +
+ {profiles.map((item) => ( + + ))} +
+
+