Проект: /root/WORK/memo (Kotlin/JVM). Заказ по слежению за файлами. Создать РОВНО ДВА новых файла: memo-watch/build.gradle.kts (плагин kotlin("jvm") + зависимость на :memo-core) memo-watch/src/main/kotlin/memo/watch/Watcher.kt memo-watch/src/main/kotlin/memo/watch/WatchMain.kt и ОДИН тестовый: memo-watch/src/test/kotlin/memo/watch/WatcherTest.kt ## Watcher.kt package memo.watch class Watcher( private val collections: List, // каталоги-коллекции private val index: (java.io.File) -> Unit, // функция индексации одной коллекции private val debounceMillis: Long = 500, private val reconcileMillis: Long = 10 * 60 * 1000, ) : AutoCloseable { /** Запускает фоновый поток: WatchService на каждый каталог рекурсивно, * debounce, плюс периодическая полная реконсиляция по mtime. */ fun start() /** Останавливает поток, закрывает WatchService. Идемпотентно. */ override fun close() /** Количество выполненных вызовов index(...) — для тестов. */ val indexCalls: Int } Требования: 1. Регистрировать каждый каталог через FileSystems.getDefault().newWatchService() с ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE и при регистрации рекурсивно обойти подкаталоги (WatchService не рекурсивен). Каталоги, начинающиеся с '.', не обходить и не регистрировать. 2. События собирать в Set (какие коллекции затронуты). Поток: take() с таймаутом debounceMillis; при накоплении — по истечении debounce вызвать index(коллекция) для каждой затронутой коллекции. Не вызывать index десятки раз на одно сохранение файла. 3. Раз в reconcileMillis — полная реконсиляция: обойти все файлы *.md во всех коллекциях, сравнить (path, mtime, size) со таблицей files, при расхождении вызвать index(коллекция). Реализацию сравнения НЕ дублировать: у :memo-core уже есть Indexer.indexFile, который сам пропускает неизменённые файлы и возвращает Boolean. Использовать его: index(коллекция) должен внутри вызывать Indexer.indexTree(коллекция). 4. Все исключения в рабочем потоке не должны убивать поток: ловить, печатать в stderr, продолжать. 5. close(): interrupt + закрыть WatchService, поток завершается не дольше 2 секунд. ## WatchMain.kt fun main(args: Array) - Аргумент: корень, содержащий коллекции (или одну коллекцию). - Определение коллекций — как в CLI (каталог с *.md на глубине до 2). - Создаёт Db(коллекция/.memo/index.db), .init(), Indexer(db, embedder) — по одному на коллекцию, Watcher с index = { коллекция -> indexer.indexTree(коллекция) }. - Печатает `наблюдаю: <коллекция>` по строке на каждую, затем блокируется навсегда до SIGINT. - Модель: MEMO_MODEL_DIR, иначе /root/WORK/memo/models/siglip2. ## WatcherTest.kt — ровно 3 теста, имена ровно такие 1. `modifyTriggersSingleIndexCall` — временный каталог с note.md; Watcher с index-счётчиком и debounce 200 мс; start(); правка файла; подождать 1.5 с; indexCalls == 1 (не больше!); close(). (Перед стартом дать watcher 300 мс прогреться.) 2. `unchangedFileDoesNotTriggerIndex` — после прогрева ничего не менять 1.5 с → indexCalls == 0. 3. `closeIsIdempotentAndStopsThread` — start(); close(); close(); повторный вызов не бросает исключение. Тесты должны быть устойчивыми: ждать не фиксированным sleep, а опросом условия с таймаутом (например, до 5 с с шагом 100 мс), чтобы не флапать на медленной машине. После: ./gradlew :memo-watch:test --rerun-tasks — зелёные; ./gradlew build -x test компилируется. Коммит: git add -A && git commit -m "watch: слежение за файлами (WatchService + debounce + реконсиляция)" СТРОГИЕ ЗАПРЕТЫ: - Не выводить план текстом; сразу создавай файлы. - Не трогать memo-core и memo-cli. - Не добавлять зависимости, кроме :memo-core.