94 lines
7.1 KiB
Markdown
94 lines
7.1 KiB
Markdown
Проект: /root/WORK/memo (Kotlin/JVM). Заказ на исправление ТРЁХ доказанных дефектов демона `memo-watch`.
|
||
|
||
## Доказанные дефекты (воспроизведено на живом корпусе)
|
||
|
||
Корпус `/root/WORK/memo-e2e-watch2` — копия эталонного (`infra`, `life/books`, `work/jira`, 12 .md).
|
||
Все `.memo` предварительно удалены. Запуск:
|
||
MEMO_MODEL_DIR=/root/WORK/memo/models/siglip2 \
|
||
memo-watch/build/install/memo-watch/bin/memo-watch /root/WORK/memo-e2e-watch2
|
||
|
||
1. **Создаёт 6 баз вместо 3.** Вывод `наблюдаю:` перечисляет корень, `infra`, `life`, `work`,
|
||
`life/books`, `work/jira` и всем заводит `.memo/index.db`. Причина — своя локальная копия
|
||
`discoverCollections()` в `WatchMain.kt` (walkTopDown + depth 2 «есть .md где-то внутри»);
|
||
в `memo-core` эту же ошибку уже вылечили (`findCollections` = только каталог с *.md НАПРЯМУЮ),
|
||
но watcher остался на старой копии.
|
||
2. **Не индексирует при старте.** Сразу после запуска (без правок файлов) в базах 0 файлов и 0 чанков;
|
||
наполняется только та коллекция, в которой потом что-то изменилось. Проверка:
|
||
правка одного файла в `infra` → в `infra/.memo` появились файлов 4, чанков 12, а `life/books`
|
||
и `work/jira` остались с нулями. Никакого первичного прохода при старте нет.
|
||
3. **Держит все базы открытыми постоянно.** `WatchMain` открывает `Db` и `Embedder` на каждую
|
||
коллекцию и не закрывает до SIGINT; тот же `.db` в это время открывают CLI и MCP.
|
||
Это надо проверить на отсутствие «database is locked» (одновременные watcher + поиск).
|
||
|
||
## Что сделать
|
||
|
||
Правки ТОЛЬКО в модуле `memo-watch` (`src/main` и `src/test`). `memo-core` не менять.
|
||
|
||
1. **Убрать копию логики.** В `WatchMain.kt` удалить локальную `discoverCollections` и функцию
|
||
определения корня по `.memo`; использовать из ядра:
|
||
`memo.core.findCollections(base)` и `memo.core.resolveCollection(raw)`.
|
||
Поведение: передан один каталог-коллекция → он один; передан корень → все коллекции внутри.
|
||
2. **Первичный проход при старте.** После `watcher.start()` (или до него) выполнить для каждой
|
||
коллекции `indexer.indexTree(coll)` один раз, чтобы индекс был полным сразу, без ожидания событий.
|
||
Вывести в stdout по строке: `индексирую: <путь> -> обновлено <N>`. Затем уже строки `наблюдаю:`.
|
||
3. **Устойчивость к параллельному доступу.** Включить в `Db` уже есть WAL — не менять. Вместо этого
|
||
добавить в `WatchMain` обработку ошибок записи так, чтобы `database is locked` не убивал демон:
|
||
при ошибке печатать в stderr и повторять попытку один раз через 1 секунду (достаточно локальной
|
||
обёртки вокруг `indexer.indexTree`). Не менять код ядра.
|
||
|
||
## Тесты: дополнить memo-watch/src/test/kotlin/memo/watch/WatcherTest.kt и добавить новый файл
|
||
|
||
Новый файл `memo-watch/src/test/kotlin/memo/watch/WatchMainLogicTest.kt`, ровно 2 теста:
|
||
1. `collectionsFoundByCoreRule` — дерево: `root/a.md`, `root/sub/b.md`, `root/nested/only/deep/d.md`,
|
||
`root/empty/` → `findCollections(root)` даёт ровно три каталога (root, root/sub, root/nested/only/deep),
|
||
и среди них НЕТ `root/nested` и `root/nested/only`.
|
||
2. `startupIndexPassFillsEveryCollection` — временный корень с двумя коллекциями (`root/c1/x.md`,
|
||
`root/c2/y.md`), у каждой создаётся `Db` + `.memo`, вызывается `Indexer.indexTree(coll)` по разу
|
||
(как это делает первичный проход), после чего в обеих базах
|
||
`SELECT COUNT(*) FROM chunks` > 0 и `SELECT COUNT(*) FROM files` == 1.
|
||
Модель брать из `MEMO_MODEL_DIR` (как в CoreSmokeTest).
|
||
|
||
## Обязательная сквозная проверка (приложить вывод)
|
||
|
||
```bash
|
||
cd /root/WORK/memo
|
||
./gradlew :memo-watch:installDist -q
|
||
rm -rf /tmp/mw && cp -r /root/WORK/memo-e2e /tmp/mw
|
||
find /tmp/mw -name .memo -type d -prune -exec rm -rf {} + 2>/dev/null
|
||
|
||
# 1) старт: 3 базы, все наполнены сразу
|
||
MEMO_MODEL_DIR=/root/WORK/memo/models/siglip2 \
|
||
memo-watch/build/install/memo-watch/bin/memo-watch /tmp/mw > /tmp/mw.log 2>&1 &
|
||
WPID=$!
|
||
sleep 40
|
||
echo "--- базы:"; find /tmp/mw -name index.db | sort
|
||
echo "--- чанки сразу после старта:"
|
||
python3 - <<'EOF'
|
||
import sqlite3, glob
|
||
for db in sorted(glob.glob("/tmp/mw/**/.memo/index.db", recursive=True)):
|
||
c = sqlite3.connect("file:"+db+"?mode=ro", uri=True)
|
||
print(db.replace("/tmp/mw",""), "файлов", c.execute("SELECT COUNT(*) FROM files").fetchone()[0],
|
||
"чанков", c.execute("SELECT COUNT(*) FROM chunks").fetchone()[0])
|
||
c.close()
|
||
EOF
|
||
|
||
# 2) параллельно с работающим демоном ищем тем же ядром — не должно быть locked
|
||
memo-cli/build/install/memo/bin/memo search /tmp/mw/life/books "кто ведёт рассказ в романе" --k 3 --json | head -c 400
|
||
echo
|
||
kill $WPID 2>/dev/null; wait $WPID 2>/dev/null
|
||
cat /tmp/mw.log
|
||
```
|
||
|
||
Ожидается: РОВНО три пути `index.db`; у каждой коллекции ненулевые файлы и чанки СРАЗУ после старта;
|
||
поиск при работающем демоне отвечает без ошибки `database is locked`; в логе — строки
|
||
`индексирую: ... -> обновлено N` и `наблюдаю: ...` (по три каждого вида).
|
||
|
||
После: ./gradlew test --rerun-tasks — все тесты проекта зелёные.
|
||
Коммит: git add -A && git commit -m "watch: первичный проход при старте, коллекции по правилу ядра"
|
||
|
||
СТРОГИЕ ЗАПРЕТЫ:
|
||
- Не выводить план текстом; сразу правь файлы.
|
||
- Не менять memo-core, memo-cli, memo-mcp.
|
||
- Не менять смысл существующих тестов WatcherTest.
|
||
- Не добавлять зависимости.
|