| name | update-index |
| description | Обновление и верификация индекса документов судебного дела. Используй, когда юрист говорит «обнови индекс», «проверь целостность дела», «синхронизируй файлы с индексом», «я вручную добавил/удалил/переместил файлы», «проверь, всё ли на месте», «актуализируй реестр», «какие файлы не в индексе». Также используй автоматически после ручных операций с файлами, когда состояние файловой системы могло разойтись с index.yaml. НЕ используй для приёма новых материалов (это intake/add-evidence/add-opponent) или генерации таблицы (это catalog).
|
Update-Index — Обновление и верификация индекса
Скилл работает по контракту preview → confirm → apply → verify. Оркестрацию ведёт main-Sonnet: он сам читает .vassal/index.yaml, сканирует файловую систему, создаёт/удаляет md-зеркала, обновляет .vassal/history.md и архивирует лог прогона. При большом числе документов (ориентир: >30 затронутых записей) main-Sonnet может опционально делегировать Haiku-subagent только механические grep/YAML-сводки по контракту 3.3A из shared/subagent-dispatch.md, но все записи в файлы дела делает только main-Sonnet.
Предусловия
- Существуют
.vassal/index.yaml и .vassal/case.yaml.
- Установлены зависимости для
extract_text.py. Если [PLUGIN_ROOT]/scripts/setup.sh на этой машине ещё не запускался — запусти его один раз.
- Для опционального массового помощника доступен
Task(model=haiku), но отсутствие Haiku-subagent не блокирует update-index: main-Sonnet может довести скилл до конца самостоятельно.
Переменные сессии
Вычисли один раз в начале прогона:
run_timestamp = текущее локальное ГГГГ-ММ-ДД-ЧЧмм
next_id_hint = текущее next_id из .vassal/index.yaml (если поля нет — считай 1)
Фаза 1 — Preview (main-Sonnet)
Вызови скрипт для получения состояния:
python3 "$PLUGIN_ROOT/scripts/scan_case_state.py" "$CASE_ROOT"
Распарси JSON: new_files (новые файлы), orphans (записи без файла), stale_mirrors (устаревшие зеркала), index_count, fs_count.
Покажи Сюзерену превью целиком: сколько документов в индексе, сколько файлов на диске, и три списка по режимам A/B/C.
ЗАПРЕЩЕНО на preview-фазе: любые записи на диск, mkdir, cp, rm, запуск extract_text.py.
Фаза 2 — Confirm
- Дождись явного решения Сюзерена по каждому классу изменений:
- режим A: какие новые файлы добавить в индекс;
- режим B: какие
orphans удалить и у каких записей обновить путь;
- режим C: какие зеркала пересоздать.
- Если решение неполное или двусмысленное, пересобери preview и запроси уточнение.
- Если Сюзерен отменил действие — остановись без изменений.
Фаза 3 — Apply (main-Sonnet)
Выполняй только явно подтверждённый diff-план. Любая неоднозначность = остановка без побочных изменений.
Режим A — Добавить новые файлы
- Для каждого подтверждённого нового файла возьми следующий
doc-ID строго из next_id_hint / next_id в .vassal/index.yaml.
- Для каждого файла используй точный цикл извлечения текста:
TMP=$(mktemp -d -t vassal-update-index-XXXXXX)
trap 'rm -rf "$TMP"' EXIT
OCR_JSON="$TMP/extract.json"
python3 "$PLUGIN_ROOT/scripts/extract_text.py" "$file" --output-dir "$TMP" > "$OCR_JSON"
Перед OCR обязательно приведи file к каноническому пути и проверь, что он резолвится внутри корня дела; если путь уводит чтение за пределы case root, останови apply для этого файла. Затем прочитай saved_to из JSON-вывода extract_text.py и используй именно этот путь к .txt. Если нужен ожидаемый <stem>, считай его строго как Path(filepath).stem в Python, то есть имя файла без последнего суффикса. Перед подготовкой зеркала обязательно проверь, что extract_text.py завершился с exit 0, saved_to присутствует, резолвится внутри текущего $TMP, оканчивается на .txt и указывает на обычный файл. Если любой из этих пунктов не выполнен, прерви apply для данного файла без записи зеркала и без добавления записи в индекс; молча считать тело пустым запрещено.
- Сначала собери candidate-зеркало в
$TMP по шаблону из shared/mirror-template.md: frontmatter должен отражать будущую запись индекса, а тело зеркала — полный извлечённый текст без усечения по страницам или символам. Не публикуй зеркало в .vassal/mirrors/ до успешного коммита индекса.
- Добавь запись в candidate-версию
.vassal/index.yaml для нового документа. Минимально зафиксируй:
id, title, date, type, source, file, mirror;
origin (name, date, batch: update-index-YYYY-MM-DD);
extraction_method, confidence;
mirror_stale: false.
- Применяй изменения атомарно: держи исходный
index.yaml и staged-зеркало в $TMP, валидируй candidate-YAML, затем атомарно замени .vassal/index.yaml; только после успешного коммита индекса атомарно опубликуй staged-зеркало в .vassal/mirrors/doc-NNN.md. Если запись индекса не закоммитилась, зеркало публиковать запрещено. Если публикация зеркала после коммита индекса всё же не удалась, немедленно откати .vassal/index.yaml к предшествующему состоянию.
- Cleanup временного каталога обязан происходить в любом исходе через
trap; отдельный rm -rf "$TMP" на happy-path не считается достаточным.
Режим B — Разрулить orphans
- Для каждой orphan-записи исполни только подтверждённое решение Сюзерена.
- Если решение — удалить запись, удали её из
.vassal/index.yaml и удали соответствующее зеркало.
- Если решение — обновить путь, измени только поле
file на подтверждённый новый путь.
- Ничего не удаляй и не перепривязывай по собственной инициативе.
Режим C — Пересоздать устаревшие зеркала
- Для каждого подтверждённого
doc-ID возьми путь к исходнику из поля file соответствующей записи .vassal/index.yaml.
- Для каждого такого файла используй тот же точный цикл полного re-OCR:
TMP=$(mktemp -d -t vassal-update-index-XXXXXX)
trap 'rm -rf "$TMP"' EXIT
OCR_JSON="$TMP/extract.json"
python3 "$PLUGIN_ROOT/scripts/extract_text.py" "$file" --output-dir "$TMP" > "$OCR_JSON"
Перед OCR обязательно приведи file из .vassal/index.yaml к каноническому пути и проверь, что он резолвится внутри корня дела; запись, уводящая чтение за пределы case root, считается ошибкой apply. Затем прочитай saved_to из JSON-вывода extract_text.py и используй именно этот путь к .txt. Если нужен ожидаемый <stem>, считай его строго как Path(filepath).stem в Python, то есть имя файла без последнего суффикса. Перед подготовкой нового тела зеркала обязательно проверь, что extract_text.py завершился с exit 0, saved_to присутствует, резолвится внутри текущего $TMP, оканчивается на .txt и указывает на обычный файл. Если любой из этих пунктов не выполнен, не перезаписывай существующее зеркало, не выставляй запись как свежую и останови apply с ошибкой для этого doc-ID.
- Сначала собери candidate-зеркало и candidate-версию записи индекса в
$TMP; до успешного коммита индекса не перезаписывай существующее зеркало.
- Обнови в candidate-записи
.vassal/index.yaml минимум следующие поля:
extraction_method и confidence по текущему результату извлечения;
last_verified = текущая дата;
mirror_stale: false.
- Применяй изменения атомарно: staged-копию
index.yaml провалидируй и атомарно замени только при успешной записи; staged-зеркало публикуй атомарной заменой только после успешного коммита индекса. Если коммит индекса не удался, старое зеркало должно остаться нетронутым. Если замена зеркала после коммита индекса не удалась, сразу откати index.yaml и сохрани прежнее зеркало.
- Cleanup временного каталога обязан происходить в любом исходе через
trap; отдельный rm -rf "$TMP" на happy-path не считается достаточным.
Общая дисциплина apply
- После завершения apply запиши краткую строку в
.vassal/history.md.
- После любого непустого apply-прогона проверь, что
.vassal/index.yaml читается:
python3 -c "import pathlib, yaml; yaml.safe_load(pathlib.Path('.vassal/index.yaml').read_text(encoding='utf-8'))"
- Не создавай временные OCR-артефакты вне
mktemp-каталога и не оставляй их после завершения шага.
Фаза 4 — Verify (main-Sonnet)
- Снова прочитай
.vassal/index.yaml и проверь, что все затронутые surviving-записи читаются и указывают на существующие file/mirror.
- Для всех затронутых документов выставь
ocr_quality/ocr_quality_reason через единый скрипт:
- вызови
python3 "$PLUGIN_ROOT/scripts/classify_ocr_quality.py" --extraction-method <extraction_method> --confidence <confidence> --total-chars <total_chars> --pages <pages> и подставь результат в поля. Если total_chars или pages неизвестны или отсутствуют, передавай значение "" или "null" — скрипт обработает их как None и не упадёт.
- Для
update-index, если отдельный /vassal-litigator-cc:reocr не запускался, фиксируй ocr_reattempted: false.
- Для записей с
ocr_quality: ok всегда ставь ocr_quality_reason: "". Если качество low или empty, не скрывай это в резюме: перечисли doc-ID и причину ручной проверки.
- Проверь, что:
- новые зеркала созданы, а пересозданные зеркала обновлены;
- для режима C
mirror_stale: false;
- удалённые orphan-записи действительно исчезли из индекса.
- Покажи Сюзерену итог: сколько файлов добавлено (режим A), сколько orphan-решений применено (режим B), сколько зеркал пересоздано (режим C), сколько записей получили
ocr_quality low/empty.
Идемпотентность
Повторный запуск безопасен: если запись уже синхронизирована и зеркало не устарело, повторно создавать её не нужно.
Граничные случаи
- Файл переименован вручную: старая запись становится
orphan, новое имя — новым файлом. Предложи Сюзерену либо обновить путь в режиме B, либо подтвердить две отдельные операции.
- Если после
extract_text.py отсутствует saved_to, saved_to резолвится вне текущего $TMP, не оканчивается на .txt или файл по этому пути не является обычным файлом, считай это ошибкой apply, а не допустимым пустым зеркалом: в режиме A не добавляй запись, в режиме C сохраняй старое зеркало и не помечай запись как свежую; обязательно сообщи агенту/пользователю причину сбоя.
- Если путь
file из preview-плана или из .vassal/index.yaml после каноникализации уводит чтение за пределы case root, считай это ошибкой apply и не запускай extract_text.py для такой записи.
- Если в индексе есть legacy-записи
v0.5.x без ocr_quality, а они попали в режим B/C, после verify заполни им ocr_quality по презумпции из shared/conventions.md.