Срез 0 [7.5]: финальная волна, документы — T5.1, T5.3, T6.1, T6.3, T6.6, M-3

- README: верхняя граница задержки settle, шаг 5 «Полного круга»; критерии ошибочного файла (корень не объект)
- CLAUDE.md: по одному json на каждый распознанный документ; критерии ошибочного файла
- design.md: D-5 — оговорка про нестроковый error.code отдельным предложением; №79 — источник указания, SQL уборки в порядке FK
- open-questions-proposal.md п.20: замены ../docs/dev/README.md (185–187, 224–226, 252–253) и комментарий docker-compose.yml:184
- план: Task 6 — пометка про замену down -v (R-27)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-04 16:01:13 +04:00
parent ed5622374e
commit b127c02422
5 changed files with 12 additions and 10 deletions

View File

@ -5,7 +5,7 @@
## Что это
Адаптер внешней ИС Сканирование (Content AI) для PDF/PNG/JPG: переименовать файл `UUID_имя`, положить в «Входящие»
(через `SCAN_TMP_DIR`, по умолчанию `Tmp`: `.part` пишется туда, на той же ФС/шаре, и переименовывается), забрать **все**
`Исходящие/UUID/*.json` (ИС кладёт по одному json на файл ответа), склеить и сохранить сырой ответ в S3
`Исходящие/UUID/*.json` (ИС кладёт по одному json на каждый распознанный документ), склеить и сохранить сырой ответ в S3
`raw/{job_id}.json` (в БД не регистрируется),
нормализовать в `../docs/contracts/recognition-result.schema.json` (`statementDocument`), опубликовать
`recognition.result`. Таймаут → ошибка `TIMEOUT` по `min(reply_deadline, старт + SCAN_TIMEOUT_SECONDS=300)`, старт —
@ -18,8 +18,8 @@
отсчёт от первого опроса, увидевшего окончательный набор). Не ждать «один файл `<UUID>.json`».
- Склеенный `raw/{job_id}.json`: `{"job_id", "files": [{"name", "response", "parse_error"}, ...]}`, все `*.json`
каталога по возрастанию имени; нормализатор получает ровно эту структуру.
- Успех файла ответа — `error.code == ""` (`error` при этом не `null`!), ошибка — непустой `error.code` или
`parse_error`. Проверка `error is None` для реального ответа неверна. Ошибочные файлы — в `metrics.warnings`
- Успех файла ответа — `error.code == ""` (`error` при этом не `null`!), ошибка — непустой `error.code`,
`parse_error` или корень ответа не объект (`INVALID_FORMAT`). Проверка `error is None` для реального ответа неверна. Ошибочные файлы — в `metrics.warnings`
(`<имя>: <код>: <сообщение>`); все файлы ошибочны → код первого, есть успешный → `NOT_IMPLEMENTED` (Срез 0).
- В одном json может быть несколько таблиц разных форм (баланс, ОФР); тип документа задаёт `label` таблицы. Таблицы не
интерпретируются в Срезе 0 — разбор в Срезе 3.

View File

@ -63,7 +63,7 @@ ack (с `attempt` повторного запроса); файл в ИС вто
- Готовность набора — по стабилизации (settle): в каталоге есть хотя бы один `*.json`, а имена и размеры `*.json` не
менялись `SCAN_SETTLE_SECONDS` (по умолчанию 2 с). Отсчёт идёт от первого опроса, который увидел окончательный
набор (а не от времени изменения файлов), поэтому ответ приходит не раньше чем через `SCAN_SETTLE_SECONDS` после
такого опроса; `0` — готов любой непустой набор. Появление или изменение файла сбрасывает отсчёт; набор, который так
такого опроса и не позднее `SCAN_SETTLE_SECONDS + SCAN_POLL_SECONDS` после первого опроса, увидевшего набор; `0` — готов любой непустой набор. Появление или изменение файла сбрасывает отсчёт; набор, который так
и не стабилизировался до дедлайна, даёт `TIMEOUT`.
- Таймаут ожидания: `min(reply_deadline, момент записи файла + SCAN_TIMEOUT_SECONDS)`; опрос каждые `SCAN_POLL_SECONDS`
(ожидание settle входит в этот срок).
@ -163,7 +163,8 @@ MinIO и NATS); те же значения подхватываются из `EN
| `NOT_IMPLEMENTED` | есть хотя бы один успешный файл (`error.code == ""`); нормализатор Среза 0 — заглушка. Ошибочные файлы набора — в `metrics.warnings` | да |
| `UNKNOWN_ERROR` | сырой ответ или результат не записан в S3; нормализатор упал; неизвестный код ИС (в том числе `error.code` не строка: число, список, `true`); непредвиденная ошибка | да |
Файл ответа считается ошибочным, если его `error.code` непустой либо файл не разобрался как JSON (`parse_error`).
Файл ответа считается ошибочным, если его `error.code` непустой, файл не разобрался как JSON (`parse_error`) либо корень
ответа не объект (`INVALID_FORMAT`).
`metrics.warnings` итогового конверта содержит по строке `<имя файла>: <код>: <сообщение>` на каждый ошибочный файл (и при
`NOT_IMPLEMENTED`, и когда ошибочны все файлы); если ошибочных файлов нет, поле `metrics` не передаётся.
@ -214,7 +215,8 @@ curl -s localhost:8000/healthcheck
4. Опубликовать в `recognition.image.request` тело по образцу `api/contracts/examples/request-image.json`
(свои `job_id` и `file_id` — UUID, `original_name`, `s3.key`, `reply_deadline` в будущем, `attempt` = 1) с заголовком
`Nats-Msg-Id: <job_id>:1`.
5. Ожидать: ack `processing`, через `SCAN_EMULATOR_DELAY` (+ `SCAN_SETTLE_SECONDS`) — конверт `error`:
5. Ожидать: ack `processing`, через `SCAN_EMULATOR_DELAY` (+ до `SCAN_SETTLE_SECONDS + SCAN_POLL_SECONDS` после первого опроса, увидевшего набор) —
конверт `error`:
- `aep_3` — `NOT_IMPLEMENTED` и `metrics.warnings` с одной строкой `<job_id>_ofr.json: INVALID_DOCUMENT: ...`
(второй файл набора — ответ-ошибка);
- `aep_6` — `NOT_IMPLEMENTED` без `metrics`;

View File

@ -53,4 +53,4 @@
### Task 6: [R6] Проверка в реальном compose
- [x] Без правок `../docs`: `docker compose -f ../docs/dev/docker-compose.yml --profile services up -d --build` нужных сервисов (`scan-gateway`, `scan-emulator` + зависимости; `--wait` по именам сервисов, не голый), затем сценарии через реальный compose: положить `aep_3.pdf`, `aep_6.pdf`, `aep_5.PDF` из `../fixtures/ocr` в MinIO и вставить задания способом из `../docs/dev/README.md` («Проверка окружения»); зафиксировать выводы (в т.ч. содержимое `raw/<job_id>.json`) в отчёт. В конце окружение остановить (`down -v`). Если `../docs/dev/docker-compose.yml` требует правки (например, переменных `SCAN_TMP_DIR`/`SCAN_SETTLE_SECONDS`) — не править, а записать точный фрагмент в `open-questions-proposal.md`.
- [x] Без правок `../docs`: `docker compose -f ../docs/dev/docker-compose.yml --profile services up -d --build` нужных сервисов (`scan-gateway`, `scan-emulator` + зависимости; `--wait` по именам сервисов, не голый), затем сценарии через реальный compose: положить `aep_3.pdf`, `aep_6.pdf`, `aep_5.PDF` из `../fixtures/ocr` в MinIO и вставить задания способом из `../docs/dev/README.md` («Проверка окружения»); зафиксировать выводы (в т.ч. содержимое `raw/<job_id>.json`) в отчёт. В конце окружение остановить (`down -v` — заменено R-27, design #77: `rm -sfv` собственных сервисов). Если `../docs/dev/docker-compose.yml` требует правки (например, переменных `SCAN_TMP_DIR`/`SCAN_SETTLE_SECONDS`) — не править, а записать точный фрагмент в `open-questions-proposal.md`.

View File

@ -56,7 +56,7 @@ NATS без JetStream: сообщение доставляется один ра
### D-5. Нормализатор — интерфейс; в Срезе 0 сводит ошибки по файлам и отвечает `NOT_IMPLEMENTED`
`Normalizer.normalize(raw: object) -> NormalizeOutcome(status, documents, error, metrics, warnings)` (`raw` — склеенный ответ,
разобранная структура D-4; JSON каждого файла разобран при склейке, не-объект на входе -> `INVALID_FORMAT`).
`NullNormalizer` относит файл к «ошибочным», если `parse_error` не `null` либо `response.error.code` — непустая строка (код не строка, не `null` и не `""` — число, список, `true` — тоже ошибка с кодом `UNKNOWN_ERROR`, R-32, design #71);
`NullNormalizer` относит файл к «ошибочным», если `parse_error` не `null` либо `response.error.code` — непустая строка. Кроме того, `error.code` не строка и не `null`/`""` (число, список, bool) — тоже ошибка файла, код `UNKNOWN_ERROR` (R-32, design #71);
**успех файла — `error.code == ""`** (реальный успешный ответ ИС несёт `error = {"code": "", "message": ""}`, поэтому
проверка «`error` не `null`» неверна; отсутствующий или `null` `error` ошибкой не считается). Если ошибочны **все**
файлы — `status = error`, `error.code` — код первого ошибочного файла по имени (коды Content AI совпадают с перечнем
@ -214,4 +214,4 @@ warnings; `aep_5.PDF` → `INVALID_DOCUMENT` с `raw_s3_key`; объект `raw/
| 76 | 2026-10-04 | R-36 (design #76): решения исполнителя Task 5 (7.4): интеграционный `test_roundtrip.py` с `SCAN_SETTLE_SECONDS=0.2` и `SCAN_POLL_SECONDS=0.1` (`.env.test` держит settle = 0 для остальных тестов); эмулятору теста отдаются копии только `aep_3.json`, `aep_3_ofr.json`, `aep_5.json`, `aep_6.json` и синтетический `ok_doc.json` (не весь `fixtures/ocr`); сценарий `aep_5` берёт реальное имя `aep_5.PDF` (расширение в верхнем регистре) и проверяет, что единственный ошибочный файл попадает и в итоговую ошибку, и в `metrics.warnings` (D-5); проверка `raw/` общая для всех сценариев: имена файлов склейки = все `*.json` каталога ответа по возрастанию, тела совпадают с файлами; в сценарии `unknown` устаревшее ожидание `data_ocr = []` заменено на объект с `table_structure: []` (R-28) — расхождение обнаружено первым прогоном (в Task 4 тест не запускался, №74); T3.3: D-5 уточнён — не строковый, не `null`/`""` `error.code` (число, список, `true`) — тоже ошибка файла с `UNKNOWN_ERROR` (R-32); T4.1: сквозной тест `TestAnswerSet` на реальных фикстурах помечен `fixtures` | тест должен проверять то, что есть в коде и спеке, а не прежнюю форму ответа; settle 0.2 с не удлиняет тест заметно; полная копия `fixtures/ocr` подтянула бы лишние наборы | тесты `fixtures` пропускаются без репозитория `../fixtures`; интеграционные тесты делят NATS и queue group `scan-gateway` с другими экземплярами шлюза |
| 77 | 2026-10-04 | R-27 (design #77): уборка после сквозной проверки Task 6 (7.5) — `docker compose -f ../docs/dev/docker-compose.yml --project-name arm-ka --profile services rm -sfv` только своих сервисов (`scan-gateway`, `scan-emulator`, `arm-core`, `arm-core-migrate`, одноразовый `minio-init`) и удаление тома `arm-ka_hotfolder` после проверки, что его не использует ни один контейнер, вместо `down -v` из плана; `postgres`/`nats`/`minio` и их тома не тронуты | проект compose `arm-ka` общий для всех worktree: `down -v` удалил бы и тома `postgres-data`/`minio-data` с данными другой работы | человек хотел полный сброс — одна команда `docker compose --profile services down -v` из `docs/dev` |
| 78 | 2026-10-04 | R-37 (design #78): состав и сборка сервисов в Task 6 (7.5): `up -d --build arm-core scan-gateway scan-emulator`, затем `up -d --wait` по тем же именам; `tabular-parser` не поднимался (конвейер `image` его не использует); контекст сборки `scan-gateway` в compose — `../../scan-gateway` от `docs/dev` worktree, т. е. сам worktree ветки (`.wt/slice-0/scan-gateway`), поэтому отдельный `make docker-build` не нужен; `arm-core:dev` пересобран из worktree `arm-core` (кэш, ID образа не изменился) | проверяется шлюз; лишний сервис только добавил бы шум на шине; правка compose не требовалась | если `tabular-parser` нужен для проверки чего-то ещё — отдельный прогон |
| 79 | 2026-10-04 | R-38 (design #79): после Task 6 тестовые строки БД оставлены (`legal_entity` ИНН `7700000001`, четыре `s3_object`/`stored_file`/`recognition_job` — id в отчёте Task 6), а объекты `uploads/…` и `raw/…` в MinIO удалены (`mc rm` поимённо); строки `s3_object` поэтому указывают на несуществующие ключи | так велел бриф (строки БД — оставить, объекты MinIO — удалить); бакет до прогона был пуст | чтение этих `s3_object` даст `NoSuchKey`; убрать — четыре `delete` по id или полный сброс |
| 79 | 2026-10-04 | R-38 (design #79): после Task 6 тестовые строки БД оставлены (`legal_entity` ИНН `7700000001`, четыре `s3_object`/`stored_file`/`recognition_job` — id в отчёте Task 6), а объекты `uploads/…` и `raw/…` в MinIO удалены (`mc rm` поимённо); строки `s3_object` поэтому указывают на несуществующие ключи | по указанию контроллера (dispatch Task 6): строки БД — оставить, объекты MinIO — удалить; бакет до прогона был пуст | чтение этих `s3_object` даст `NoSuchKey`; убрать — SQL в порядке FK (подставить id из отчёта Task 6: `<job_id>` — `015ea48c-d91f-4d90-a7d8-0d559a8739a3`, `1bc7743c-d902-40c5-9009-09bda2de5f72`, `0ea16933-4cc1-4b50-bda9-a97089a918c5`, `9942727c-7f7d-43e8-a82d-925e0140c2e7`; `<file_id>` — `92b342b2-2d65-489e-b252-e26569780373`, `072e922f-b550-461c-9bfc-7cbb1961a46b`, `61a53aaf-af6c-4b30-8651-97c03bd0ba8e`, `e2d4f412-ecd7-4fd3-b2e3-91693432a186`; `<s3_object_id>` — `d569c917-c764-4591-be16-8058a688abf7`, `2ef2d832-d4f1-4819-9467-36bd24bf4c93`, `cd61b5f0-9582-4da5-bc55-77e505a5b32e`, `c81c8d72-9c6f-48f8-bbbb-3c6ce7a43e4f`): `begin; delete from dkb.recognition_job where id in (<job_id>, …); delete from dkb.stored_file where id in (<file_id>, …); delete from dkb.s3_object where id in (<s3_object_id>, …); delete from dkb.legal_entity where inn = '7700000001'; commit;` (порядок: `recognition_job.file_id → stored_file`, `stored_file.s3_object_id → s3_object`, `stored_file.legal_entity_id → legal_entity`; `stored_file.current_recognition_job_id` у этих строк пуст, `document`/`osv_entry` не создавались) либо полный сброс |

File diff suppressed because one or more lines are too long