9.3 KiB
9.3 KiB
TASK — memo (ТЗ для исполнителя)
Исполнитель: OpenCode (opencode run --auto, модель local/codding-big).
Репозиторий: git.binom.pw/subochev/memo. Рабочая копия: ~/WORK/memo.
Спека: docs/SPEC.md. Тест-план: ./TESTING.md.
0. Жёсткие границы
- Стек фиксирован. Kotlin/JVM, Gradle 8.14 (обёртка), JDK 21. Без Android-таргетов, без KMP-таргетов, без новых внешних зависимостей кроме перечисленных в §2. Захотелось библиотеку — стоп, спросить.
- Только JVM. Никаких
commonMain. Ядро должно остаться переносимым по смыслу, но проект — JVM. - Ни одного сетевого вызова в рантайме поиска. Модель — локальные файлы, эмбеддинг — ONNX на CPU.
- Тесты пишет исполнитель, но приёмка — не его. Заявление «тесты зелёные» не принимается: приёмка
идёт мутацией (см.
TESTING.md). - Не трогать чужие репозитории (
text-embedding-kmp,ksqlite,mcp). Только потребительская зависимость. - Не коммитить
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)
// 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)
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 и эмбеддер работают на этой машине.
- Gradle-проект по §1, wrapper 8.14 из локального дистрибутива (
distributionUrlможно оставитьhttps://services.gradle.org/distributions/gradle-8.14-bin.zip— dist уже в кэше). memo-core:Embedder.kt— обёртка надTextEmbeddingExtractor(§3), ленивая инициализация, один инстанс,embed(text): FloatArrayразмерности 768.memo-core:Db.kt— открытиеindex.dbчерезSQLiteConnection.open(...), включениеPRAGMA journal_mode=WAL,PRAGMA synchronous=NORMAL, создание схемы из §5 спеки (chunks, chunks_fts, chunks_vec float[768], files).- Тесты 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; если файлов модели нет — тест падает с внятным сообщением (не пропускается молча).
TESTING.mdне менять (он приёмочный, написан заказчиком).
Критерий готовности заказа: ./gradlew :memo-core:test — все 4 теста зелёные; в отчёте
memo-core/build/test-results/test/*.xml видно 4 testcase. Коммит с осмысленным сообщением.
Запрещено в этом заказе: писать чанкер, поиск, watcher, MCP, CLI. Только каркас и 4 теста.
5. Схема индекса (одна база на коллекцию)
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 <path>,memo search <path> "<query>" [--k N] [--json],memo status <path>.--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.