docs(scan-gateway): README и CLAUDE.md по итогам финального ревью Среза 3a
Статус при ошибочном файле набора (№28), новые предупреждения (№29, формулировка ИНН), правило Money (№30), длина дат (№31), дубли yaml (№33), очистка удаляет всё содержимое Output/<job_id>/, процедура заполнения fsbu4 со списком тестов с пустой редакцией; SCAN_CLEANUP и SCAN_RULES_DIR в .env.example. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
parent
a65ff8cf40
commit
576ce2c205
@ -17,12 +17,13 @@
|
||||
§11 и §11.1: правило, которого там нет, не реализуется, а поднимается вопросом. Функции очистки
|
||||
(`src/normalizer/ocr/`) перенесены из старого ядра **дословно** (единственное исключение для `reference-old-arm`):
|
||||
при правке сначала разберись, что делает источник; расхождения с ним — только решением, записанным в
|
||||
`openspec/changes/slice-3a-ocr-normalizer/design.md` (таблица «Decisions made without discussion»: №12, №13, №20, №22).
|
||||
`openspec/changes/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, даты разобраны, редакция определена по дате и заполнена,
|
||||
нет предупреждений); иначе `needs_check`; ни одной таблицы → `error RECOGNITION_FAILED`. Предупреждения — строки
|
||||
нет предупреждений) и ни один файл набора не ошибочен (№28); иначе `needs_check`; ни одной таблицы → `error RECOGNITION_FAILED`. Предупреждения — строки
|
||||
`metrics.warnings` (`<имя json>: table:<i>: <текст>`); при изменении их текстов обнови README (раздел «Предупреждения»)
|
||||
и golden.
|
||||
- Эталоны нормализатора лежат в **fixtures** (`../fixtures/ocr/expected_normalize/`): `make golden-update`, diff смотреть
|
||||
@ -39,7 +40,7 @@
|
||||
- Успех файла ответа — `error.code == ""` (`error` при этом не `null`!), ошибка — непустой `error.code`,
|
||||
`parse_error` или корень ответа не объект (`INVALID_FORMAT`). Проверка `error is None` для реального ответа неверна.
|
||||
Ошибочные файлы — в `metrics.warnings` (`<имя>: <код>: <сообщение>`); все файлы ошибочны → код первого; есть
|
||||
успешный — документы из успешных файлов. `NOT_IMPLEMENTED` сервис больше не публикует (`NullNormalizer` остался только
|
||||
успешный — документы из успешных файлов, статус не выше `needs_check` (№28). `NOT_IMPLEMENTED` сервис больше не публикует (`NullNormalizer` остался только
|
||||
для тестов воркера).
|
||||
- В одном json может быть несколько таблиц разных форм (баланс, ОФР); тип документа задаёт `label` таблицы (запасной
|
||||
путь — коды строк). `checks` в документах не передаются — их считает ядро (Срез 3b).
|
||||
|
||||
67
README.md
67
README.md
@ -92,8 +92,11 @@ ack (с `attempt` повторного запроса); файл в ИС вто
|
||||
|
||||
- **Когда.** Воркер вызывает `HotFolder.cleanup` сразу после того, как набор ответа прочитан, склеен и сохранён в
|
||||
`raw/{job_id}.json` — при любом исходе разбора и нормализации (в том числе когда все файлы набора — ошибки ИС).
|
||||
- **Что удаляется** (всё, что есть): `Output/<job_id>/*.json` и сам каталог `Output/<job_id>/`, `Input/<job_id>.<ext>`,
|
||||
`Input/<job_id>.<ext>.done` (след эмулятора), `Tmp/.<job_id>.<ext>.part`. Отсутствующий файл не ошибка.
|
||||
- **Что удаляется** (всё, что есть): **всё содержимое** каталога `Output/<job_id>/` (не только `*.json`) и сам каталог,
|
||||
`Input/<job_id>.<ext>`, `Input/<job_id>.<ext>.done` (след эмулятора), `Tmp/.<job_id>.<ext>.part`. Отсутствующий файл
|
||||
или каталог не ошибка. В режиме `local` каталог ответа удаляется рекурсивно; в режиме `smb` удаляются файлы каталога, а
|
||||
подкаталог внутри `Output/<job_id>/` не даст снять каталог (`rmdir` → `STATUS_DIRECTORY_NOT_EMPTY`): ошибка очистки
|
||||
поднимается и логируется, остальные пункты (входящий файл, `.done`, `.part`) в этом задании не удаляются.
|
||||
- **Когда не вызывается:** `TIMEOUT` (ИС может ещё писать ответ), `FILE_READ_ERROR`, `RECOGNITION_FAILED` записи во
|
||||
входящий каталог, ошибка записи `raw/` в S3. Входящий файл и каталог ответа тогда остаются.
|
||||
- **Ошибки удаления** (например, `PermissionError` на шаре): одна строка в логе (`warning`) на задание; конверт,
|
||||
@ -171,13 +174,22 @@ ack (с `attempt` повторного запроса); файл в ИС вто
|
||||
- `values` — по одному на колонку документа; `amount` — число (в файле результата деньги строкой) или `null`;
|
||||
`string_value` — текст ячейки, только когда сумма не разобрана; `is_recognized = confidence == 100 и разбор не
|
||||
отбрасывал символов`.
|
||||
- `header.report_date` — после замены двойников; `inn` — только цифры.
|
||||
- **`Money` (решение №30).** Контракт допускает не больше двух знаков после запятой. Сумма с тремя и более знаками
|
||||
(`1,234`, `0,125`: пробел тысяч, распознанный как запятая) не принимается: `amount = null`, `string_value` — текст
|
||||
ячейки, `is_recognized = false` (таблица получает `needs_check`, остальные ячейки и документ целы). Сумма вида
|
||||
`1.234,56` (несколько точек) разбирается как `123456` — все точки считаются разделителями тысяч (дословный перенос) —
|
||||
с `is_recognized = false`.
|
||||
- `confidence` ячеек и полей шапки приводится к диапазону 0–100, `NaN` и бесконечность дают 0 (`json.loads` принимает
|
||||
`NaN`/`Infinity`, а контракт требует 0–100).
|
||||
- `header.report_date` — после замены двойников; `inn` — только цифры. Тексты дат (дата отчёта, подпись колонки) длиннее
|
||||
200 символов не разбираются и не проходят замену двойников (решение №31: регулярки дат на длинном тексте работают
|
||||
неприемлемо долго).
|
||||
|
||||
### Справочники `api/rules/`
|
||||
|
||||
Данные, а не код; загружаются один раз при старте из `SCAN_RULES_DIR` (по умолчанию `rules`, путь от `api/`) с
|
||||
pydantic-валидацией; битый файл (код не из 4–5 цифр, пустое наименование, дубль `id`) — сервис не стартует, в ошибке
|
||||
имя файла.
|
||||
pydantic-валидацией; битый файл (код не из 4–5 цифр, пустое наименование, дубль `id`, **повторяющийся ключ yaml** — например,
|
||||
один код строки дважды в `lines`; решение №33) — сервис не стартует, в ошибке имя файла (и ключ с номером строки).
|
||||
|
||||
| Файл | Содержимое |
|
||||
|---|---|
|
||||
@ -199,20 +211,37 @@ pydantic-валидацией; битый файл (код не из 4–5 ци
|
||||
`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.
|
||||
|
||||
### Статус результата
|
||||
|
||||
- Все файлы набора ошибочны — `error` с кодом первого ошибочного файла по имени (см. «Коды ошибок»).
|
||||
- Из успешных файлов не собран ни один документ (нет ни одной таблицы с шапкой или столбцом кода, `table_structure`
|
||||
пуст или это не объект `data_ocr`) — `error`, `RECOGNITION_FAILED`, сообщение «ни одной таблицы не разобрано»;
|
||||
причины по таблицам — в `metrics.warnings`; `raw_s3_key` заполнен.
|
||||
- Документы есть — `done`, если всё распознано: у всех документов `inn_confidence` и `organization_name_confidence`
|
||||
- Документы есть — `done`, если всё распознано и ни один файл набора не ошибочен: у всех документов `inn_confidence` и `organization_name_confidence`
|
||||
равны 100; дата отчёта разобрана и распознана; все подписи колонок разобраны в даты; все `line_code_confidence` и
|
||||
`line_name_confidence` равны 100; все значения `is_recognized`; тип по `label` совпал с типом по кодам; редакция
|
||||
определена по дате отчёта или дате колонки и заполнена; нет предупреждений уровня таблицы и файла нормализации. Иначе `needs_check`.
|
||||
Единицы (`units`) в критерий не входят (в контракте у поля нет `confidence`). Реальные сканы почти всегда дают
|
||||
`needs_check`: `done` означает лишь «нечего подсвечивать аналитику».
|
||||
- Ошибочный файл набора при наличии успешного (например, `<job_id>_ofr.json` с `INVALID_DOCUMENT`) статус сам по себе не
|
||||
понижает: он попадает в `metrics.warnings` строкой `<имя>: <код ИС>: <сообщение>`.
|
||||
- Ошибочный файл набора при наличии успешного (например, `<job_id>_ofr.json` с `INVALID_DOCUMENT`) **понижает статус до
|
||||
`needs_check`** (решение №28: потерянный ОФР не должен остаться незамеченным под `done`); документы собираются из
|
||||
успешных файлов, а ошибочный файл попадает в `metrics.warnings` строкой `<имя>: <код ИС>: <сообщение>`.
|
||||
- `metrics.rows` — число строк во всех документах. Файл результата `results/{file_id}/{job_id}/result.json` пишется для
|
||||
`done` и `needs_check`; предупреждения (`metrics.warnings`) лежат и в конверте, и в файле результата.
|
||||
|
||||
@ -225,10 +254,13 @@ pydantic-валидацией; битый файл (код не из 4–5 ци
|
||||
|
||||
| Уровень | Текст |
|
||||
|---|---|
|
||||
| файл | `ИНН не распознан` (ИНН нет или `confidence` ≠ 100) |
|
||||
| файл | `ИНН не распознан` (в поле `inn` нет цифр, ИНН не извлечён) |
|
||||
| файл | `ИНН распознан неуверенно` (ИНН извлечён, но `confidence` ≠ 100 или из текста убран мусор) |
|
||||
| файл | `дата отчёта не разобрана` |
|
||||
| файл | `data_ocr отсутствует или не объект` |
|
||||
| таблица | `нет шапки и столбца кода` (таблица пропущена, остальные разбираются) |
|
||||
| таблица | `нет шапки и столбца кода` (таблица пропущена, остальные разбираются; также когда `rows` не список или у таблицы есть шапка, но нет ни одной строки данных) |
|
||||
| таблица | `строк с нечисловым кодом пропущено: <N>` (непустой код, не целое число: `1150 :`, `изо :`; одно на таблицу) |
|
||||
| таблица | `строк без кода с суммами пропущено: <N>` (пустой код, но в колонках значений есть разобранная сумма: «БАЛАНС», «Выручка»; как в Excel-пути, OQ-141; строки без кода и без сумм — заголовки групп, «в том числе:» — не считаются; одно на таблицу) |
|
||||
| таблица | `шапка синтезирована: колонки без дат` (в таблице нет строки-шапки: раскладка выведена по данным) |
|
||||
| таблица | `колонка <i> без даты: <подпись колонки>` |
|
||||
| таблица | `нет колонок значений` |
|
||||
@ -283,7 +315,7 @@ pydantic-валидацией; битый файл (код не из 4–5 ци
|
||||
| `SCAN_SETTLE_SECONDS` | `2` | набор `*.json` в `Output/<job_id>/` готов, когда имена и размеры не менялись столько секунд, считая от первого опроса, увидевшего окончательный набор (>= 0; `0` — сразу) |
|
||||
| `SCAN_TIMEOUT_SECONDS` | `300` | потолок ожидания ответа ИС, секунды (дополнительно к `reply_deadline`) |
|
||||
| `SCAN_MAX_INFLIGHT` | `4` | одновременно обрабатываемых заданий |
|
||||
| `SCAN_CLEANUP` | `false` | очистка горячей папки после чтения набора ответа: удаляет `Output/<job_id>/`, `Input/<job_id>.<ext>` (и `.done`), `Tmp/.<job_id>.<ext>.part`; `false` — ничего не удаляется (кроме `.part`, снятого самой записью); см. «Очистка горячей папки» |
|
||||
| `SCAN_CLEANUP` | `false` | очистка горячей папки после чтения набора ответа: удаляет всё содержимое `Output/<job_id>/` и каталог, `Input/<job_id>.<ext>` (и `.done`), `Tmp/.<job_id>.<ext>.part`; `false` — ничего не удаляется (кроме `.part`, снятого самой записью); см. «Очистка горячей папки» |
|
||||
| `SCAN_RULES_DIR` | `rules` | каталог справочников нормализатора (`statement_lines.yaml`, `ocr_lookalikes.yaml`, `statement_labels.yaml`), путь от текущего каталога (`api/`); битый справочник — сервис не стартует |
|
||||
| `SERVICE_NAME` | `scan-gateway` | `service.name` в конверте |
|
||||
| `VERSION` | `1.0.0` | версия сервиса (`/healthcheck`, `service.version`) |
|
||||
@ -435,9 +467,12 @@ make golden-update # пересобрать эталоны нормализ
|
||||
|
||||
- Редакция `fsbu4` (отчётность с 2025 года) пока пуста: имена строк документов 2025 года берутся из OCR, предупреждение
|
||||
«редакция fsbu4 не заполнена», статус `needs_check`. Состав редакции — данные владельца (см. «Справочники»).
|
||||
- Строки, чей код не целое число после обрезки пробелов (например, `1150 :` — перенос `_classify_row` старого ядра:
|
||||
считается повторной шапкой), и строки без кода **пропускаются молча**, без предупреждения; в документ не попадают,
|
||||
остаются в `raw/`.
|
||||
- Строки, чей код не целое число после обрезки пробелов (например, `1150 :`), разбор относит к классу `HEADER` и
|
||||
пропускает (перенос `_classify_row`; раскладка колонок при этом **не** переписывается — её меняет только строка с
|
||||
ячейкой «Код…»). Строки с пустым кодом тоже пропускаются. В документ они не попадают и остаются в `raw/`, но
|
||||
таблица получает предупреждения `строк с нечисловым кодом пропущено: <N>` и `строк без кода с суммами пропущено: <N>`
|
||||
(последнее — только если в строке есть сумма), то есть `needs_check` (решение №29). Сами строки не восстанавливаются
|
||||
(OQ-149): в реальных сканах так теряются, например, «БАЛАНС» и «Выручка» с пустым кодом.
|
||||
- Скобка, распознанная как `}` или `]` (`(7744536}`), знак минус не даёт: сумма остаётся положительной с
|
||||
`is_recognized = false` (решение №12, дословный перенос `_parse_amount`; расширение набора закрывающих скобок — после
|
||||
проверки на стенде, вынесено в открытые вопросы docs).
|
||||
@ -447,8 +482,8 @@ make golden-update # пересобрать эталоны нормализ
|
||||
`O` вместо нуля отбрасывается как не-целое (решение №22). Суммы разбираются `parse_amount` без замены символов.
|
||||
- Повтор кода строки в таблице: остаётся первая строка, вторая теряется в документе (решение №15).
|
||||
- `checks` в документах нет: соотношения §7 считает ядро (Срез 3b).
|
||||
- Ошибочный файл набора при успешных статус не понижает (`done` возможен, предупреждение `<имя>: <код>: ...` остаётся
|
||||
в `metrics.warnings`).
|
||||
- Ошибочный файл набора при успешных даёт `needs_check` (`done` невозможен, решение №28); предупреждение
|
||||
`<имя>: <код>: ...` остаётся в `metrics.warnings`.
|
||||
- Расшифровки без кода (OQ-36) и склейка одной формы из нескольких таблиц (OQ-77) не обрабатываются: каждая таблица —
|
||||
свой документ.
|
||||
|
||||
|
||||
@ -47,6 +47,12 @@ SCAN_SETTLE_SECONDS=2
|
||||
SCAN_TIMEOUT_SECONDS=300
|
||||
# Сколько заданий обрабатывается одновременно
|
||||
SCAN_MAX_INFLIGHT=4
|
||||
# Очистка горячей папки после прочтения ответа (D-2): true — удалить всё содержимое Output/<job_id>/, входящий файл
|
||||
# (и его .done), осиротевший .part в Tmp; false (по умолчанию) — ничего не удалять
|
||||
SCAN_CLEANUP=false
|
||||
# Каталог справочников нормализатора OCR (statement_lines.yaml, ocr_lookalikes.yaml, statement_labels.yaml);
|
||||
# относительный путь — от каталога запуска (api/); битый справочник — сервис не стартует
|
||||
SCAN_RULES_DIR=rules
|
||||
|
||||
# --- SMB (только при SCAN_HOTFOLDER_MODE=smb; в режиме local весь блок не нужен) ---
|
||||
# OPENBAO__SMB__SMB_HOST=smb.example.local
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user