diff --git a/context/05-domain-rules.md b/context/05-domain-rules.md index 03c3618..9160c07 100644 --- a/context/05-domain-rules.md +++ b/context/05-domain-rules.md @@ -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, второй тип документа) diff --git a/context/09-delivery-plan.md b/context/09-delivery-plan.md index 72f318a..122b8f6 100644 --- a/context/09-delivery-plan.md +++ b/context/09-delivery-plan.md @@ -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.