# LatticeSearch — Architecture

*How the engine works, and **why** it feels instant.*

LatticeSearch is a local, private, offline semantic search engine for your whole PC — "Everything for meaning." It finds files by **name**, by **image content** (CLIP), by **document meaning** (multilingual‑e5), by **sound** (CLAP), by **video frames**, and by a stack of **enrichment** signals (OCR, speech, captions, faces, pixel‑hashes, generation prompts, colors, code symbols). Everything runs on the machine you already own — no NPU, no cloud, no subscription.

This document explains the tiered index architecture, the parallel query path, the warm model‑host split, the absorption/regeneration storage model, the security model, and the cross‑platform seams. For the HTTP surface see [`API.md`](./API.md).

---

## 1. The big picture

The engine (`server.py`) is a single-process `ThreadingHTTPServer` bound to `127.0.0.1:8765`. It loads several **independent per‑modality indexes** into RAM, each a specialist "sense," and fans a query out across them in parallel. There is **no single joint embedding space** — a deliberate decision (no CPU‑friendly, commercially‑licensable unified model exists; ImageBind/Jina‑CLIP‑v2 are CC‑BY‑NC). Instead: *per‑modality indexes + a query router*, presented to the user as one ranked "search everything" result.

Two optional out‑of‑process helpers accelerate the two heaviest costs:

- **`model_host.py`** (`:8767`) — a **warm model host** that keeps CLIP / e5 / CLAP resident and serves embeddings over a socket, so the engine restarts light.
- **`latticesearch-rs`** (`:8766`) — a **Rust name‑tier sidecar**: an mmap byte‑arena with a SIMD substring scan, ~8–25× faster than the Python name tier and a fraction of the RAM.

```mermaid
flowchart TB
    UI["Browser UI (ui.html) · CLI (lattice.py) · MCP (mcp_server.py) · Rust palette"]
    UI -->|"HTTP 127.0.0.1:8765 (+X-LS-Token on mutating)"| ENG

    subgraph ENG["Engine — server.py (:8765)"]
        direction TB
        ROUTER["do_GET / do_POST\nrouter + jitter guard + token/origin gate"]
        POOL["ThreadPoolExecutor (POOL)\nfan tiers out in parallel"]
        ROUTER --> POOL
        POOL --> NAME["NAME · MetaIndex\nRAM blob + _BlobLower + dir-index"]
        POOL --> IMG["IMAGE · SemanticIndex\nCLIP ViT-B-32 + crop tiles + dhash"]
        POOL --> DOC["DOC · DocIndex\nmultilingual-e5 chunks + shards"]
        POOL --> AUD["AUDIO · AudIndex\nCLAP windows"]
        POOL --> VID["VIDEO · VidIndex\nframes + timestamps"]
    end

    NAME -. "LS_RUST_NAME (auto-probe)" .-> RUST["Rust sidecar :8766\nmmap arena + SIMD scan"]
    IMG -. "LS_MODEL_HOST=1" .-> HOST["Model host :8767\nCLIP · e5 · CLAP warm"]
    DOC -. .-> HOST
    AUD -. .-> HOST

    ENG --> DISK["index_meta.tsv(.zst) · index_v0/*.npy + *.json\nprefs · recovery_store · backup_store"]
```

### The tiers at a glance

| Tier | Class | Model | Unit indexed | Backing files |
|---|---|---|---|---|
| **NAME** | `MetaIndex` | none (pure code) | every file path + size + mtime | `index_meta.tsv` (or `.zst`), `index_meta_<L>.tsv` per drive |
| **IMAGE** | `SemanticIndex` | CLIP ViT‑B‑32 (open_clip, `laion2b_s34b_b79k`) | whole image **+ 5 crop tiles** + pixel dhash | `index_v0/img_embs.npy`, `img_paths.json`, `tile_embs.npy`, `img_dhash.npy` |
| **DOC** | `DocIndex` | multilingual‑e5‑small (384‑d) | ~1200‑char text chunks (+ absorption shards) | `index_v0/doc_embs.npy`, `doc_meta.json`, `doc_embs_s*.npy` |
| **AUDIO** | `AudIndex` | CLAP (`laion/clap‑htsat‑unfused`, 512‑d) | ~10 s overlapping windows (best moment per file) | `index_v0/aud_embs.npy`, `aud_paths.json`, `aud_times.json` |
| **VIDEO** | `VidIndex` | CLIP (shared space) | sampled frames w/ timestamps + thumbs | `index_v0/vid_embs.npy`, `vid_meta.json` |

### Enrichment (folds into the tiers above, built offline/at idle)

All of these are **batch, background, optional** — never in the interactive search path — and all feed one of the existing tiers (mostly the DOC text index via e5):

- **OCR** (`enrich.py ocr`, RapidOCR) — text *inside* screenshots → `source:"ocr"`.
- **Speech** (`enrich.py speech`, faster‑whisper tiny int8) — what was *said* in audio **and video soundtracks** (direct PyAV decode) → `source:"speech"`, timestamped.
- **Captions** (`enrich.py caption`, Florence‑2 `<DETAILED_CAPTION>`) — scene descriptions for photos without text → `source:"caption"`.
- **Caption/OCR chunking** (`enrich._emit`) — index the whole text *plus* clause‑level chunks, so a minor element ("a flag" on a driving licence) isn't diluted in a long blob.
- **Faces** (`face.py`, insightface buffalo_l) — 512‑d face embeds → DBSCAN/greedy clusters → the People view.
- **Pixel dhash** (`phash.py`) — 8‑byte perceptual hash of *actual pixels*; identical/resized/recompressed/renamed → Hamming 0. Drives exact‑copy detection and visual‑duplicate reclaim.
- **Generation prompts** (`genmeta.py`) — stdlib PNG `tEXt/zTXt/iTXt` parser pulls ComfyUI/A1111 prompts → search AI renders by the prompt that made them (`source:"genmeta"`).
- **PDF‑OCR** (`pdfocr.py`) — OCR of scanned PDFs, landed through the absorption shard path.
- **Colors** (`colors.py`) — precomputed average RGB per image (`img_colors.json`) for the color graph lens.
- **Code symbols** (`codesym.py`) — function/class symbols → file:line, "where is the retry logic."
- **Extractors** (`content.py`) — pdf/docx/xlsx/pptx/rtf/md/code/logs/jsonl/ipynb/eml/epub/srt/vtt, zip member‑lists, browser bookmarks, archives, ebooks.

---

## 2. Why it's instant

The measured feel on this machine (1.85M files, 23k images embedded, 28.7k doc chunks): a **fresh full‑tier search returns in 72–88 ms (median 87)**; a **repeat/chip‑click serves from the 30 s response cache in 2–3 ms**. The Rust name tier answers in **9.6–16.7 ms over 1,849,709 rows**. Every fast path is the result of one idea repeated: **precompute once, then do arithmetic in RAM — never walk the disk on the hot path.**

### 2.1 Name search — a single RAM string, not a disk walk

`MetaIndex` loads the whole‑drive path list once and stores it as **one giant lowercased string** (`self.blob = "\n" + "\n".join(lowered_paths) + "\n"`) plus an `offsets` array marking line boundaries. A substring query becomes `self.blob.find(term, pos)` — a C‑level scan over contiguous memory — and the matching line index is recovered with a `bisect` on `offsets`. No `os.walk`, no per‑file `stat`, no directory traversal. Filters (category, size, mtime) are applied **during** the scan so a category can never "starve" behind a candidate cap.

- **RAM optimization (`_BlobLower`):** the naive version kept the lowercased paths *three times* (a `lower` list + the blob + the original `paths`). `_BlobLower` is a tiny accessor that derives `lower[i]` by slicing the blob on demand (`blob[1+offsets[i]:offsets[i+1]]`) — **losslessly dropping a ~382 MB copy** (server RAM 3526 → 2866 MB).
- **Benchmark vs Windows Search** (both warm): name `apple` 2,143 hits / 376 ms (5.5× and ~10× the coverage); all `.gif` 13,571 / 60 ms (**105×**). Windows can't do semantic search at all on a non‑NPU machine.

### 2.2 The instant file explorer — a prebuilt directory index

A background thread (`_build_dir_index`) walks the path list once and builds two maps: `dir_files` (parent → child file indices) and `dir_subs` (parent → child folders). `handle_ls` then serves any folder with **zero disk I/O**. Measured: `C:\` in 5.9 ms (Windows `Get-ChildItem` 163 ms), most folders 3–26 ms. The same pass precomputes the **Photos timeline** rows (`media_rows`), turning a per‑view O(1.66M) scan (~2,100 ms) into a presorted slice (~168 ms).

### 2.3 Semantic search — matrix multiply, not model inference per file

Each modality index is a `float32` matrix in RAM. A query is encoded **once** to a vector `q`, then scored against the whole index with a single `embs @ q` matmul (`SemanticIndex.query`, `DocIndex.query`, etc.) and an `argsort`. Scanning 4k–23k image vectors takes **0.4–1.6 ms**; the cost is the *encode*, not the *scan*. This is why the warm model host (below) matters so much.

- **Crop robustness:** `find_by_vector` merges the whole‑image index **and** the per‑image crop‑tile index, taking the max score per path — so a cropped query wins via the original's tile embedding (Union Jack center‑crop: MISS → rank #1 @0.833).
- **Pixel identity is separate:** `dhash` (perceptual pixel hash) answers "is this literally the same picture" regardless of semantics — used for exact‑copy and dedup, distinct from the CLIP embedding.

### 2.4 The instant law — cheap tiers inline, expensive models never block

The single most important performance rule, learned the hard way (a cross‑encoder reranker was silently adding **+2,650 ms to every search**):

> **Search = bi‑encoder + vector scan only.** Anything expensive — the cross‑encoder reranker, and any live Whisper/OCR/Florence inference — runs at **index time** (batch) or in the **deliberate agent** (`/api/ask`, budget 1200 ms), never on `/api/search`. Fixing this took `apple` from 3,900 ms → 246 ms, then parallel tiers took it to ~87 ms.

### 2.5 Parallelism + true wall‑time + cache

`/api/search` submits the name/doc/audio tiers to a shared `ThreadPoolExecutor` (`POOL`) while the CLIP/e5 encoders run, so end‑to‑end latency ≈ the **slowest** tier, not the sum. `search_ms` is reported as real **wall** time (it used to be the tier‑sum, which overstated cost under parallelism). A 30 s response cache serves repeat queries (chip clicks, back‑nav) in 2–3 ms and is skipped in incognito mode.

### 2.6 The latency guard (jitter budget)

Every `/api/*` request is timed in the `do_GET`/`do_POST` wrappers against a per‑endpoint budget (`ENDPOINT_BUDGETS`: search 200 ms, ls 150, photos 300, graph/expand/similar 500, ask 1200, …). A breach appends to a `JITTER` ring buffer and logs a "SLOW …" warning surfaced in the Debug view. This catches an entire regression class the moment it appears.

### 2.7 On disk: zstd index + int8 models

- **Index compression:** `compress_index()` writes `index_meta.tsv.zst` (level 3): **212 MB → 20 MB (10.8×)**, loaded transparently. Level‑3 is the sweet spot (L19 hits 14.9× but 166 s to compress). Honest note: this is plain zstd near the entropy limit — there is *no* magic lattice compressor that beats zip/xz losslessly.
- **Model quantization:** fp32 → int8 ONNX is ~4× smaller models (3.7 GB → ~0.9 GB) — the Rust/ONNX port ships this. Image embeddings are already int8‑capable (4× smaller, ~free recall). Note the **numpy fp16 trap**: numpy has no CPU SIMD fp16 GEMM, so fp16 scans are ~1,277 ms — never serve fp16 in numpy; keep f32 in Python, int8 in Rust.

---

## 3. The warm model‑host split (v48)

**Problem:** loading CLIP + e5 + CLAP costs 20–60 s, paid on *every* engine restart — brutal during development (≈20 restarts/day) and impossible on low‑RAM boxes that can't hold torch + five fp32 models.

**Solution — the "lattice virtual environment":** move the heavy, slow‑to‑load, rarely‑changing models into a **separate resident process** and let the fast‑changing HTTP engine attach to it over a socket. The models are the "virtual environment" the lightweight engine plugs into; many engines (per user, or LITE + semantic on a small box) can share one host.

```mermaid
flowchart LR
    subgraph HOST["model_host.py — 127.0.0.1:8767 (stays warm)"]
        CLIP["CLIP ViT-B-32\n(open_clip)"]
        E5["multilingual-e5-small"]
        CLAP["CLAP (lazy on first use)"]
        LK["per-model locks:\nclip_lock · e5_lock · clap_lock"]
    end

    subgraph E1["Engine A (:8765) — LS_MODEL_HOST=1"]
        direction TB
        SI["SemanticIndex / DocIndex / AudIndex\nno torch import, no local model"]
    end
    subgraph E2["Engine B (per-user / LITE box)"]
        SI2["indexes only"]
    end

    SI -->|"_host_post('/clip_text' | '/clip_image' | '/e5' | '/clap_text')\nraw float32 bytes, X-Shape header"| HOST
    SI2 --> HOST
```

- **Host API** (`127.0.0.1` only, `X-LS-Token` required on POST): `GET /health` → `{models, device}`; `POST /clip_text {texts}`; `POST /clip_image` (raw image bytes); `POST /e5 {texts, query}`; `POST /clap_text {texts}`. Responses are **raw `float32` bytes** with an `X-Shape` header — the engine reads them straight into a numpy array (`_host_post`), no JSON‑encoding of vectors.
- **Per‑model locks:** CLIP and e5 requests from the engine's *parallel* tiers must not queue behind each other — one global lock cost the doc tier 150 ms+ of waiting. So the host holds `clip_lock`, `e5_lock`, `clap_lock` independently.
- **Engine host‑mode (`LS_MODEL_HOST=1`):** on startup `host_probe()` pings `:8767/health`; if CLIP + e5 are warm, `SemanticIndex/DocIndex/AudIndex` **skip their model loads and skip importing torch entirely** (~6 s saved), routing all 10 encode call sites through `_host_post`. **Engine restart drops 60 s → 19 s.** The trade‑off (accepted by design): per‑query latency ticks up slightly (a socket hop + same‑model GPU serialization), well within the instant budget; the payoff is dev‑restart speed, per‑user engines sharing one host, and netbook‑viable semantic search. If the host is absent or `LS_MODEL_HOST≠1`, every tier loads its own model as before — the split is purely additive.

---

## 4. The Rust name‑tier sidecar

`latticesearch-rs` (`main.rs` + `arena.rs`) is the production shape of the name tier: a single ~15 MB self‑contained binary, netbook‑sized.

- **`arena.rs` — mmap byte‑arena.** `index_meta.tsv` is `mmap`'d (never copied to heap). The only real allocation is a `Vec<Row>` of `{off:u32, len:u32, size:u64, mtime:i64}` (~24 bytes/file). `path()` is a zero‑copy slice into the map. The OS pages in only the touched bytes: the 1.66M‑file index that costs **~1.27 GB in Python holds in ~150–250 MB** here (measured RSS 287 MB, 80 MB private) — the "compact, expand‑on‑access" idea done natively and losslessly.
- **SIMD rarest‑byte anchored scan.** On load, the arena samples case‑folded byte frequencies. Each query term anchors its scan on its **rarest** byte (not its first), and terms are AND‑ordered rarest‑anchor‑first so the cheapest rejection runs first. The scan uses `memchr2` (SIMD) to jump between anchor candidates in both cases, verifying each with an ASCII case‑insensitive compare. The row table is chunked across threads (up to 16), each stopping early at `limit` local hits. Result: **9.6–16.7 ms median over 1,849,709 rows**, multi‑term AND, no second lowercase copy of the index.
- **Name‑tier HTTP sidecar** (`main.rs serve`, `127.0.0.1:8766`, loopback‑only): `GET /find?q=<substr>&limit=60` → `{hits:[[path,size,mtime],…], total_scanned, ms}`; `GET /health` → `{rows}`.
- **Auto‑detection & fallback.** The engine's `rust_names()` probes `:8766/health`; when present (`state["rust_names"]`) it routes name search there with a 0.3 s timeout, re‑probing every 5 min. Any failure flips the flag and the Python name tier serves as always. Opt out with `LS_RUST_NAME=0`. End‑to‑end, wiring in the sidecar took search 215 ms → **92–123 ms**.
- `main.rs` also carries the **Ed25519 offline license** verify + VPS revocation gate (`license.rs`) — the production licensing path (embed pubkey, HWID ≥2/3 match, optional revocation check, fail‑open 14‑day offline grace; the VPS stores only `{id→revoked, updates_until}`, no files/telemetry).

---

## 5. Absorption & regeneration — MOF‑inspired storage

Two ideas borrowed from **metal‑organic frameworks** (absorb by night, release by day) keep the index cheap to grow and cheap to keep fresh.

### 5.1 Absorption (shards + condense)

Appending to a dense numpy index means rewriting the whole array — linear cost that grows with the corpus. Instead, once the base exceeds ~4 MB, `_append_docs` writes **constant‑cost shards** (`doc_embs_s*.npy` / `doc_meta_s*.json`). `DocIndex` loads base + all shards transparently at startup. When the machine is idle, **`condense.py`** "evaporates" the loose shards back into the dense base (round‑trip tested: base 10 + shards 5 → dense 15, shards deleted). Absorb loose data fast; crystallize into the dense form when idle. (PDF‑OCR and other late arrivals ride this same shard path into production.)

### 5.2 The regeneration cycle (nightly)

**`regen_cycle.py`** runs the whole maintenance chain — `condense` → docs/images/audio backfills → `genmeta` → `phash` → `colors` → `symbols` → resnap‑if‑stale — all bounded and resumable. The server scheduler fires it **03:00–05:00 when the machine is idle** (>30 min since the last search, ≤ once per 20 h). Low‑energy cycling: the index maintains itself while you sleep, and the whole index is ~0.06% of the drive's weight.

### 5.3 Live watcher + staleness guard

- **Live watcher** (`watcher`): polls the hot folders in `WATCH_DIRS` (Pictures/Desktop/Documents/Downloads/dropzone + `watch_extra.json` + `~/LatticeDrive`). New files join the **live registry** (`live_rows`, an in‑RAM list searched *before* the snapshot) instantly; images are queued for CLIP embedding and drained **only when idle** (>0.8 s since the last search — background forward passes must never steal CPU from a live search). Search and `filter_ext` merge live + snapshot with path dedup. The product replaces this poll with NTFS USN‑journal tailing for whole‑drive real‑time (the "high‑hertz" path).
- **Staleness guard:** on startup, if the name snapshot is >24 h old the engine kicks off a background rescan. (Dogfooding caught a 17 h‑stale snapshot serving 9 of 55 files and undercounting `du` — hence the guard.)
- **Index‑sync for Lattice Hands:** any file the engine writes/moves via `/api/ops` is `index_touch`'d into the live registry, so it is searchable **immediately** — proven: a uniquely‑named file is found by search the instant after it's written.

### 5.4 Single‑instance guard + watchdog

A port guard makes a second engine on `:8765` exit(3) rather than double‑bind. **`watchdog.py`** polls every 60 s and relaunches the engine (and the Rust sidecar) after 2 missed heartbeats, tailing crashes to `crash.log` (with `faulthandler` on the serve loop); it registers both at login (HKCU `Run`). The engine also loads `index_meta.tsv` clamped to min‑length against torn writes, so a half‑written index can never 500 a search.

---

## 6. Security & privacy model

Privacy is the product's entire positioning (the anti‑Recall pitch: index only what you point at, nothing leaves the machine). The engine is defensive by construction.

- **Loopback bind only.** The engine, the Rust sidecar, and the model host all bind `127.0.0.1` — never a routable interface. `/media` and `/file` stream user bytes and are localhost‑only.
- **Per‑request local API token.** A random `ls_token.txt` sits **beside the server** (auto‑minted `secrets.token_hex(16)` if absent). Every **mutating** endpoint requires it via the `X-LS-Token` header (or `?token=`). The served UI has the token injected and auto‑attaches it to every same‑origin `fetch`; CLI/MCP clients read the token file. This blocks the CSRF‑style attack where a random web page fires requests at `127.0.0.1` (the browser allows the request; the missing token makes it a 403). The mutating set (`MUTATING_PATHS`) includes `/api/fileop`, `/api/ops`, `/api/open`, `/api/reveal`, `/api/upload`, `/api/transfer`, `/api/sync`, `/api/backup`, `/api/drive_scan`, `/api/memory`, `/api/clipboard`, `/api/settings`, `/api/star`, `/api/prefs`, `/api/recent_add`, `/api/trash`, `/api/restore`, `/api/index_start`, `/api/resnap`.
- **Origin rejection.** Any `/api/*` request carrying a foreign `Origin` header is 403'd outright (`_origin_ok`): a page can be `http://127.0.0.1`/`localhost` or send no Origin at all (plain `<img>`/`<video>` tags, which same‑origin policy keeps unreadable anyway).
- **DOM‑XSS hardening.** All 101 UI call sites use a `%27`‑hardened encoder (`encodeURIComponent` does not escape apostrophes — a crafted filename could otherwise break out of an `onclick` string).
- **Incognito mode** (`/api/incognito`, 🕶 toggle). While ON: recents are not recorded (server *and* client), the activity log is silent, Lattice Memory `store` is **refused (403)**, clipboard capture pauses, the search cache is skipped, and jitter labels are redacted. Honest scope: no trace *in the lattice* — the OS itself still journals (USN/MFT/event logs/jump lists); it is not an anti‑forensics tool. Deletes are still recovery‑archived by design (safety over tracelessness).
- **Safe by default.** Deletes archive the bytes (content‑addressed sha256 blob) to `recovery_store` **then** send to the Recycle Bin — recoverable ~90 days. The recovery index is written atomically under a lock (it *is* the user's safety net). All index writes are atomic (`tmp` + `os.replace`).
- **Clipboard secret filter.** The clipboard watcher never stores AWS keys / `password=` / long base64 / PEM blocks.
- **Cloud‑placeholder guard.** ~30% of Dropbox files here are online‑only; a 1‑byte read would *download* them. `blindspots.is_placeholder_st` (zero extra syscalls) guards every background tier so the index never mass‑hydrates cloud files. Placeholders stay name‑searchable.

---

## 7. Cross‑platform

The Python engine is portable at its seams and the Rust production build is portable by construction.

- **Data‑driven path separator.** The dir‑index and search infer the separator **from the data, not the platform**: a Windows‑built index has `C:\…` rows (`sep = "\\"`), a Linux one has `/…` rows (`sep = "/"`). Case‑folding is uniform (which caught a real `Lib`/`lib` dedup bug on Linux). POSIX roots are handled explicitly.
- **Portable native actions.** `open_file_native` (`startfile`/`open`/`xdg-open`), `reveal_file_native` (`explorer /select` / `open -R` / dbus `FileManager1`), and `is_os_hidden` (Windows attribute *or* Unix dotfile) abstract the OS‑specific bits.
- **Headless LITE.** `--lite` skips all ML model loads (name search + instant explorer only). Combined with data‑driven separators this is the **VPS proof**: on `sge-vps`, LITE indexed **700,588 files in 16.6 s (42k files/s)**, search 10–22 ms, `ls` 0.057 ms, `du` over 648k files in 191 ms, **RSS 360 MB**, token 403s enforced, loopback‑only — `scan3.js` needed *zero* changes. This doubles as the cold‑cache, fresh‑host datapoint.
- **The Rust/Tauri port is inherently portable:** `memmap2` / `ed25519-dalek` / `std` are cross‑platform; Tauri targets Win/Mac/Linux; `readdir` scanning is portable; the fast‑path is MFT/USN on Windows with FSEvents/inotify equivalents on Mac/Linux, same HTTP API.

---

## 8. Substrate for AI agents

A side effect of "meaning search over 1.6M files in milliseconds" is that LatticeSearch is a **cheaper filesystem substrate for AI agents** than grep + dir‑walk + read cycles.

- **EYES** (`/api/search`, `/api/ask`, `/api/fs`, `lattice grep`): a benchmark of 3 lookups — naive recursive grep 54,465 tokens / 133 s **→ 1 lattice call 1,230 tokens / 11 s = 44× fewer tokens, 12× faster.**
- **HANDS** (`/api/ops`: read `--about`, write, edit, move, copy, delete — all atomic, index‑synced, recoverable): 13–20× faster than spawning a PowerShell per op. `lattice sh` batches a 6‑step workflow in **485 ms vs 15,042 ms** for six PowerShell spawns (≈22.6–31×); the `du` step alone is where it wins biggest — the index answers a subtree size in tens of ms (a ~1.06M‑file subtree in ~850 ms) while a PowerShell recursive walk dominated its baseline (on the order of ~430× for `du`).
- Surfaced to agents over **MCP** (`mcp_server.py`, ~20 tools) and a **CLI** (`lattice.py`); `/api/aiaccess` emits copy‑paste‑correct configs. See [`API.md`](./API.md).

---

## 9. Component / file map

| Component | File(s) | Role |
|---|---|---|
| Engine | `server.py` | HTTP server, index classes, routing, security |
| Warm model host | `model_host.py` | resident CLIP/e5/CLAP over a socket (`:8767`) |
| Rust production core | `latticesearch-rs/src/{main,arena,license}.rs` | mmap name arena + SIMD scan + license (`:8766`) |
| Name snapshot | `scan3.js`, `drivescan.js`, `drives.py` | parallel drive walk → `index_meta.tsv`; multi‑drive |
| Content extraction | `content.py`, `enrich.py`, `genmeta.py`, `pdfocr.py`, `colors.py`, `codesym.py`, `face.py`, `phash.py`, `tiles.py`, `audio.py` | build the semantic/enrichment tiers |
| First‑run / maintenance | `index_all.py`, `backfill.py`, `condense.py`, `regen_cycle.py`, `watchdog.py` | tiered indexing, absorption, nightly regen, keep‑alive |
| Lattice Hands / fs / sync / backup | `transfer.py`, `latticesync.py`, `backup.py`, `memory_lattice.py`, `clipwatch.py` | file ops, sync, dedup backups, agent memory, clipboard |
| Compression | `compress_index()` (in `server.py`) | zstd index pack/unpack |
| Clients | `ui.html`, `palette.html`, `lattice.py`, `mcp_server.py` | browser UI, hotkey palette, CLI, MCP |

*Ports: engine `:8765` · Rust name sidecar `:8766` · warm model host `:8767`. Token file: `ls_token.txt` (beside `server.py`).*
