docs(context): §11.1 очистка OCR-текста — правила из модуля старого ядра для нормализатора scan-gateway (#42)

- 05-domain-rules §11.1: буквы-двойники, `|` как разделитель, разбор дат из OCR, раскладка без шапки,
  классы строк, тип таблицы по кодам, очистка кода/наименования, суммы и критерий is_recognized,
  шапка документа, ошибка разбора одного документа. Перенос кода ofr_result_conversion — единственное
  исключение для reference-old-arm (решение владельца 06.10.2026); обвязка записи в БД не переносится.
- 09-delivery-plan: Срез 3 — нормализатор по §11 и §11.1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
Andrey Epifancev 2026-10-06 17:33:40 +04:00
parent 4583b3c358
commit 087109c9a4
2 changed files with 61 additions and 1 deletions

View File

@ -730,6 +730,64 @@ OQ-92 (пустые «Организация»/«Группа компаний»
только строки с кодом) + `financial_statement_value` (число: убрать пробелы/`-`, скобки → минус; `confidence`; `is_recognized`).
Сверки: ИНН/имя vs `legal_entity` → `document.counterparty_check`; дата vs период версии → `period_check`.
### §11.1 Очистка OCR-текста (перенос из старого ядра, Срез 3)
Источник — модуль старого ядра `DKB_ARMKA` `api/src/services/ofr_result_conversion/` (`utils.py`, `date_utils.py`,
`consts.py`; в локальном `reference-old-arm` тот же код лежит в `api/src/core/PDFWorker/`). Решение владельца
06.10.2026: модуль **проверен на реальном стенде ИС Сканирование, его код можно переносить** в нормализатор `scan-gateway`.
Это **единственное исключение** из правила «доменный код из `reference-old-arm` не переносить» (CLAUDE.md workspace).
Переносятся чистые функции очистки и разбора; обвязка старого ядра (`json_to_db_rows`, dataclass'ы `Meta/DbRow/DbValue`,
прямая запись в `financial_statement_*`) не переносится — результат ложится в `statementDocument` контракта v1.2
(`string_value`, `confidence`, `is_recognized`, `period_date`, `period_text`, `line_code_confidence`). Перенос — с тестами
на `fixtures/ocr/aep_*.json`; изменения правил ниже — только через этот параграф.
1. **Буквы-двойники рядом с цифрами** (`date_utils._normalize`, `_LOOKALIKES`): `О/о/O/o → 0`, `З/з → 3`,
`l/I/|/і → 1`, `б → 6`, `S/s → 5`, `Ч → 4` — заменяются **только если символ стоит вплотную к цифре**, в несколько
проходов («2ОО5» → «2005»); слова («Общество») не портятся. Если первая строка значения состоит только из цифр,
пробелов, `|`, точек и двоеточий — `|` считается разделителем клеток формы («2|0|2|4|» → «2 0 2 4»), а не единицей.
2. **Даты из OCR-текста** (`date_utils._parse_date`): сначала текстовые формы по всей строке в порядке приоритета —
диапазон «с 01.01.2025 по 30.09.2025» (берётся конец), «30 сентября 2025 г./r./года» (латинская `r` вместо `г`),
«I квартал 2026» / «1-й кв.» / «первый квартал», «1 полугодие» / «1 п/г», «9 месяцев 2025», «январь – сентябрь 2025»
(последний месяц), «за 2025 г.» / «2025 год» (31.12); затем числовые — три группы цифр («30.09.2025», «2025 03 31»,
«30/09/25»), иначе цифры склеиваются: 8 цифр → ДДММГГГГ / ГГГГММДД / ГГГГДДММ (первый валидный вариант), 4 → год
(31.12, только 1990–2100), 6 → ДДММГГ; хвост «0:00:00» отбрасывается. Двузначный год → 20YY. Период квартала/полугодия/
N месяцев → последний день последнего месяца. Не дата (название организации) → `None`. Разборщик общий с §12 п. 4
(Excel), там на вход приходят чистые строки.
3. **Раскладка колонок** (`_build_layout`, `_infer_layout`): шапка — строка с ячейкой, начинающейся на «код» (после
нормализации: нижний регистр, всё не-буквенно-цифровое → пробел); наименование — ячейка с «наименование», иначе ближайшая
заполненная слева от кода; колонки значений — правее кода, где в тексте есть четыре цифры года или текст начинается на
«на » / «за ». Повторная шапка внутри таблицы **перезаписывает** раскладку. **Без шапки** раскладка выводится по данным:
код — столбец с наибольшим числом значений из справочника кодов строк; наименование — столбец левее кода с наибольшим
числом текстовых ячеек; значения — столбцы правее кода, где суммы не реже мусора (полоса скана) или столбец пуст
(период без данных); шапка синтезируется (`Код` / `Наименование показателя`, тексты периодов пустые). Строки до первой
настоящей шапки разбираются по выведенной раскладке. Ни шапки, ни столбца кода — таблица пропускается с warning.
**Рулинг 06.10.2026 (вместо прежнего «без шапки → колонки без дат, `NEEDS_CHECK`»):** раскладку выводить, как в старом
ядре, но документ без шапки всё равно отдавать `needs_check` с причиной в `metrics.warnings` — колонки без `period_date`
аналитик подтверждает руками.
4. **Классы строк** (`_classify_row`): пустой код → строка пропускается; нечисловой код → заголовок (не данные);
наименование «Баланс» → `BALANCE`, начинается с «итого» → `TOTAL`, иначе обычная строка. Берутся только строки с кодом
(как в §11 выше); расшифровки без кода — OQ-36.
5. **Тип таблицы** (`_resolve_table_type`): по первой цифре кодов строк данных — больше `1xxx` → BALANCE, больше `2xxx` →
INCOME; это запасной путь, когда `label.form_name` зашумлён; при расхождении с `label` — `needs_check`.
6. **Код и наименование строки**: код очищается до букв, цифр, пробела и точки (`_clear_value`: переносы строк и
табуляции → пробел, остальное выбрасывается); наименование берётся **из справочника по коду** (`consts.codes_mapping`,
у нас — `rules/statement_lines.yaml` из §12 п. 2), а из OCR — только если код не найден в справочнике (`line_code_confidence
< 100`). Код считается распознанным (`line_code_recognized`), если `confidence == 100`, очищенный текст равен сырому и код
есть в справочнике.
7. **Суммы** (`_parse_amount`): запятая → точка, пробелы убираются; содержимое скобок — отрицательное; из строки
удаляется всё, кроме цифр, `+`, `-`, `.`; несколько точек — разделители тысяч, остаётся последняя; `Decimal`, иначе
`None`. Прочерк, «–», «—», пусто — пустое значение (NULL, не 0). `string_value` — очищенный текст (`_clear_value`).
**`is_recognized`** значения = `confidence == 100` **и** разбор суммы не потребовал выбрасывать символы (сырой текст без
скобок равен очищенному). Для ячеек шапки: `is_recognized` = `confidence == 100` и в тексте не было мусора; если дата
разобралась — `period_text` нормализуется в «На ДД.ММ.ГГГГ», `period_date` — результат п. 2. Ячейки под колонкой
заголовка нет — пустое значение с `is_recognized = false`, чтобы не сдвигать столбцы.
8. **Шапка документа**: ИНН — только цифры из `inn.form_name`, «распознан», если `confidence == 100` и в сыром тексте не
было ничего, кроме цифр; единицы — `_clear_value`, тот же критерий; дата отчёта — п. 2, «распознана», если
`confidence == 100` и дата разобралась. Нераспознанное → `needs_check` (как в §11).
9. **Ошибка разбора одного документа** (исключение в разборе `data_ocr`) не роняет файл: документ помечается ошибкой
(`BACKEND_PARSING_ERROR` + текст исключения), остальные документы файла обрабатываются; хотя бы один документ есть —
конверт `needs_check`, ни одного — `error` (в терминах `recognition-result-message`).
---
## §12. ББ и ОФР из Excel (tabular-parser, второй тип документа)

View File

@ -160,7 +160,9 @@ Change'и: `arm-core/openspec/changes/slice-2c-osv-ingest` (issue arm-core#14,
## Срез 3 — ББ/ОФР: scan-gateway, маппинг форм, логические проверки, подтверждение документа
- `scan-gateway`: горячие папки (по FNR02) с заглушкой-эмулятором ИС Сканирование для dev (возвращает `aep_*.json`
по имени файла); сырой ответ → S3 `raw/`; нормализация в контракт результата; таймаут 5 мин.
по имени файла); сырой ответ → S3 `raw/`; нормализация в контракт результата по §11 и **§11.1** (очистка OCR-текста:
перенос проверенного на стенде модуля `ofr_result_conversion` старого ядра — единственное разрешённое исключение для
`reference-old-arm`, решение владельца 06.10.2026); таймаут 5 мин.
- Маппинг: `financial_statement_meta/column/line/value` из JSON; числа, скобки, confidence; ИНН/имя сверка.
- Логические проверки §7 (чистые функции `domain/checks.py`), акцепт КА, «Подтвердить данные» → `CONFIRMED`,
`RECOGNIZED_DATA_EDIT` с before/after.