From 64dab792908a2d31d13b9f0f9eabfa6dd7861aef Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 18 Oct 2026 20:23:30 +0400 Subject: [PATCH] =?UTF-8?q?docs:=20README=20=D0=B8=20CLAUDE.md=20=E2=80=94?= =?UTF-8?q?=20=D0=B0=D0=B4=D0=B0=D0=BF=D1=82=D0=B5=D1=80=20OCR,=20arm=5Fst?= =?UTF-8?q?atements,=20=D0=BA=D0=BE=D0=BD=D1=82=D1=80=D0=B0=D0=BA=D1=82=20?= =?UTF-8?q?2.0;=20=D0=B8=D0=BD=D1=82=D0=B5=D0=B3=D1=80=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=BE=D0=BD=D0=BD=D1=8B=D0=B5=20=D1=82=D0=B5=D1=81=D1=82=D1=8B?= =?UTF-8?q?=20(#46)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W --- CLAUDE.md | 55 ++--- README.md | 247 +++++++---------------- api/tests/integration/test_roundtrip.py | 72 +++---- openspec/changes/statements-v2/design.md | 9 + openspec/changes/statements-v2/tasks.md | 14 +- 5 files changed, 157 insertions(+), 240 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 2a66a21..b0b9a54 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ старом АРМ), забрать **все** `Исходящие//*.json` (ИС кладёт по одному json на каждый распознанный документ), склеить и сохранить сырой ответ в S3 `raw/{job_id}.json` (в БД не регистрируется), очистить горячую папку (только при `SCAN_CLEANUP=true`), нормализовать в `../docs/contracts/recognition-result.schema.json` -(`statementDocument` v1.2: по одному документу на таблицу `table_structure[i]`), опубликовать `files.tabular_parsed` +(`statementDocument` v2.0 общим кодом `arm_statements`: по одному документу на таблицу `table_structure[i]`), опубликовать `files.tabular_parsed` (по умолчанию; задаётся `OPENBAO__NATS__NATS_*_SUBJECT`) (`done` / `needs_check` / `error`). Таймаут → ошибка `TIMEOUT` по `min(reply_deadline, старт + SCAN_TIMEOUT_SECONDS=1200)`, старт — запись файла во «Входящие». **Нет доступа к БД.** @@ -16,18 +16,21 @@ - `api/arm_statements/` — копия пакета `arm-statements`, напрямую не править: обновление — `../docs/scripts/sync-statements.sh` (`--in-place` в PR фичи), свежесть — `make statements-check` (в `check-full`). - Нормализация по фактическим ответам `../fixtures/ocr/aep_*.json`, а не по ТЗ (OQ-13, OQ-14), по `05-domain-rules.md` - §11 и §11.1: правило, которого там нет, не реализуется, а поднимается вопросом. Функции очистки - (`src/normalizer/ocr/`) перенесены из старого ядра **дословно** (единственное исключение для `reference-old-arm`): - при правке сначала разберись, что делает источник; расхождения с ним — только решением, записанным в - `openspec/changes/archive/2026-10-07-slice-3a-ocr-normalizer/design.md` (таблица «Decisions made without discussion»: №12, №13, №20, №22, - №30 — `Money` не больше двух знаков после запятой, №31 — тексты дат длиннее 200 символов не разбираются). - Функции `ocr/*` — чистые: без I/O, без настроек, без логов. -- Справочники — данные: `api/rules/*.yaml` (редакции форм `66n` / `fsbu4`, двойники, `label` → тип), правила не - зашиваются в код. `fsbu4` пока пуста — данные кладёт владелец, код при этом не меняется. -- Статус: `done` только когда всё распознано (`confidence` 100, даты разобраны, редакция определена по дате и заполнена, - нет предупреждений) и ни один файл набора не ошибочен (№28); иначе `needs_check`; ни одной таблицы → `error RECOGNITION_FAILED`. Предупреждения — строки - `metrics.warnings` (`<имя json>: table:: <текст>`); при изменении их текстов обнови README (раздел «Предупреждения») - и golden. + §11, §11.1 и §11.9: правило, которого там нет, не реализуется, а поднимается вопросом. Шаги 2–8, словари и статусы — + в пакете `arm_statements`; в шлюзе остаются только: + - адаптер шага 1 — `api/src/normalizer/ocr/adapter.py` (таблица ИС → кандидат, `ref` — номер строки ИС); + - обход набора json — `api/src/normalizer/ocr/answer_set.py` (`normalize_candidate` на каждую таблицу, + `envelope_status`, коды `JSON_FAILED` / `TABLE_PARSE_ERROR`; Р-32/Р-33); + - граница контракта — `to_contract` / `warning_to_contract` пакета, вызываются в `api/src/normalizer/ocr/__init__.py` + (`OcrNormalizer`); своего конвертера и своего уплощения шапки нет, шлюз только валидирует словарь моделями + `src/contract`. + Переносы старого ядра (`ocr/text.py … document.py`) и справочники `api/rules/` удалены: словари меняются через + репозиторий `arm-statements`, не здесь. Решения по адаптеру (P5-n, R-n) — `openspec/changes/statements-v2/design.md`. +- Статус документа и конверта определяет пакет (`envelope_status`); шлюз его не пересчитывает. Все json набора + ошибочны — `error` с кодом первого ошибочного файла, а `metrics.warnings` всё равно перечисляют `JSON_FAILED` каждого + json. Успешный json без таблиц — `JSON_FAILED`. Ни одной таблицы — `error RECOGNITION_FAILED`. +- Предупреждения — структуры `{code, severity, message, locator}`, не строки; новый код предупреждения вводится только + словарём пакета (`WarningCode`, `make_warning`). При изменении правил обнови README (раздел «Предупреждения») и golden. - Эталоны нормализатора лежат в **fixtures** (`../fixtures/ocr/expected_normalize/`): `make golden-update`, diff смотреть в fixtures и объяснять; эталоны меняются отдельным PR fixtures. - Горячие папки (раскладка FileWorker): имя `.`, запись сразу в `Input` открытием `wb` с перезаписью, без @@ -41,18 +44,18 @@ каталога по возрастанию имени; нормализатор получает ровно эту структуру. - Успех файла ответа — `error.code == ""` (`error` при этом не `null`!), ошибка — непустой `error.code`, `parse_error` или корень ответа не объект (`INVALID_FORMAT`). Проверка `error is None` для реального ответа неверна. - Ошибочные файлы — в `metrics.warnings` (`<имя>: <код>: <сообщение>`); все файлы ошибочны → код первого; есть + Ошибочные файлы — предупреждения `JSON_FAILED` в `metrics.warnings` (`locator = file:<имя>`, `message = <код>: <сообщение>`); все файлы ошибочны → код первого; есть успешный — документы из успешных файлов, статус не выше `needs_check` (№28). `NOT_IMPLEMENTED` сервис больше не публикует (`NullNormalizer` остался только для тестов воркера). -- В одном json может быть несколько таблиц разных форм (баланс, ОФР); тип документа задаёт `label` таблицы; если все коды - строк одной группы (`1xxx`/`2xxx`) и противоречат `label` — тип по кодам с предупреждением (#30); `label` не сопоставлен — коды по большинству. `checks` в документах не передаются — их считает ядро (Срез 3b). +- В одном json может быть несколько таблиц разных форм (баланс, ОФР); тип документа и редакцию определяет пакет + (`05-domain-rules.md` §11.9). `checks` считает пакет и отдаёт в документах. - Dev-режим: эмулятор ИС Сканирование (`make emulator`, то же, что `python -m src.scan_emulator` из `api/`; в контейнере — тот же образ с `command: python -m src.scan_emulator`): по входящему `.` считает sha256 содержимого, находит пару в `SCAN_FIXTURES_DIR` (PDF + `<имя>.json` + `<имя>_*.json`) и кладёт в `Output//` набор как `.json` / `_<суффикс>.json` (атомарно, через `Tmp`). Промах → набор `SCAN_EMULATOR_DEFAULT`, иначе ответ-ошибка `INVALID_DOCUMENT`. Имя входящего файла на выбор пары не влияет. - Описание сервиса, протокол горячих папок, нормализация, env, запуск и коды ошибок — `README.md`; требования и решения - Среза 3a — `openspec/changes/archive/2026-10-07-slice-3a-ocr-normalizer/` (`design.md`, `specs/`), Среза 0 — + текущей фичи — `openspec/changes/statements-v2/`, Среза 3a — `openspec/changes/archive/2026-10-07-slice-3a-ocr-normalizer/`, Среза 0 — `openspec/changes/archive/2026-10-04-slice-0-skeleton/`. ## Соглашения шаблона @@ -79,22 +82,20 @@ SMB-сессия, S3, NATS, воркер; `GET /healthcheck`; gzip/proxy middlew - `api/src/core/` — `worker.py` (задание: проверка → ack → S3 → горячая папка → опрос → `raw/` → очистка (под флагом) → нормализатор → результат; ошибки по таблице D-7), `nats.py` (core NATS без JetStream, queue group `scan-gateway`, публикация с повторами), `clients.py` (`S3Storage`), `health.py` (`nats`/`hotfolder` в `/healthcheck`). -- `api/src/normalizer/` — протокол `Normalizer`, `NormalizeOutcome`, `NullNormalizer` (заглушка Среза 0, остаётся для тестов - воркера) и `OcrNormalizer` (Срез 3a, подключён в `create_app`). -- `api/src/normalizer/ocr/` (Срез 3a) — конвейер чистых функций: `text.py` → `dates.py` → `amounts.py` → `layout.py` → - `rows.py` → `header.py` → `document.py`; `__init__.py` — `OcrNormalizer` (обход файлов склейки, статус, `metrics`). -- `api/src/normalizer/rules.py` — `load_rules(SCAN_RULES_DIR)`: pydantic-модели справочников, `OcrRules` (выбор редакции - по дате / по кодам); `api/rules/` — `statement_lines.yaml` (редакции `66n`, `fsbu4`), `ocr_lookalikes.yaml`, - `statement_labels.yaml`. -- `api/src/contract/` + `api/contracts/` — pydantic-модели и байт-в-байт копии схем `../docs/contracts`; идентичность по правилу MAJOR (S-53, +- `api/src/normalizer/` — протокол `Normalizer`, `NullNormalizer` (заглушка Среза 0, остаётся для тестов воркера; ошибки + файлов выдаёт `JSON_FAILED` через `make_warning` пакета), `outcome.py` (`NormalizeOutcome`), `files.py` (ошибки json + набора) и `OcrNormalizer` (подключён в `create_app`). +- `api/src/normalizer/ocr/` — `adapter.py` (адаптер шага 1), `answer_set.py` (обход набора json), `__init__.py` + (`OcrNormalizer`, граница контракта через `to_contract`). Остальной разбор — `api/arm_statements/` (копия пакета). +- `api/src/contract/` + `api/contracts/` — pydantic-модели (контракт 2.0, `CONTRACT_VERSION`) и байт-в-байт копии схем `../docs/contracts`; идентичность по правилу MAJOR (S-53, `tests/contract_identity.py`; docs — `../docs` или `ARM_KA_DOCS`) проверяет `api/tests/unit/test_contract.py` (входит в `make check`). - `api/src/scan_emulator/` — эмулятор ИС Сканирование (`SCAN_FIXTURES_DIR`, `SCAN_EMULATOR_DELAY`, `SCAN_EMULATOR_POLL_SECONDS`, `SCAN_EMULATOR_DEFAULT`; пара по sha256 содержимого); в compose контейнера пока нет — `make emulator`. - `Makefile` (`venv`, `lint`, `test`, `test-integration`, `check`, `docker-build`, `check-full`, `up`, - `emulator`, `golden-update`), `api/pyproject.toml` (ruff, mypy), `Dockerfile` + `.dockerignore` (один образ: шлюз и эмулятор), + `emulator`, `golden-update`, `statements-check`), `api/pyproject.toml` (ruff, mypy), `Dockerfile` + `.dockerignore` (один образ: шлюз и эмулятор), `api/.env.example`, `api/.env.test`. -- Тесты: `api/tests/unit/` (в том числе `unit/normalizer/` — юниты по модулям `ocr/` и golden), `api/tests/integration/` +- Тесты: `api/tests/unit/` (в том числе `unit/normalizer/` — адаптер, обход набора, граница контракта, реальные ответы и golden), `api/tests/integration/` (маркеры `integration`, `fixtures`; `test_roundtrip.py` — сквозной сценарий с эмулятором и `SCAN_CLEANUP`); `api/tests/golden_update.py` — сбор склейки реального ответа и запись эталонов в `../fixtures/ocr/expected_normalize/` (`make golden-update`); перед PR — `make check-full`. diff --git a/README.md b/README.md index 6a9d0e2..03a0d91 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,9 @@ Адаптер внешней ИС Сканирование (Content AI) для АРМ КА. Принимает из NATS запросы на распознавание изображений (PDF/PNG/JPG), передаёт файл в ИС через «горячие папки», забирает ответ, сохраняет сырой ответ в S3, нормализует его -и публикует результат в `files.tabular_parsed`. Контракт обмена — `../docs/contracts/` (v1.2: `statementDocument`; -`schema_version` в конвертах и файле результата сервис по-прежнему пишет `1.0` — `CONTRACT_VERSION`, решение №14 -change'а `slice-3a-ocr-normalizer`: поля 1.1–1.2 документы PDF-пути не используют), архитектура — -`../docs/context/03-architecture.md`. +и публикует результат в `files.tabular_parsed`. Контракт обмена — `../docs/contracts/` (v2.0: `statementDocument` с +`status`, `line_status`, `needs_review`, структурными предупреждениями; `schema_version = "2.0"` в конвертах, включая +ack, и в файле результата), архитектура — `../docs/context/03-architecture.md`. Границы сервиса (DECISION-4): @@ -14,7 +13,7 @@ change'а `slice-3a-ocr-normalizer`: поля 1.1–1.2 документы PDF- - сырой ответ ИС лежит в S3 (`raw/{job_id}.json`) и в БД не регистрируется; - саму ИС Сканирование мы не пишем: для разработки есть эмулятор (`api/src/scan_emulator/`). -Состояние: Срез 3a. Сквозной путь работает, ответ ИС нормализуется в `statementDocument` v1.2 (раздел «Нормализация»); +Состояние: сквозной путь работает, ответ ИС нормализуется общим кодом `arm_statements` в `statementDocument` v2.0 (раздел «Нормализация»); раскладка горячих папок — как в проверенном в контуре `armka_file_worker` (`.`). Известные ограничения — в конце файла. Правила для агента и соглашения репозитория — `CLAUDE.md`; требования и решения — `openspec/changes/archive/2026-10-07-slice-3a-ocr-normalizer/` (`design.md`: таблица «Decisions made without discussion») и архив @@ -122,8 +121,8 @@ ack (с `attempt` повторного запроса); файл в ИС вто ## Склеенный ответ ИС (`raw/{job_id}.json`) -В S3 до нормализации сохраняется **склейка** всех `*.json` каталога ответа (контракт v1.0 не меняется: `raw_s3_key` — -по-прежнему одна строка `raw/{job_id}.json`): +В S3 до нормализации сохраняется **склейка** всех `*.json` каталога ответа (`raw_s3_key` — одна строка +`raw/{job_id}.json`): ```json { @@ -142,147 +141,73 @@ ack (с `attempt` повторного запроса); файл в ИС вто `error.code == ""`** (`error` при этом не `null`: `{"code": "", "message": ""}`), поэтому проверять `error != null` нельзя. Ошибка — непустой `error.code` (`INVALID_DOCUMENT`, ...). -## Нормализация (`statementDocument` v1.2) +## Нормализация (`statementDocument` v2.0) -`OcrNormalizer` (`api/src/normalizer/ocr/`) превращает склеенный ответ ИС в документы контракта v1.2 по `§11` и `§11.1` -`../docs/context/05-domain-rules.md`: из каждой таблицы `data_ocr.table_structure[i]` каждого успешного файла получается -один `statementDocument` (`locator = "file:<имя json>;table:"`). Проверенные на стенде функции очистки старого ядра -перенесены дословно (единственное исключение для `reference-old-arm`, решение владельца 06.10.2026), DB-обвязка не -переносится. Нормализатор — чистые функции без I/O и настроек; `checks` в документах не передаются (их считает ядро). +Нормализация ББ/ОФР — общий код пакета `arm_statements` (шаги 2–8, статусы, перевод в контракт; `05-domain-rules.md` +§11.9). Копия пакета лежит в `api/arm_statements/` и напрямую не правится: обновление — `sync-statements` +(`../docs/scripts/sync-statements.sh`), свежесть — `make statements-check`. Словари (редакции форм, двойники, `label` → +тип) живут в пакете (`default_rules()`); правка словарей — через репозиторий `arm-statements`, а не здесь. -Конвейер (`src/normalizer/ocr/`): `text.py` (двойники букв и цифр, `|` как разделитель клеток, `clean_value`) → -`dates.py` (`parse_date`: даты и периоды вида «на 30 июня 2025 г», «за январь - июнь», «1 квартал», «9 месяцев» → последний день периода) → -`amounts.py` (`parse_amount`: запятая → точка, скобки → минус, прочерк → `null`) → `layout.py` и `rows.py` (раскладка -колонок по шапке с ячейкой «Код» или, если шапки нет, по данным; классы строк; повторная шапка перезаписывает -раскладку) → `header.py` (организация, ИНН, единицы, дата отчёта из `data_ocr`) → `document.py` (тип документа, -колонки, строки, значения с `is_recognized`) → `__init__.py` (`OcrNormalizer.normalize`: обход файлов склейки, статус, -`metrics`). +Что остаётся в шлюзе (`api/src/normalizer/`): -Что получается в документе (подробно — требование «Нормализация ответа ИС» спеки `openspec/changes/archive/2026-10-07-slice-3a-ocr-normalizer/`): +- `ocr/adapter.py` — адаптер **шага 1** (`05-domain-rules.md` §11.1): таблица `data_ocr.table_structure[i]` → кандидат + пакета (сетка ячеек по возрастанию `row_index`, `ref` ячейки — номер строки ИС, шапка `data_ocr`, заголовок формы). + Нарушенная структура таблицы (повтор `row_index` / `column_index`, нецелые индексы, `rows` не список) — исключение + адаптера, то есть `TABLE_PARSE_ERROR` всей таблицы. +- `ocr/answer_set.py` — обход набора json склейки: на каждую таблицу успешного файла зовётся `normalize_candidate` + пакета, исключение одной таблицы не роняет остальные; статус набора — `envelope_status` пакета. +- `ocr/__init__.py` — `OcrNormalizer`: результат пакета переводится в модели контракта функциями `to_contract` / + `warning_to_contract` (граница контракта; шлюз только валидирует словарь своими pydantic-моделями), версия копии + (`api/arm_statements/VERSION`) идёт в `NormalizeOutcome` и в `service.statements_version` файла результата. +- `files.py` — ошибки файла набора (`error.code`, `parse_error`, корень не объект); `outcome.py` — `NormalizeOutcome`. -- `document_type` — по `label` таблицы (`statement_labels.yaml`), если не сопоставился — по первой цифре кодов строк - (`1…` → `BALANCE`, `2…` → `INCOME`), иначе `OTHER` (документ всё равно отдаётся, статус `needs_check`). -- `columns` — колонки значений первой шапки, перенумерованные с 0; `period_date` — из подписи колонки. -- `lines` — только строки данных с целым кодом. Повтор кода: остаётся первая строка (в БД `line_code` уникален в - документе), остальные теряются в документе, но остаются в `raw/`. -- `values` — по одному на колонку документа; `amount` — число (в файле результата деньги строкой) или `null`; - `string_value` — текст ячейки, только когда сумма не разобрана; `is_recognized = confidence == 100 и разбор не - отбрасывал символов`. -- **`Money` (решение №30).** Контракт допускает не больше двух знаков после запятой. Сумма с тремя и более знаками - (`0,125`, `12,3456`; запятая-разделитель тысяч `4,200`/`1,234` — это 4200/1234, AURORA-1405, #35; точка так же: `4.200`/`837.994` — 4200/837994, #37) не принимается: `amount = null`, `string_value` — текст - ячейки, `is_recognized = false` (таблица получает `needs_check`, остальные ячейки и документ целы). Точка-разделитель тысяч — ровно три цифры после каждой точки, цифр до первой точки сколько угодно - (`[+-]?[1-9]\d*(\.\d{3})+`, скобки на всю строку; `4.200`, `(4.200)`, `1.234.567`, `1234.567`, `1 234.567`), без - потери `is_recognized`; `4.2`, `4.20`, `1234.56` — дроби, `1.2345` — текст (№30); для запятой правило прежнее - (`\d{0,2}` до первой запятой, `1234,567` — не тысячи). - Смешанная сумма вида `1.234,56` (разделители обоих видов) разбирается как `123456` — все точки считаются разделителями тысяч (дословный перенос) — - с `is_recognized = false`. -- `confidence` ячеек и полей шапки приводится к диапазону 0–100, `NaN` и бесконечность дают 0 (`json.loads` принимает - `NaN`/`Infinity`, а контракт требует 0–100). -- `header.report_date` — после замены двойников; `inn` — только цифры. Тексты дат (дата отчёта, подпись колонки) длиннее - 200 символов не разбираются и не проходят замену двойников (решение №31: регулярки дат на длинном тексте работают - неприемлемо долго). +Ушло в пакет: очистка текста, даты, суммы, раскладка колонок, классы строк, типы форм, редакции, статусы строк и +документов, предупреждения документов, `checks` (§7). Старые модули `ocr/text.py … document.py` и каталог `api/rules/` +удалены. -### Справочники `api/rules/` - -Данные, а не код; загружаются один раз при старте из `SCAN_RULES_DIR` (по умолчанию `rules`, путь от `api/`) с -pydantic-валидацией; битый файл (код не из 4–5 цифр, пустое наименование, дубль `id`, **повторяющийся ключ yaml** — например, -один код строки дважды в `lines`; решение №33) — сервис не стартует, в ошибке имя файла (и ключ с номером строки). - -| Файл | Содержимое | -|---|---| -| `statement_lines.yaml` | `editions`: список редакций форм `{id, valid_from?, valid_until?, lines: {код: наименование}}` | -| `ocr_lookalikes.yaml` | `digits`: символ, который OCR ставит вместо цифры → цифра (применяется только к датам, вплотную к цифрам) | -| `statement_labels.yaml` | `labels`: нормализованная (нижний регистр, не-буквенно-цифровое → пробел) подстрока `label` таблицы → `BALANCE` / `INCOME` | - -Редакции форм: `66n` (Приказ Минфина № 66н, отчётность по 2024 год включительно, `valid_until: 2024-12-31`; заполнена из -`codes_mapping` старого ядра) и `fsbu4` (ФСБУ 4/2023, отчётность с 2025 года, `valid_from: 2025-01-01`; **пока -пуста** — данные положит владелец). Редакция документа выбирается по `header.report_date`, если его нет — по самой -поздней `period_date` колонок, если нет и её — по числу совпадений кодов (с предупреждением). Имя строки берётся из -словаря выбранной редакции по коду; если кода в нём нет — из ячейки наименования OCR. Пустая выбранная редакция — -предупреждение «редакция fsbu4 не заполнена», имена из OCR, `needs_check`. - -Как заполнить `fsbu4`: в `api/rules/statement_lines.yaml` заменить `lines: {}` у редакции `id: fsbu4` на таблицу -`"<код>": "<наименование>"` (коды — строки из 4–5 цифр в кавычках, без дублей; наименования непустые; источник — -Приложения 3–4 ФСБУ 4/2023, черновой перечень кодов — шаблоны `fixtures/statements-xlsx/templates/`). Код нормализатора не -меняется. Затем: `cd api && .venv/bin/pytest tests/unit/normalizer/test_rules.py` (валидность данных), -`make golden-update` и `git -C ../fixtures diff` (эталоны документов 2025 года изменятся: имена строк и предупреждения — -объяснить в PR fixtures), перезапуск сервиса (справочники читаются при старте). - -Тесты, которые закрепляют **пустую** `fsbu4` и после заполнения упадут — их нужно обновить (список получен поиском по -`fsbu4` / `не заполнена`): - -- `api/tests/unit/normalizer/test_document.py`: `TestTypeAndEdition::test_edition_2025_is_empty` (имя строки из OCR, - предупреждение «редакция fsbu4 не заполнена») и `TestEditionWarnings::test_empty_edition_gives_single_warning` - (`warnings == ["редакция fsbu4 не заполнена"]`); -- `api/tests/integration/test_roundtrip.py`, сценарий 3 (`aep_6.pdf`, тест `test_real_fixture_aep_6_needs_check_with_two_documents`: - строки «редакция fsbu4 не заполнена» для `table:0` и `table:1`) и сценарий 4 (`ok_doc.pdf`, тест - `test_synthetic_success_answer_gives_needs_check`: имя строки `1600` «БАЛАНС» из OCR, единственное предупреждение про - пустую редакцию, статус `needs_check`; при заполненной редакции с кодом `1600` этот документ станет `done`), а также - комментарии к ним в начале файла; -- эталоны `../fixtures/ocr/expected_normalize/aep_6.json` (предупреждения «редакция fsbu4 не заполнена», имена строк - 2025 года из OCR) — их проверяет `api/tests/unit/normalizer/test_golden.py` (обновляются `make golden-update`); -- `api/tests/unit/normalizer/test_ocr_normalizer.py` и `api/tests/unit/test_worker.py` (`aep_6`, `SUCCESS_ANSWER` с датой - 2025 года) пустую `fsbu4` напрямую не закрепляют: проверяют `needs_check`, который остаётся из-за `confidence` < 100. +Документ на каждую таблицу успешного файла: `locator = "file:<имя json>;table:"`, `header.form_name` — заголовок +таблицы ИС. Подробные правила шагов 2–8 и статусов — `05-domain-rules.md` §11.9, предупреждения документа — там же и +в схеме `statementDocument`. ### Статус результата -- Все файлы набора ошибочны — `error` с кодом первого ошибочного файла по имени (см. «Коды ошибок»). -- Из успешных файлов не собран ни один документ (нет ни одной таблицы с шапкой или столбцом кода, `table_structure` - пуст или это не объект `data_ocr`) — `error`, `RECOGNITION_FAILED`, сообщение «ни одной таблицы не разобрано»; - причины по таблицам — в `metrics.warnings`; `raw_s3_key` заполнен. -- Документы есть — `done`, если всё распознано и ни один файл набора не ошибочен: у всех документов `inn_confidence` и `organization_name_confidence` - равны 100; дата отчёта разобрана и распознана; все подписи колонок разобраны в даты; все `line_code_confidence` и - `line_name_confidence` равны 100; все значения `is_recognized`; тип по `label` совпал с типом по кодам; редакция - определена по дате отчёта или дате колонки и заполнена; нет предупреждений уровня таблицы и файла нормализации. Иначе `needs_check`. - Единицы (`units`) в критерий не входят (в контракте у поля нет `confidence`). Реальные сканы почти всегда дают - `needs_check`: `done` означает лишь «нечего подсвечивать аналитику». -- Ошибочный файл набора при наличии успешного (например, `_ofr.json` с `INVALID_DOCUMENT`) **понижает статус до - `needs_check`** (решение №28: потерянный ОФР не должен остаться незамеченным под `done`); документы собираются из - успешных файлов, а ошибочный файл попадает в `metrics.warnings` строкой `<имя>: <код ИС>: <сообщение>`. -- `metrics.rows` — число строк во всех документах. Файл результата `results/{file_id}/{job_id}/result.json` пишется для - `done` и `needs_check`; предупреждения (`metrics.warnings`) лежат и в конверте, и в файле результата. +- `envelope_status` пакета: из статусов документов и предупреждений файла. Документы есть и все `done`, предупреждений + файла уровня `error` нет — `done`; иначе `needs_check`; ни одного документа — `error`. +- Все json набора ошибочны (Р-33) — `error` с кодом и сообщением первого ошибочного файла по имени (см. «Коды ошибок»); + `metrics.warnings` при этом перечисляет `JSON_FAILED` каждого json. +- Успешные файлы не дали документов (нет таблиц, все таблицы потеряны) — `error`, `RECOGNITION_FAILED`, сообщение + «ни одной таблицы не разобрано». +- Ошибочный файл при наличии успешного — `JSON_FAILED` (`error`) в `metrics.warnings`, статус не выше `needs_check`. +- Успешный json без `data_ocr`, без `table_structure` или с пустым `table_structure` — тоже `JSON_FAILED` (Р-32). +- `metrics.rows` — число строк `lines` всех документов (теперь и строки без кода). Файл результата + `results/{file_id}/{job_id}/result.json` пишется для `done` и `needs_check`; `metrics.warnings` в конверте и в файле + результата одинаковы. ### Предупреждения (`metrics.warnings`) -Одна строка на предупреждение; уровня таблицы — `<имя json>: table:: <текст>`, уровня файла — `<имя json>: <текст>`, -ошибочного файла — `<имя json>: <код ИС>: <сообщение>` (код `INVALID_FORMAT` для файла, не разобранного как JSON, и -для корня-не-объекта). Строка обрезается до 1000 символов, строк не больше 100: лишние сводятся в последнюю -(`ещё предупреждений: ; все ошибки файлов — в raw/.json`). Тексты (из `src/normalizer/ocr/`): +`metrics.warnings` — список структур `{code, severity, message, locator}` (строк больше нет). Предупреждения **файла**, +которые порождает шлюз и пакет: -| Уровень | Текст | -|---|---| -| файл | `ИНН не распознан` (в поле `inn` нет цифр, ИНН не извлечён) | -| файл | `ИНН распознан неуверенно` (ИНН извлечён, но `confidence` ≠ 100 или из текста убран мусор) | -| файл | `дата отчёта не разобрана` | -| файл | `data_ocr отсутствует или не объект` | -| таблица | `нет шапки и столбца кода` (таблица пропущена, остальные разбираются; также когда `rows` не список или у таблицы есть шапка, но нет ни одной строки данных) | -| таблица | `строк с нечисловым кодом пропущено: ` (непустой код, не целое число: `1150 :`, `изо :`; одно на таблицу) | -| таблица | `строк без кода с суммами пропущено: ` (пустой код, но в колонках значений есть разобранная сумма: «БАЛАНС», «Выручка»; как в Excel-пути, OQ-141; строки без кода и без сумм — заголовки групп, «в том числе:» — не считаются; одно на таблицу) | -| таблица | `строк с несколькими кодами разлеплено: ` (ИС склеила соседние строки: код `1500\n1700`, суммы через перевод строки; строка разлеплена на N строк, OQ-156; одно на таблицу) | -| таблица | `шапка синтезирована: колонки без дат` (в таблице нет строки-шапки: раскладка выведена по данным) | -| таблица | `колонка без даты: <подпись колонки>` | -| таблица | `нет колонок значений` | -| таблица | `повторная шапка изменила число колонок значений` (одно на таблицу; значения лишних колонок отброшены, недостающие — «ячейки нет») | -| таблица | `тип по label (