# Классификация застряла на 68/84: диагностика 2026-08-27 ## Наблюдение На экране классификация остаётся на `68/84`, хотя контейнер продолжает работать. Проверка через ВМ: ```text GET /api/batch-progress?batch=acbdffe5-e3ec-43f4-ab61-84861cac35bd {"counts":{"classified":68,"garbage":16},"ok":true,"total":84} ``` Повторный запрос вернул то же значение. Kubernetes показывает pod `pythonk8s-574b5cf9b4-z4wxz` в состоянии `Running`, `Ready 1/1`, `RESTARTS 0`. Событий Kubernetes нет. На ВМ `contracts.service` активен, `convert_server.py` запущен в одном экземпляре. ## Причина в коде `site/routes/api_bp.py` возвращает раздельные счётчики `classified`, `failed` и `garbage`, а `total` равен числу всех документов батча. `site/static/app.js` в `runClassify()` считает завершённые документы так: ```javascript var done = (d.counts.classified || 0) + (d.counts.failed || 0); ``` `garbage` в эту сумму не включён. Для текущего батча: ```text done = 68 classified + 0 failed = 68 total = 84 garbage = 16 фактически завершено = 68 + 0 + 16 = 84 ``` Поэтому условие `done >= total` никогда не выполняется, polling не останавливается, а кнопка остаётся на `68/84`. Это ошибка фронтендового подсчёта прогресса, а не зависание Kubernetes или LLM на 68-м файле. ## Исправление В `site/static/app.js` к числу завершённых документов добавлен конечный статус `garbage`: ```javascript var done = (d.counts.classified || 0) + (d.counts.failed || 0) + (d.counts.garbage || 0); ``` Версия приложения повышена с `2.0.17` до `2.0.18`; cache-buster статических скриптов обновлён в `site/templates/index.html`. Kubernetes проверялся только командами чтения через ВМ: `kubectl get`, `kubectl describe`, `kubectl top`, `kubectl logs`. ## Дополнительная проверка загрузки ZIP При проверке загрузки разными способами обнаружен отдельный дефект связи файлов с архивом. `expandZipClient()` создаёт для каждого распакованного файла поле `zip_source`, а `uploadFile(file, onProgress, zipSource)` умеет передавать его в `/api/upload_refs`. Однако вызов `uploadFile()` в цикле `onFilesSelected()` передавал только два аргумента. Поэтому backend получал `zip_source=null`, и связь с исходным ZIP терялась. Проверены архивы: - `testgen/out/zips/duplicates_inside.zip` — целый ZIP, два файла с одинаковым именем; - `testgen/out/zips/mixed_with_error.zip` — целый ZIP, корректный и повреждённый DOCX. Исправление применено в `site/static/files.js`: третьим аргументом передаётся `entry.zip_source`. Для обычных файлов значение `null`, поэтому их поведение не изменяется. Версия приложения повышена с `2.0.18` до `2.0.19`, cache-buster обновлён в `site/templates/index.html`. ## Проверки исправления - `node --check site/static/files.js` — успешно; - frontend upload-тесты — успешно; - layer 2 upload-тесты — успешно; - архивы `duplicates_inside.zip` и `mixed_with_error.zip` прошли `unzip -t`. ## Реальный end-to-end тест сверки 27.08.2026 выполнен тест через deployed API `v2.0.19` на batch `0dd0d3a7-3e52-4888-95eb-1b6d3e37f40e`. Загружены через VM-поток два реальных DOCX: - `договор-XXX001-03700.docx` — распарсен, 20 элементов; - `допник-1-XXX001-03700.docx` — распарсен, 16 элементов. Классификация завершилась `2/2`, но результат классификации неожиданен: - допсоглашение попало в группу `03700`; - базовый договор попал в `__unresolved__` со статусом `garbage` и причиной отсутствия matching-договора. Это функциональный дефект/проблема классификации тестовой пары: без базового договора группа не проверяет корректное сравнение договора с приложением. Тем не менее реальный pipeline сверки для сформированной группы проверен: - `/api/apply-groups` — `200`, создан contract ID `b859e813-bac9-4eb9-82b5-6b5666f2ba4a`; - `/process-v2` — SSE завершён событием `complete`; - LLM вернул `6` операций в режиме `partial`; - применено: `added=6`, `updated=0`, `deleted=0`; - ошибок соединения и SSE не было; - время pipeline: `9.4с`. Следующий обязательный тест для проверки matching — загрузить базовый договор с точным именем/содержимым, которое классификатор ожидает как базовый, и повторить ту же пару. Код после этого прогона не изменялся. ## Исправление ложного garbage для базового договора При повторной проверке реального файла `договор-XXX001-03700.docx` установлена точная причина ошибочной классификации: второй garbage-фильтр искал подстроку `УПД` в первых 2000 символах. В договоре эта аббревиатура встречается в обычном условии оплаты: «на основании УПД и счета». Из-за этого договор получал `garbage=header_keywords`, хотя его заголовок начинается со слова «Договор». Исправление в `site/services/classify.py`: - удалено общее substring-срабатывание для `УПД`; - добавлено распознавание `УПД` и полного названия только с начала строки, когда это заголовок самого документа. Добавлены регрессионные тесты: упоминание УПД внутри договора не является garbage, а заголовок документа `УПД № ...` является garbage. Версия приложения повышена до `2.0.20`, cache-buster обновлён в `site/templates/index.html`.