From b127c02422cdce5a52e1e460b1fa1dae28a5e451 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 16:01:13 +0400 Subject: [PATCH] =?UTF-8?q?=D0=A1=D1=80=D0=B5=D0=B7=200=20[7.5]:=20=D1=84?= =?UTF-8?q?=D0=B8=D0=BD=D0=B0=D0=BB=D1=8C=D0=BD=D0=B0=D1=8F=20=D0=B2=D0=BE?= =?UTF-8?q?=D0=BB=D0=BD=D0=B0,=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D1=8B=20=E2=80=94=20T5.1,=20T5.3,=20T6.1,=20T6.3,?= =?UTF-8?q?=20T6.6,=20M-3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- CLAUDE.md | 6 +++--- README.md | 8 +++++--- docs/superpowers/plans/2026-10-04-multi-file-response.md | 2 +- openspec/changes/slice-0-skeleton/design.md | 4 ++-- .../changes/slice-0-skeleton/open-questions-proposal.md | 2 +- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 77fabbd..40575a3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 @@ отсчёт от первого опроса, увидевшего окончательный набор). Не ждать «один файл `.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. diff --git a/README.md b/README.md index 9e00964..440587f 100644 --- a/README.md +++ b/README.md @@ -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: :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` с одной строкой `_ofr.json: INVALID_DOCUMENT: ...` (второй файл набора — ответ-ошибка); - `aep_6` — `NOT_IMPLEMENTED` без `metrics`; diff --git a/docs/superpowers/plans/2026-10-04-multi-file-response.md b/docs/superpowers/plans/2026-10-04-multi-file-response.md index 24542f9..83d0927 100644 --- a/docs/superpowers/plans/2026-10-04-multi-file-response.md +++ b/docs/superpowers/plans/2026-10-04-multi-file-response.md @@ -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/.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/.json`) в отчёт. В конце окружение остановить (`down -v` — заменено R-27, design #77: `rm -sfv` собственных сервисов). Если `../docs/dev/docker-compose.yml` требует правки (например, переменных `SCAN_TMP_DIR`/`SCAN_SETTLE_SECONDS`) — не править, а записать точный фрагмент в `open-questions-proposal.md`. diff --git a/openspec/changes/slice-0-skeleton/design.md b/openspec/changes/slice-0-skeleton/design.md index e4af381..13e1831 100644 --- a/openspec/changes/slice-0-skeleton/design.md +++ b/openspec/changes/slice-0-skeleton/design.md @@ -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: `` — `015ea48c-d91f-4d90-a7d8-0d559a8739a3`, `1bc7743c-d902-40c5-9009-09bda2de5f72`, `0ea16933-4cc1-4b50-bda9-a97089a918c5`, `9942727c-7f7d-43e8-a82d-925e0140c2e7`; `` — `92b342b2-2d65-489e-b252-e26569780373`, `072e922f-b550-461c-9bfc-7cbb1961a46b`, `61a53aaf-af6c-4b30-8651-97c03bd0ba8e`, `e2d4f412-ecd7-4fd3-b2e3-91693432a186`; `` — `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 (, …); delete from dkb.stored_file where id in (, …); delete from dkb.s3_object where id in (, …); 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` не создавались) либо полный сброс | diff --git a/openspec/changes/slice-0-skeleton/open-questions-proposal.md b/openspec/changes/slice-0-skeleton/open-questions-proposal.md index d9d3548..7916c30 100644 --- a/openspec/changes/slice-0-skeleton/open-questions-proposal.md +++ b/openspec/changes/slice-0-skeleton/open-questions-proposal.md @@ -135,7 +135,7 @@ python -m src.scan_emulator` завершается кодом 2 с сообще | 17 | Replay с `attempt + 1` для `job_id`, который есть в LRU процесса, получает только ack и не обрабатывается (R-17, spec «обработан этим процессом»); по `../docs/context/03-architecture.md` replay — «сброс в `queued` с `attempt + 1`». На единственном инстансе такое задание повисает до `reply_deadline` -> `timeout` | I-3, R-17 | При одобрении: LRU `_done` с ключом `(job_id, attempt)` — более высокий `attempt` готового задания обрабатывать заново; `_active` — по `job_id` (повтор во время обработки — только ack). Решает человек | | 18 | `make up` в tabular-parser (`../tabular-parser/Makefile`, цель `up`) тоже не запускает `minio-init`: на свежем томе бакета нет | I-1 | Чужой репозиторий — только сообщить владельцу; в scan-gateway исправлено (`run --rm minio-init` после `up -d --wait`) | | 19 | Расхождения и наблюдения контроллера при исполнении (`Observation` в `progress.md`) | `progress.md` | См. ниже | -| 20 | Сквозная проверка 7.5 (Task 6, 04.10.2026) на профиле `services`: `aep_3.pdf` → `error NOT_IMPLEMENTED` + `metrics.warnings` про `_ofr.json`, `aep_6.pdf` → `error NOT_IMPLEMENTED` без `metrics`, `aep_5.PDF` → `error INVALID_DOCUMENT` + одна строка `warnings`; **`../docs/dev/docker-compose.yml` правки не требует**: умолчания `SCAN_TMP_DIR=Tmp` и `SCAN_SETTLE_SECONDS=2` подходят (`Tmp` создаётся шлюзом в томе `hotfolder`, после прогона пуст). Устарел раздел «Проверка окружения» `../docs/dev/README.md` (сценарий 2 и абзац «не покрывает») | Task 6, R-37 (design #78) | Владельцу `docs`, в `../docs/dev/README.md`: (1) строки 220–221 заменить на `# [2026-10-04 11:23:58 UTC] 600B STANDARD 0ea16933-4cc1-4b50-bda9-a97089a918c5.json` и `dmc cat "local/arm-ka/raw/$JOB.json" # {"job_id": "", "files": [{"name": ".json", "response": {"file_uid": "", "error": {"code": "INVALID_DOCUMENT", "message": "Не удалось определить тип документа."}, "data_ocr": {…}}, "parse_error": null}]}`; (2) в строке 226 лог эмулятора — `_aep_5.PDF -> /data/hotfolder/Output/` (каталог набора, не файл); (3) строки 227–229 («успешный путь image … не проходит — нужен успешный ответ ИС») заменить на: «Успешные ответы ИС есть (`aep_3.json` + `aep_3_ofr.json`, `aep_6.json`); шлюз склеивает все `*.json` каталога ответа и отвечает `NOT_IMPLEMENTED` (нормализация — Срез 3), ошибочные файлы набора — в `metrics.warnings`»; (4) строку 230 заменить на «`processing` виден ~10 с: задержка эмулятора 2 с, затем до `SCAN_POLL_SECONDS + SCAN_SETTLE_SECONDS` (5 + 2 с) после первого опроса, увидевшего набор»; (5) в строке 255 «у ошибок `scan-gateway` `recognition_job.metrics` пустой» заменить на «у ошибок `scan-gateway` в `recognition_job.metrics` только `warnings` — по строке `<имя файла>: <код>: <сообщение>` на ошибочный файл ответа ИС, если такие есть»; (6) после сценария 2 добавить сценарий «2а. `image` → успешный ответ ИС → `NOT_IMPLEMENTED`»: `new_job image PDF ocr/aep_3.pdf && poll 60` → `# 11:22:07 queued` / `# 11:22:08 processing` / `# 11:22:18 error NOT_IMPLEMENTED`; `dpsql -c "select status, error_code, metrics from dkb.recognition_job where id = '$JOB'"` → `# error | NOT_IMPLEMENTED | {"warnings": ["_ofr.json: INVALID_DOCUMENT: Не удалось определить тип документа."]}`; `new_job image PDF ocr/aep_6.pdf && poll 60` → `error NOT_IMPLEMENTED`, `metrics` пусто; (7) в той же строке 255 «полная очистка — `docker compose --profile services down -v`» дополнить оговоркой, что это удаляет и общие тома `postgres-data`/`minio-data` всех worktree (R-27, design #77) | +| 20 | Сквозная проверка 7.5 (Task 6, 04.10.2026) на профиле `services`: `aep_3.pdf` → `error NOT_IMPLEMENTED` + `metrics.warnings` про `_ofr.json`, `aep_6.pdf` → `error NOT_IMPLEMENTED` без `metrics`, `aep_5.PDF` → `error INVALID_DOCUMENT` + одна строка `warnings`; **`../docs/dev/docker-compose.yml` функциональных правок не требует** (только устаревший комментарий, п. 11): умолчания `SCAN_TMP_DIR=Tmp` и `SCAN_SETTLE_SECONDS=2` подходят (`Tmp` создаётся шлюзом в томе `hotfolder`, после прогона пуст). Устарел раздел «Проверка окружения» `../docs/dev/README.md` (сценарий 2 и абзац «не покрывает») | Task 6, R-37 (design #78) | Владельцу `docs`, в `../docs/dev/README.md`: (1) строки 220–221 заменить на `# [2026-10-04 11:23:58 UTC] 600B STANDARD 0ea16933-4cc1-4b50-bda9-a97089a918c5.json` и `dmc cat "local/arm-ka/raw/$JOB.json" # {"job_id": "", "files": [{"name": ".json", "response": {"file_uid": "", "error": {"code": "INVALID_DOCUMENT", "message": "Не удалось определить тип документа."}, "data_ocr": {…}}, "parse_error": null}]}`; (2) в строке 226 лог эмулятора — `_aep_5.PDF -> /data/hotfolder/Output/` (каталог набора, не файл); (3) строки 227–229 («успешный путь image … не проходит — нужен успешный ответ ИС») заменить на: «Успешные ответы ИС есть (`aep_3.json` + `aep_3_ofr.json`, `aep_6.json`); шлюз склеивает все `*.json` каталога ответа и отвечает `NOT_IMPLEMENTED` (нормализация — Срез 3), ошибочные файлы набора — в `metrics.warnings`»; (4) строку 230 заменить на «`processing` виден ~10 с: задержка эмулятора 2 с, затем до `SCAN_POLL_SECONDS + SCAN_SETTLE_SECONDS` (5 + 2 с) после первого опроса, увидевшего набор»; (5) в строке 255 «у ошибок `scan-gateway` `recognition_job.metrics` пустой» заменить на «у ошибок `scan-gateway` в `recognition_job.metrics` только `warnings` — по строке `<имя файла>: <код>: <сообщение>` на ошибочный файл ответа ИС, если такие есть»; (6) после сценария 2 добавить сценарий «2а. `image` → успешный ответ ИС → `NOT_IMPLEMENTED`»: `new_job image PDF ocr/aep_3.pdf && poll 60` → `# 11:22:07 queued` / `# 11:22:08 processing` / `# 11:22:18 error NOT_IMPLEMENTED`; `dpsql -c "select status, error_code, metrics from dkb.recognition_job where id = '$JOB'"` → `# error | NOT_IMPLEMENTED | {"warnings": ["_ofr.json: INVALID_DOCUMENT: Не удалось определить тип документа."]}`; `new_job image PDF ocr/aep_6.pdf && poll 60` → `error NOT_IMPLEMENTED`, `metrics` пусто; (7) в той же строке 255 «полная очистка — `docker compose --profile services down -v`» дополнить оговоркой, что это удаляет и общие тома `postgres-data`/`minio-data` всех worktree (R-27, design #77); (8) строки 185–187 (абзац «Рецепт прогнан дословно при уже существовавшей в БД строке `legal_entity` … отдельно не проверялась.») заменить на «Рецепт прогнан дословно на чистой БД (вставка `legal_entity` без конфликта по `inn`) и повторно при уже существующей строке (`on conflict (inn) do nothing`); оба пути проверены.» (или удалить абзац: ветка вставки на чистой БД проверена в Task 6); (9) строки 224–226: «Код `INVALID_DOCUMENT` здесь — **настоящий ответ ИС из фикстуры**: обе пары в `fixtures/ocr` (`aep_5.json`, `aep_8_form4.json`) — ответы-ошибки ИС Сканирование («Не удалось определить тип документа.», пустой `table_structure`). Эмулятор нашёл пару (в логе `scan-emulator`: `_aep_5.PDF -> …/Output//.json`; при отсутствии пары сообщение было бы «…: нет эталонного ответа в фикстурах»).» заменить на «Код `INVALID_DOCUMENT` здесь — **настоящий ответ ИС из фикстуры**: `aep_5.json` и `aep_8_form4.json` — ответы-ошибки ИС Сканирование («Не удалось определить тип документа.», пустой `table_structure`); у `aep_3` и `aep_6` есть успешные ответы (сценарий 2а). Эмулятор нашёл набор ответов (в логе `scan-emulator`: `_aep_5.PDF -> /data/hotfolder/Output/`; при отсутствии набора сообщение было бы «…: нет эталонного ответа в фикстурах»).» (строка 227 «Поэтому успешный путь `image` … не проходит — нужен успешный ответ ИС в `fixtures/ocr`.» заменяется по (3)); (10) строки 252–253: «успешные результаты (`done`/`needs_check`, регистрация файла `results/…` в `s3_object`) — заглушка парсера и фикстуры ИС дают только ошибки;» заменить на «успешные результаты (`done`/`needs_check`, регистрация файла `results/…` в `s3_object`) — заглушка парсера и нормализатор шлюза Среза 0 (`NOT_IMPLEMENTED`) дают только ошибки, хотя фикстуры ИС (`aep_3`, `aep_6`) успешные ответы содержат;»; (11) `../docs/dev/docker-compose.yml:184`: «# Фикстуры ответов ИС — ../../fixtures/ocr (только чтение): пара <имя>.json для входного файла UUID_<имя>.ext.» заменить на «# Фикстуры ответов ИС — ../../fixtures/ocr (только чтение): набор <имя>.json + <имя>_*.json для входного файла UUID_<имя>.ext.» (только комментарий, поведение compose не меняется) | Строки `Observation` из `progress.md` (только пересказ, статусы — в ledger):