# TASK — memo (ТЗ для исполнителя) Исполнитель: OpenCode (`opencode run --auto`, модель `local/codding-big`). Репозиторий: `git.binom.pw/subochev/memo`. Рабочая копия: `~/WORK/memo`. Спека: `docs/SPEC.md`. Тест-план: `./TESTING.md`. ## 0. Жёсткие границы 1. **Стек фиксирован.** Kotlin/JVM, Gradle 8.14 (обёртка), JDK 21. Без Android-таргетов, без KMP-таргетов, без новых внешних зависимостей кроме перечисленных в §2. Захотелось библиотеку — стоп, спросить. 2. **Только JVM.** Никаких `commonMain`. Ядро должно остаться переносимым по смыслу, но проект — JVM. 3. **Ни одного сетевого вызова в рантайме поиска.** Модель — локальные файлы, эмбеддинг — ONNX на CPU. 4. **Тесты пишет исполнитель, но приёмка — не его.** Заявление «тесты зелёные» не принимается: приёмка идёт мутацией (см. `TESTING.md`). 5. **Не трогать чужие репозитории** (`text-embedding-kmp`, `ksqlite`, `mcp`). Только потребительская зависимость. 6. Не коммитить `models/`, `*.db`, `build/`, `~/notes`. ## 1. Структура ``` memo/ ├── settings.gradle.kts # rootProject.name = "memo" ├── build.gradle.kts # общий ext: kotlinVersion, onnxVersion ├── gradle/wrapper/ # Gradle 8.14 (взять из ~/.gradle/wrapper/dists/gradle-8.14-bin) ├── memo-cli/ # main: CLI (index|search|status) ├── memo-core/ # ядро: чанкер, схема БД, индексация, поиск ├── memo-watch/ # демон: WatchService + debounce + reconcile ├── memo-mcp/ # MCP-сервер (stdio, JSON-RPC 2.0) ├── docs/SPEC.md ├── TASK.md TESTING.md README.md LICENSE ``` `memo-core` НЕ зависит от `memo-watch`, `memo-mcp`, `memo-cli`. Зависимость только в одну сторону. ## 2. Зависимости (проверены 02.10.2026) ```kotlin // repositories: mavenCentral(), maven("http://nexus.xx/repository/caffeine/"){ isAllowInsecureProtocol = true } implementation("pw.binom.db:ksqlite:0.1.4") // Maven Central; SQLite 3.53.4 + sqlite-vec 0.1.9 + FTS5 + JSON1 implementation("pw.binom.ai.embeddingtext:api:5") // caffeine runtimeOnly("pw.binom.ai.embeddingtext:siglip-jvm:5") // caffeine; тянет com.microsoft.onnxruntime:onnxruntime:1.16.0 implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") // для MCP ``` Kotlin `2.4.10`, JVM target **17** (не 21 — совместимость с артефактами автора). ## 3. Контракт эмбеддера (не изобретать — это готовый API) ```kotlin import pw.binom.voice.embeddingtext.TextEmbedding import pw.binom.voice.embeddingtext.TextEmbeddingExtractor import pw.binom.voice.embeddingtext.createSiglip2TextExtractor val ex: TextEmbeddingExtractor = createSiglip2TextExtractor( modelPath = "/path/text_model_int8.onnx", // 283 MB tokenizerPath = "/path/tokenizer.model", // 4.2 MB ) val e: TextEmbedding = ex.embed("текст") // e.dim == 768, e.values: FloatArray val cos: Float = e.cosineSimilarity(other) ex.close() ``` - Размерность — **768**, константа в коде (`EMBEDDING_DIM = 768`), не вычисляется из вектора. - Движок **дорогой в создании**: один экземпляр на процесс, создаётся лениво, `close()` на выходе. - Модель в репозиторий не кладётся; путь — из конфига/аргумента (`models/siglip2/`). ## 4. Заказ №1 (первый шаг, остальное — после приёмки) **Цель:** каркас сборки + доказательство, что ksqlite и эмбеддер работают на этой машине. 1. Gradle-проект по §1, wrapper 8.14 из локального дистрибутива (`distributionUrl` можно оставить `https://services.gradle.org/distributions/gradle-8.14-bin.zip` — dist уже в кэше). 2. `memo-core`: `Embedder.kt` — обёртка над `TextEmbeddingExtractor` (§3), ленивая инициализация, один инстанс, `embed(text): FloatArray` размерности 768. 3. `memo-core`: `Db.kt` — открытие `index.db` через `SQLiteConnection.open(...)`, включение `PRAGMA journal_mode=WAL`, `PRAGMA synchronous=NORMAL`, создание схемы из §5 спеки (chunks, chunks_fts, chunks_vec float[768], files). 4. Тесты **jvmTest** (`memo-core/src/test/kotlin/...`) ровно четыре: - `ksqliteSmoke`: открыть временную БД, создать таблицу, вставить, прочитать — значение совпало. - `vec0KnnRoundTrip`: `CREATE VIRTUAL TABLE v USING vec0(embedding float[4])`, вставить 3 вектора, `WHERE embedding MATCH ? ORDER BY distance LIMIT 1` вернул ожидаемый `rowid`. - `fts5FindsRussianWord`: FTS5-таблица, вставка «внутренний домен траефик», поиск `MATCH 'траефик'` находит строку (проверка unicode61 на кириллице). - `embedderProduces768`: реальный эмбеддинг «привет мир» → `dim == 768`, без NaN; если файлов модели нет — тест **падает с внятным сообщением** (не пропускается молча). 5. `TESTING.md` не менять (он приёмочный, написан заказчиком). **Критерий готовности заказа:** `./gradlew :memo-core:test` — все 4 теста зелёные; в отчёте `memo-core/build/test-results/test/*.xml` видно 4 `testcase`. Коммит с осмысленным сообщением. **Запрещено в этом заказе:** писать чанкер, поиск, watcher, MCP, CLI. Только каркас и 4 теста. ## 5. Схема индекса (одна база на коллекцию) ```sql CREATE TABLE files(path TEXT PRIMARY KEY, mtime REAL, size INTEGER, hash TEXT, indexed_at REAL); CREATE TABLE chunks( id INTEGER PRIMARY KEY, path TEXT NOT NULL, heading TEXT, line INTEGER NOT NULL, ord INTEGER NOT NULL, text TEXT NOT NULL, hash TEXT NOT NULL); CREATE INDEX idx_chunks_path ON chunks(path); CREATE VIRTUAL TABLE chunks_fts USING fts5(text, heading, tokenize='unicode61'); CREATE VIRTUAL TABLE chunks_vec USING vec0(embedding float[768]); ``` - `chunks.id` = `rowid` для `chunks_vec` (вставлять `INSERT INTO chunks_vec(rowid, embedding) VALUES (?,?)`). - `chunks_fts.rowid` = `chunks.id` (та же нумерация). - Истина — файлы на диске. База удаляется `rm -rf <коллекция>/.memo` и пересобирается. ## 6. Смежные заказы (после заказа №1, по одному) - **№2 Чанкер+индексация:** разбор markdown по заголовкам `#`..`###`, чанк ≤ 800 токенов (оценка: 4 символа ≈ 1 токен) с хвостовым перекрытием 15%; `Indexer.indexFile(path)` — хэш + mtime, пропуск неизменённых; удаление чанков исчезнувших файлов. - **№3 Поиск:** `Searcher.search(root, query, k)`: FTS5 BM25 (`bm25(chunks_fts)`) + KNN по `chunks_vec` + RRF (`k=60`), возврат `path`, `line`, `heading`, `score`, `text` (обрезка 1200 символов). Плюс `stat`-догон: если файл изменился с момента индексации — переиндексировать перед поиском. - **№4 CLI:** `memo index `, `memo search "" [--k N] [--json]`, `memo status `. `--json` обязателен — на нём стоит приёмка. - **№5 Watcher:** `WatchService` (RECURSIVE), debounce 500 мс, очередь, reconcile полным сканом раз в 10–15 мин, устойчивость к удалению файла. - **№6 MCP-сервер:** stdio, JSON-RPC 2.0, `initialize` / `tools/list` / `tools/call`; инструменты `memo_search`, `memo_status`, `memo_reindex`. Свой минимальный, без `subochev/mcp`. ## 7. Что считается провалом заказа - Агент вывел план текстом и завершился без изменений в файлах (`git status` пуст). - Тесты зелёные, но при мутации боевого кода (см. `TESTING.md`) не падают. - Изменения за пределами оговорённых файлов. - Добавлены зависимости, которых нет в §2.