Проект: /root/WORK/memo (Kotlin/JVM). Заказ по индексации markdown-файлов в базу. Создать РОВНО ОДИН новый файл: memo-core/src/main/kotlin/memo/core/Indexer.kt Дополнить существующий: memo-core/src/test/kotlin/memo/core/CoreSmokeTest.kt (добавить ОДИН новый тест-метод в конец класса, существующие не трогать) ## Indexer.kt package memo.core class Indexer(private val db: Db, private val embedder: Embedder) { /** Индексирует один файл. Возвращает true, если файл был (пере)проиндексирован, * false — если пропущен (не изменился с прошлой индексации). */ fun indexFile(path: java.io.File): Boolean /** Индексирует все .md файлы в дереве каталога root (рекурсивно). Возвращает число * реально переиндексированных файлов. */ fun indexTree(root: java.io.File): Int } Правила: 1. Перед индексацией файла сравнить с записью в таблице files по колонке path: - если записи нет — индексировать; - если mtime и size совпадают с сохранёнными — вернуть false, ничего не делать (НЕ считать хэш); - если mtime/size изменились — посчитать sha-256 содержимого; если хэш совпал с сохранённым — обновить в files поля mtime/size/indexed_at (файл тронули, содержимое то же) и вернуть false; - иначе — полная переиндексация файла. 2. Полная переиндексация файла: удалить из chunks все строки с этим path; удалить из chunks_fts строки с rowid из удалённых chunks (DELETE FROM chunks_fts WHERE rowid IN (...)); то же для chunks_vec. Затем разбить текст через chunkMarkdown (см. memo-core/src/main/kotlin/memo/core/Chunker.kt), для каждого чанка: посчитать эмбеддинг embedder.embed(chunk.text), вставить строку в chunks, взять last_insert_rowid() и вставить те же rowid в chunks_fts (колонки text, heading) и в chunks_vec (колонка embedding). Всё — в одной транзакции (conn.beginTransaction/commit/rollback). 3. В files писать: path (абсолютный), mtime (доля секунды, REAL), size, hash (hex sha-256), indexed_at (epoch секунды). 4. indexTree: обход рекурсивно, только файлы с расширением .md, скрытые каталоги (начинающиеся с '.') и каталоги .memo не обходить. Возвращать число переиндексированных файлов. ## Тест: добавить в CoreSmokeTest.kt метод fun indexerSkipsUnchangedFile() - создать временный каталог, в нём файл note.md с содержимым "# Заголовок\nтело заметки\n## Второй\nещё текст"; - создать Db на временном index.db (не забыть db.init()) и Embedder на модели (путь брать так же, как в существующем тесте embedderProduces768 — через MEMO_MODEL_DIR); - первый indexFile(...) должен вернуть true; - второй indexFile(...) сразу же — обязан вернуть false (файл не менялся); - дописать в файл "\n## Третий\nновый текст\n", второй indexFile обязан вернуть true; - в конце: assertTrue, что SELECT count(*) FROM chunks без условия больше нуля (запрос через db.conn). После: ./gradlew :memo-core:test --rerun-tasks — все тесты зелёные. Коммит: git add -A && git commit -m "core: индексатор файлов с пропуском неизменённых" СТРОГИЕ ЗАПРЕТЫ: - Не выводить план текстом; сразу создавай/правь файлы. - Не трогать Chunker.kt, Embedder.kt, Db.kt, build.gradle.kts и любые другие файлы. - Не менять существующие тесты. - Не добавлять зависимости.