diff --git a/context/03-architecture.md b/context/03-architecture.md index 87ffb4d..bb58fd1 100644 --- a/context/03-architecture.md +++ b/context/03-architecture.md @@ -34,13 +34,19 @@ `docs/contracts/recognition-result.schema.json` (контракт v1.2: `DISCLOSURE` в `document_type`, `statementDocument.checks`, границы периода ОСВ `period_start/period_end/is_from_year_start`). Правила распознавания заголовков/группировок — `rules/*.yaml`. -- **scan-gateway** — адаптер ИС Сканирование: положить файл в горячую папку `Input/.` (временный - `<имя>..tmp` рядом, затем атомарный rename; ИС игнорирует `*.tmp`), дождаться батча `Output//*.json` - (появляется целиком и больше не меняется), сохранить сырой ответ в S3 `raw/{job_id}.json` (в БД не регистрируется), - очистить `Output//` и входящий файл, нормализовать в тот же контракт результата, опубликовать - `recognition.result`. Таймаут 5 мин → `timeout`. Раскладка папок — как у проверенного в контуре PDFWorker старого АРМ - (`RSHB/armka_file_worker`, решение владельца 06.10.2026, OQ-137); текущий код шлюза именует файл `_` - и пишет временный файл в `Tmp/` — приводится к эталону в arm-scan-gateway #6. +- **scan-gateway** — адаптер ИС Сканирование: положить файл в горячую папку `Input/.` (расширение — + суффикс `original_name` в нижнем регистре; временный `.part` пишется в `Tmp`, затем публикуется во входящий каталог + без замены; временный файл остаётся в `Tmp`, в `Input` временных файлов не кладём — решение владельца 06.10.2026), + дождаться набора `Output//*.json` (по одному json на документ; набор готов, когда имена и размеры не менялись + `SCAN_SETTLE_SECONDS`), сохранить склеенный сырой ответ в S3 `raw/{job_id}.json` (в БД не регистрируется), очистить + `Output//` и входящий файл — **только под флагом `SCAN_CLEANUP`, по умолчанию выключен** (права на удаление + на SMB-шаре контура не подтверждены, OQ-144), нормализовать в тот же контракт результата (`OcrNormalizer` по + `05-domain-rules.md` §11/§11.1: один `statementDocument` на таблицу ответа, справочники `api/rules/` с редакциями + форм), опубликовать `recognition.result`. Таймаут 5 мин → `TIMEOUT`. Раскладка папок — как у проверенного в контуре + PDFWorker старого АРМ (`RSHB/armka_file_worker`, решение владельца 06.10.2026, OQ-137) с двумя отличиями: временный + файл лежит в `Tmp`, а не рядом с назначением, очистка под флагом. Эмулятор ИС Сканирование (dev) ищет пару PDF + JSON + по sha256 содержимого входящего файла, а не по имени. Реализация — arm-scan-gateway #6 (change + `slice-3a-ocr-normalizer`). - **Frontend** — React + TS по шаблону команды; экраны по макету `dkb_24.09.26` (справочник ГК, консолидации, первичные документы, проверка распознавания, сводная ОСВ, шахматка, корректировки, гудвилл, КФО, ЕХ, журнал, роли). @@ -382,8 +388,9 @@ JetStream** (3 subject `recognition.*`; аутентификация nkeys/JWT, FileWorker — `fastapi-dkb-armka-pdf-worker`); наши сервисы держат `ROOT_PATH = /` по умолчанию и получают префикс из окружения. Healthcheck `GET /healthcheck` отвечает `503`, если NATS отключён или воркер остановлен — тот же принцип, что у нас. -- Горячие папки ИС Сканирование: эталон — раскладка FileWorker (`Input/.`, `*.tmp` рядом, - `Output//*.json`, очистка после чтения), см. описание scan-gateway выше и OQ-137. +- Горячие папки ИС Сканирование: эталон — раскладка FileWorker (`Input/.`, `Output//*.json`, + очистка после чтения). Уточнение 06.10.2026: временный `.part` остаётся в `Tmp` (в `Input` временных файлов не + класть), очистка — под `SCAN_CLEANUP` (по умолчанию выключена); см. описание scan-gateway выше, OQ-137, OQ-144. - FileWorker **не** реализует наш контракт DECISION-4: subject'ы `files.*`, конверт `task_id`/`file_id`/`source_s3_key`/ `expires_at` → `status ready|timeout|error` + `error_code`, без ack `processing`, без `Nats-Msg-Id`, без queue group (один экземпляр), дедуп только в памяти процесса — доставка at-most-once. Это параллельный сервис старого АРМ, а не diff --git a/context/05-domain-rules.md b/context/05-domain-rules.md index 6641ad8..e6a67c6 100644 --- a/context/05-domain-rules.md +++ b/context/05-domain-rules.md @@ -780,6 +780,10 @@ OQ-92 (пустые «Организация»/«Группа компаний» `l/I/|/і → 1`, `б → 6`, `S/s → 5`, `Ч → 4` — заменяются **только если символ стоит вплотную к цифре**, в несколько проходов («2ОО5» → «2005»); слова («Общество») не портятся. Если первая строка значения состоит только из цифр, пробелов, `|`, точек и двоеточий — `|` считается разделителем клеток формы («2|0|2|4|» → «2 0 2 4»), а не единицей. + **Применяется только к датам** — дате отчёта (шапка) и подписям колонок значений; коды строк очищаются п. 6 + (`clean_value`), суммы — п. 7 (`parse_amount`), ИНН — только цифры (п. 8). Замена двойников внутри кодов и сумм на + стенде не проверялась, поэтому не делается (решение №22 спеки 3a): код с латинской `O` вместо нуля не распознаётся + как целое и остаётся только в `raw/`. 2. **Даты из OCR-текста** (`date_utils._parse_date`): сначала текстовые формы по всей строке в порядке приоритета — диапазон «с 01.01.2025 по 30.09.2025» (берётся конец), «30 сентября 2025 г./r./года» (латинская `r` вместо `г`), «I квартал 2026» / «1-й кв.» / «первый квартал», «1 полугодие» / «1 п/г», «9 месяцев 2025», «январь – сентябрь 2025» @@ -800,18 +804,31 @@ OQ-92 (пустые «Организация»/«Группа компаний» ядре, но документ без шапки всё равно отдавать `needs_check` с причиной в `metrics.warnings` — колонки без `period_date` аналитик подтверждает руками. 4. **Классы строк** (`_classify_row`): пустой код → строка пропускается; нечисловой код → заголовок (не данные); - наименование «Баланс» → `BALANCE`, начинается с «итого» → `TOTAL`, иначе обычная строка. Берутся только строки с кодом - (как в §11 выше); расшифровки без кода — OQ-36. + наименование «Баланс» → `BALANCE`, начинается с «итого» → `TOTAL`, иначе обычная строка. Берутся только строки с + целым кодом (как в §11 выше); расшифровки без кода — OQ-36. Строка с **непустым нечисловым** (`1150 :`, `211(1 |`) или + **пустым** кодом пропускается, как в источнике; на момент Среза 3a **предупреждения об этом нет** (строка с суммами, + но с мусорным кодом, теряется молча и остаётся только в `raw/`) — OQ-149. **Повтор кода** в таблице: остаётся первая + строка, остальные не попадают в документ, выдаётся предупреждение «код N повторяется: оставлена первая строка», документ + `needs_check` (как §12 п. 5: в БД `financial_statement_line` уникален по `(meta_id, line_code)`; старое ядро дубли не + убирало — решение №15 спеки 3a). 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`, очищенный текст равен сырому и код + табуляции → пробел, остальное выбрасывается); наименование берётся **из справочника по коду** (`consts.codes_mapping`; + у нас — `api/rules/statement_lines.yaml` **scan-gateway** с редакциями форм `66n` / `fsbu4`, см. п. 10, а не + `rules/statement_lines.yaml` из §12 — в tabular-parser такого файла нет), а из OCR — только если кода нет в справочнике + выбранной редакции (`line_code_confidence < 100`, не выше 99). Код считается распознанным (`line_code_recognized`), если `confidence == 100`, очищенный текст равен сырому и код есть в справочнике. 7. **Суммы** (`_parse_amount`): запятая → точка, пробелы убираются; содержимое скобок — отрицательное; из строки удаляется всё, кроме цифр, `+`, `-`, `.`; несколько точек — разделители тысяч, остаётся последняя; `Decimal`, иначе - `None`. Прочерк, «–», «—», пусто — пустое значение (NULL, не 0). `string_value` — очищенный текст (`_clear_value`). + `None`. Прочерк, «–», «—», пусто — пустое значение (NULL, не 0). `string_value` — текст ячейки как распознан + (`\n`/`\t` → пробел), **только если сумма не разобрана** (`amount = null`) и ячейка не пуста и не прочерк (DBML: + «как распознано, если не число»); у разобранных сумм — `null` (решение №16 спеки 3a). + **Перенесено дословно (расхождения с желаемым — OQ-147, OQ-148):** закрывающая скобка, распознанная как `}` или `]` + (`(7744536}`), минус не ставит — сумма положительная, `is_recognized = false` (решение №12); длинное/среднее тире + «—»/«–» при `confidence = 100` даёт `is_recognized = false` (тире вырезается как «лишний» символ), дефис «-» — `true` + (решение №20). Скобки сами `is_recognized` не портят: признак считается до постановки минуса (решение №13; в источнике + любая сумма в скобках получала `false`). **`is_recognized`** значения = `confidence == 100` **и** разбор суммы не потребовал выбрасывать символы (сырой текст без скобок равен очищенному). Для ячеек шапки: `is_recognized` = `confidence == 100` и в тексте не было мусора; если дата разобралась — `period_text` нормализуется в «На ДД.ММ.ГГГГ», `period_date` — результат п. 2. Ячейки под колонкой @@ -819,9 +836,21 @@ OQ-92 (пустые «Организация»/«Группа компаний» 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`). +9. **Ошибка разбора одной таблицы** (исключение в разборе `table_structure[i]`) не роняет файл: выдаётся предупреждение + уровня таблицы `BACKEND_PARSING_ERROR: <класс исключения>: <сообщение>`, остальные таблицы файла разбираются; + хотя бы один документ есть — конверт `needs_check` (статус не бывает `done`, пока есть предупреждение уровня таблицы), + ни одной таблицы не разобрано — `error` с кодом `RECOGNITION_FAILED` («ни одной таблицы не разобрано», причины — в + `metrics.warnings`). Ошибочный файл набора ответа при успешном соседе (например, `_ofr.json` с + `INVALID_DOCUMENT`) документы успешных файлов не отменяет: ошибка попадает в `metrics.warnings` строкой + `<имя json>: <код ИС>: <сообщение>`, статус результата — не выше `needs_check`, если часть файлов набора ошибочна. +10. **Редакции форм** (`api/rules/statement_lines.yaml`): `66n` (Приказ Минфина № 66н, отчётность по 2024 год включительно) + и `fsbu4` (ФСБУ 4/2023, с 2025 года; **пока пуста** — состав кодов даёт владелец, OQ-150). Редакция документа выбирается + **по дате отчёта** шапки, если её нет — по **самой поздней `period_date`** колонок значений, если нет и её — по + **числу совпадений кодов** строк со словарями редакций (с предупреждением «редакция определена по кодам»). Имя строки — + из словаря выбранной редакции по коду, из OCR — только если кода в нём нет. **Пустая** выбранная редакция → имена строк из + OCR, одно предупреждение «редакция `` не заполнена», `needs_check`. Предупреждение «код N из редакции X» + (код есть в другой редакции, но не в выбранной) выдаётся только для **непустой** выбранной редакции — иначе каждая + строка формы 2025 года давала бы шум (решение №23 спеки 3a). Редакция формы в конверт не передаётся (OQ-145). --- diff --git a/context/07-open-questions.md b/context/07-open-questions.md index 9d65ae8..15247b8 100644 --- a/context/07-open-questions.md +++ b/context/07-open-questions.md @@ -17,7 +17,7 @@ | OQ-09 | Правила элиминации ОФР (2110/2120/2310/2320/2330): точный состав субсчетов, источник средневзвешенной ставки НДС, выбор субсчетов «работником ПА самостоятельно» | FNR03 | срез 6 | не реализовывать авто-элиминацию ОФР до решения; только ручные `correction` | | OQ-10 | Гудвилл: входит ли стр. 1350 (добавочный капитал) в базу участия; УК из ББ (1310) или из ОСВ 80/83 | DBML, DOC-259025, FNR03 | срез 6 | допущение: 1310 из ББ; 1350 — настраиваемый флаг, по умолчанию выключен | | OQ-11 | Строка «ППА» в КФО (правило № 1, когда у А нет ОСВ 03) — что это и куда | FNR03 | срез 6 | не реализовывать | -| OQ-12 | Формулы ОФР: знаки 2410/2420/2430 по ФСБУ 4/2023 vs 66н; состав 1100 (1120–1140) | DOC-221281 | срез 3 | допущение: формулы §7, строки без значения = 0 | +| OQ-12 | Формулы ОФР: знаки 2410/2420/2430 по ФСБУ 4/2023 vs 66н; состав 1100 (1120–1140) | DOC-221281 | срез 3 | допущение: формулы §7, строки без значения = 0; **расширение 07.10.2026:** формулы и справочники по редакциям форм (`66n` / `fsbu4`) — OQ-146 | | OQ-13 | `confidence` от ИС Сканирование — доли 0..1 или проценты 0..100; имя поля организации (`full_name` vs `form_name`) | FNR02 vs `aep_*.json` | срез 3 (scan-gateway) | **закрыт для scan-gateway (04.10.2026, по `aep_3.json`, `aep_6.json`):** `confidence` — числа 0–100 (не доли); название организации — `data_ocr.organization.form_name` (шумный OCR-текст), ИНН — `inn.form_name`, единицы — `units.form_name`, дата — `date.form_name` (в `aep_6.json` мусор); `full_name`/`inn.inn`/`units.measurement` из ТЗ в реальных файлах нет. Нормализация — Срез 3 | | OQ-14 | `reporting_date` в JSON ИС Сканирование — «ждём обновлённый JSON» | FNR01 | срез 3 | **уточнено (04.10.2026):** отдельного поля `reporting_date` в ответе нет; `date.form_name` ненадёжен (в `aep_6.json` — «Общество с ограниченной ответственностью», в `aep_3.json` — «2\|0\|2\|4\|»); даты колонок — в строке-шапке `rows[0]` таблицы («На 30 июня 2025 г» в `aep_6.json`), в `aep_3.json` шапки нет. Допущение прежнее: парсить из шапки таблицы, при её отсутствии — `NEEDS_CHECK`; решение — Срез 3 | | OQ-15 | Таймаут OCR 5 мин / 0 повторов (ТЗ 3) vs outbox с повторами (DBML) | DOC-037736 | срез 3 | решено DBML: `max_attempts` OCR = 1, парсер = 3 | @@ -142,13 +142,21 @@ | OQ-134 | Шапка ОСВ без строки организации и с кодом/названием счёта в двух колонках («Счет: Код / Наименование», строка «Бухгалтерский \| 9 месяцев 2025 г.»; Новоросметалл ОСВ 63): парсер не находит организацию, счёт, период, тип данных. Новый вариант заголовка — словарь или код | пакет ДКБ 06.10.2026 | tabular-parser `detect.py`/`header.py`, `osv_headers.yaml` | **реализовано** (tabular-parser PR #13, arm-tabular-parser#11 п. 4): шапка без метки организации (организация — хвост названия ведомости), период и тип данных отдельными ячейками (`periods.yaml: standalone`, `data_types.yaml: standalone_header`), подписи «Код»/«Наименование» не группировки (`osv_headers.yaml: column_captions`), код счёта — из первой строки-счёта (с предупреждением); `05-domain-rules.md` §1.1; тест `test_header_without_labels_and_code_name_columns_are_found` | | OQ-135 | Отчёт 1С «Анализ счета» с отбором по контрагентам группы (`fixtures/analysis-reports/`, в именах файлов «ОСВ_…ПоКомпаниямГруппы»): не ОСВ, но содержит ВГО по корреспонденции счетов. Нужен ли отдельный тип документа для Среза 5, или ВГО считаются только из ОСВ | пакет ДКБ 06.10.2026 | `document_type`, Срез 5 | открыт: допущение — не разбирать; парсер отвечает `INVALID_DOCUMENT` | | OQ-136 | ИНН организаций пакета ДКБ (Цимлянский, НКЗ, Новоросметалл, Приманыческий, Агрокомплекс) в шапках ОСВ отсутствуют; для теста консолидации по этим группам нужны ИНН и состав ГК/ГСК (выгрузка ЕЦП или список от ДКБ) | пакет ДКБ 06.10.2026 | seed/фикстуры справочников, тесты Среза 4–5 на новых группах | открыт | -| OQ-137 | Раскладка горячих папок ИС Сканирование: scan-gateway кладёт `Input/_` через временный `Tmp/.<имя>.part`, ничего не удаляет; PDFWorker старого АРМ (`RSHB/armka_file_worker`, проверен в контуре) кладёт `Input/.` через временный `<назначение>..tmp` рядом (ИС игнорирует `*.tmp`), читает батч `Output//*.json` и удаляет `Output//` и входящий файл после чтения. Какая раскладка актуальна? | сравнение `armka_file_worker` (README «Файлы», `clients.py`) и `scan-gateway/gateway/hotfolder.py`, `smb.py`; OQ-78, OQ-79 | scan-gateway (спека `image-recognition-gateway`, эмулятор, тесты) | **закрыт владельцем 06.10.2026: «правильно как в FileWorker»** — имя `.`, временный `*.tmp` рядом с назначением (каталог `Tmp` из OQ-79 не нужен), очистка `Output//` и входящего файла после чтения; `Output//*.json` батчем и стабилизация набора (OQ-78) остаются. Реализация — arm-scan-gateway #6 | +| OQ-137 | Раскладка горячих папок ИС Сканирование: scan-gateway кладёт `Input/_` через временный `Tmp/.<имя>.part`, ничего не удаляет; PDFWorker старого АРМ (`RSHB/armka_file_worker`, проверен в контуре) кладёт `Input/.` через временный `<назначение>..tmp` рядом (ИС игнорирует `*.tmp`), читает батч `Output//*.json` и удаляет `Output//` и входящий файл после чтения. Какая раскладка актуальна? | сравнение `armka_file_worker` (README «Файлы», `clients.py`) и `scan-gateway/gateway/hotfolder.py`, `smb.py`; OQ-78, OQ-79 | scan-gateway (спека `image-recognition-gateway`, эмулятор, тесты) | **закрыт владельцем 06.10.2026: «правильно как в FileWorker»** — имя `.`, временный `*.tmp` рядом с назначением (каталог `Tmp` из OQ-79 не нужен), очистка `Output//` и входящего файла после чтения; `Output//*.json` батчем и стабилизация набора (OQ-78) остаются. Реализация — arm-scan-gateway #6. **Уточнение 07.10.2026 (Срез 3a, решение владельца 06.10.2026):** временный `.part` остаётся в `Tmp` (в `Input` временных файлов не класть — ИС может подхватить недописанный файл, гарантии игнора `*.tmp` у нас нет; OQ-79 остаётся в силе); очистка `Output//` и входящего файла — под флагом `SCAN_CLEANUP`, по умолчанию выключена (OQ-144) | | OQ-138 | Единицы «тыс. руб.» в шапке против значений в рублях с копейками в выгрузках 1С (Унагранде Компани/Групп, НПЗ) и тысяч с дробями (Межениновская): кто и где приводит единицы. Примеры: книга НПЗ (`npz/2025-Q3`) — ББ `needs_check`, `1600 = 1700` расходится на 1.94 / 9.02 / 2.48 руб.; ОФР НПЗ и ОФР Унагранде (стр. 1) — `needs_check` по 2100/2200/2300 по той же причине; полные формы по факту сходятся только у Межениновской | `fixtures/statements-xlsx/README.md`, tabular-parser design 2b D-6, D-7, решения №22–24 (R-11, R-13, R-14) | Срез 3 (маппинг форм), §8 (КФО в тыс. руб.) | **закрыт решением владельца 06.10.2026:** единица «тыс. руб.» в шапке считается верной, значения отдаются как в файле; расхождения итогов в пределах округления (до 0,001 % наибольшего числа соотношения) статус не портят — мягкие нарушения «в пределах округления» (arm-tabular-parser#20). Книга НПЗ, ОФР стр. 1 Унагранде — `done`. Принимать ли расхождение и приводить ли единицы — решает КА в ядре (акцепт проверок, Срез 3) | -| OQ-139 | Форма без колонки кодов: сопоставление по именам строк из шаблонов 66н/ФСБУ (словарь `statement_lines.yaml`, `line_code_confidence < 100`)? Сейчас документ без строк и `needs_check` («колонка кодов не найдена») | решение владельца 06.10.2026 (вопрос 6), tabular-parser design 2b D-4 | §12 п. 2, Срез 3 | открыт: нужен реальный файл без колонки кодов; допущение — не сопоставлять по именам | +| OQ-139 | Форма без колонки кодов: сопоставление по именам строк из шаблонов 66н/ФСБУ (словарь `statement_lines.yaml`, `line_code_confidence < 100`)? Сейчас документ без строк и `needs_check` («колонка кодов не найдена») | решение владельца 06.10.2026 (вопрос 6), tabular-parser design 2b D-4 | §12 п. 2, Срез 3 | открыт: нужен реальный файл без колонки кодов; допущение — не сопоставлять по именам; **связано (Срез 3a):** то же для PDF-пути — справочник кодов по редакциям, имена из OCR при пустой редакции (OQ-146, OQ-150) | | OQ-140 | Страница-продолжение в отдельном файле (1С печатает ББ/ОФР постранично, один файл = страница): склейка в ядре по ЮЛ + период + тип формы; признак `is_continuation` / `form_page` в контракте (MINOR, две необязательных поля) | решение владельца 06.10.2026 (вопрос 2), design 2b D-8 | §12 п. 9, Срез 3, контракт v1.x | допущение принято: страницы разных файлов не склеиваются; страница 2 без шапки — документ с пустой шапкой и `needs_check`, «загрузите форму одним файлом» | | OQ-141 | Строка без кода, но с суммами в ОФР ФСБУ 4/2023 (НПЗ «Реализация иного имущества…»): хранить ли (OQ-36 расширяется с расшифровочных строк на строки с суммами) | `fixtures/statements-xlsx/npz`, design 2b D-6 | Срез 3, DBML `financial_statement_line` (хранит только строки с кодом) | допущение принято: строки без кода не отдаются, при наличии сумм — предупреждение «строк без кода с суммами пропущено: N» | | OQ-142 | Образец ФСБУ 4/2023 (`templates/ББ после 01.01.2025.xlsx`): «АКТИВ» и код 1110 в одной строке из-за объединённых ячеек; шаблоны (коды есть, сумм и шапки нет) не данные. Нужен ли их разбор вообще | `fixtures/statements-xlsx/templates`, design 2b D-8 | §12 п. 10, тесты | допущение принято: шаблоны дают `needs_check` (шапка пустая, значения `null`); разбор шаблонов как источника данных не планируется, пока владелец не скажет иначе | | OQ-143 | Лист продолжения с **единственной** кодовой строкой (книга НПЗ: лист ОФР с одной строкой 2400): `min_codes` = 3 → лист не распознаётся как форма и пропускается, строка 2400 теряется при `needs_check` документа. Понизить порог / принимать лист по близости к открытому документу ОФР / маркеру продолжения | tabular-parser design 2b: решение №25, O-9 | §12 п. 2, п. 9; полнота ОФР НПЗ | **реализовано в 2b (финальная волна, I-1):** лист с 1–2 кодами форм после листа формы берётся как продолжение (`min_codes_continuation = 1` в словаре) и присоединяется при совпадении числа колонок сумм и типа формы; не присоединён — `needs_check` и предупреждение; первая страница с 1–2 кодами формой не считается. Строка 2400 НПЗ в результате есть | +| OQ-144 | Права на удаление на SMB-шаре контура (`Input`, `Output//`, `Tmp`): можно ли сервису `scan-gateway` удалять файлы, и не удаляет ли их сама ИС Сканирование после обработки. Пока `SCAN_CLEANUP=false` — сервис ничего не удаляет (кроме `.part`, ставшего целью публикации записью без замены), в `Tmp` и `Output` накапливаются остатки | решение владельца 06.10.2026, change `slice-3a-ocr-normalizer` D-2, решения №26/№27 | scan-gateway (горячие папки), эксплуатация стенда | открыт: допущение — очистка выключена; dev-compose включает `SCAN_CLEANUP=true`. Спросить платформу/владельцев ИС; при положительном ответе — включить флаг на стенде | +| OQ-145 | Редакция формы (`66n` / `fsbu4`) в контракте: нужна ли ядру редакция документа в конверте (необязательное поле `statementDocument`, кандидат MINOR 1.3), или ядро выбирает редакцию по `report_date` само, по тому же правилу §11.1 п. 10. Сейчас scan-gateway выбирает редакцию внутри и наружу её не отдаёт | change `slice-3a-ocr-normalizer`, открытый вопрос 2 | контракт `docs/contracts/` (v1.3), Срез 3b (маппинг форм, справочник кодов в ядре) | открыт: допущение — в конверт не передаётся, контракт остаётся v1.2; при положительном решении — MINOR 1.3 со ссылкой на этот OQ | +| OQ-146 | Словари §7 (проверки форм) и §12 (справочник кодов `statement_lines.yaml`, формулы ОФР) **по редакциям** 66н / ФСБУ 4/2023 для tabular-parser (2b) и ядра (3b): один набор правил на обе редакции или раздельные; как выбирать редакцию в ядре для Excel-пути | change `slice-3a-ocr-normalizer` (справочник с редакциями только в scan-gateway), OQ-12, OQ-139 | tabular-parser `rules/*.yaml`, ядро (Срез 3b), §7, §12 п. 2 | открыт: допущение — scan-gateway ведёт свой `api/rules/statement_lines.yaml`; tabular-parser и ядро словарь по редакциям не используют; расширяет OQ-12 и OQ-139 | +| OQ-147 | Закрывающая скобка, распознанная OCR как `}` или `]` (`(7744536}` в `aep_3.json`): дословный перенос `parse_amount` минус не ставит — сумма положительная, `is_recognized = false`. Расширить ли множество закрывающих скобок в §11.1 п. 7 после проверки на стенде | change `slice-3a-ocr-normalizer`, решение №12; владелец 06.10.2026: «пока устроит как есть» | scan-gateway `normalizer/ocr/amounts.py`, §11.1 п. 7 | открыт: допущение — перенос дословный; знак теряется, строка подсвечена `is_recognized = false`, исходный текст — в `raw/` | +| OQ-148 | Длинное/среднее тире «—»/«–» как прочерк: дословный `parse_amount` вырезает тире как «лишний» символ → сумма `null`, но `is_recognized = false` даже при `confidence = 100` (дефис «-» даёт `true`). Лишний `needs_check` на формах с длинным тире. Считать ли прочерк любого вида распознанным | change `slice-3a-ocr-normalizer`, решение №20 | scan-gateway `amounts.py`, §11.1 п. 7, доля `done` на реальных сканах | открыт: допущение — перенос дословный; правило уточняется после прогона на стенде | +| OQ-149 | Строки с непустым нечисловым (`1150 :`, `211(1 |`) или пустым кодом пропускаются нормализатором (поведение источника, §11.1 п. 4); предупреждения о пропущенной строке с данными в Срезе 3a нет — строка теряется молча (остаётся в `raw/`). Пример: в `aep_3.json` строка «БАЛАНС» с пустым кодом и строка «Выручка» с кодом `211(1 |` (вместо 2110). Восстанавливать ли строку по наименованию (словарь редакции) и выдавать ли предупреждение | `fixtures/ocr/aep_3.json`, change `slice-3a-ocr-normalizer`, §11.1 п. 4 | scan-gateway `rows.py`/`document.py`, полнота форм, OQ-36, OQ-139 | открыт: допущение — строка пропускается как в источнике; аналитик видит расхождение итогов в проверках 3b | +| OQ-150 | Состав редакции `fsbu4` (ФСБУ 4/2023, отчётность с 2025 года) в `api/rules/statement_lines.yaml` scan-gateway: коды и наименования строк ББ и ОФР; источник — Приложения 3–4 к ФСБУ 4/2023 (черновой перечень кодов — шаблоны `fixtures/statements-xlsx/templates/`, OQ-142). Пока `lines: {}` | change `slice-3a-ocr-normalizer`, D-7 | scan-gateway `api/rules/`, эталоны `fixtures/ocr/expected_normalize/` для документов 2025 года, статус `done` | открыт: **данные даёт владелец**, код нормализатора не меняется; до заполнения документы 2025 года идут с именами из OCR, предупреждением «редакция fsbu4 не заполнена» и `needs_check`. Порядок заполнения — `scan-gateway/README.md` | +| OQ-151 | Эмулятор ИС Сканирование: файл, совпавший по sha256 с PDF из `SCAN_FIXTURES_DIR`, у которого нет JSON-набора (`<имя>.json`), отвечает «нет пары» (`INVALID_DOCUMENT`) и **не** уходит на `SCAN_EMULATOR_DEFAULT`. Пробел спеки эмулятора: считать ли такой файл промахом и отдавать набор по умолчанию | change `slice-3a-ocr-normalizer`, спека `scan-emulator` | scan-gateway `scan_emulator`, dev-стенд (`dev/qa-guide.md` §6) | открыт: допущение — поведение как есть; PDF без JSON-пары в `fixtures/ocr` не класть | Служебные идентификаторы в DBML/аналитике: `ANL-nnn` — замечания аналитика к макету; `ARCH-nnn` — архитектурные решения; `DECISION-n` — решения по модели; `FIX-nn` — исправления дефектов v4. diff --git a/context/09-delivery-plan.md b/context/09-delivery-plan.md index 672ccc3..e32136b 100644 --- a/context/09-delivery-plan.md +++ b/context/09-delivery-plan.md @@ -169,16 +169,31 @@ Change'и: `arm-core/openspec/changes/slice-2c-osv-ingest` (issue arm-core#14, ## Срез 3 — ББ/ОФР: scan-gateway, маппинг форм, логические проверки, подтверждение документа -- `scan-gateway`: горячие папки (по FNR02) с заглушкой-эмулятором ИС Сканирование для dev (возвращает `aep_*.json` - по имени файла); сырой ответ → S3 `raw/`; нормализация в контракт результата по §11 и **§11.1** (очистка OCR-текста: - перенос проверенного на стенде модуля `ofr_result_conversion` старого ядра — единственное разрешённое исключение для - `reference-old-arm`, решение владельца 06.10.2026); таймаут 5 мин. -- Маппинг: `financial_statement_meta/column/line/value` из JSON; числа, скобки, confidence; ИНН/имя сверка. +Срез 3 делится на два подсреза с отдельными change'ами. + +### Срез 3a — scan-gateway: горячие папки и нормализатор OCR (репозиторий scan-gateway) + +Change `slice-3a-ocr-normalizer` (arm-scan-gateway#6; docs#46, fixtures#12). Реализация — по `05-domain-rules.md` §11 и §11.1. + +- Горячие папки в раскладке FileWorker (OQ-137): `Input/.`, `.part` в `Tmp` и запись без замены, `Output//*.json` + набором (по стабилизации), очистка — под флагом `SCAN_CLEANUP` (по умолчанию выключена, OQ-144); таймаут 5 мин. +- Эмулятор ИС Сканирование для dev: пара PDF + JSON по sha256 содержимого (`fixtures/ocr`), набор по умолчанию + `SCAN_EMULATOR_DEFAULT`; сырой ответ → S3 `raw/`. +- Нормализатор `OcrNormalizer`: перенос проверенного на стенде модуля `ofr_result_conversion` старого ядра (единственное + разрешённое исключение для `reference-old-arm`, решение владельца 06.10.2026) в `statementDocument` v1.2 — по документу на + таблицу; справочники `api/rules/` с редакциями форм `66n` / `fsbu4` (`fsbu4` заполняет владелец, OQ-150); `checks` не передаются. +- Эталоны нормализатора — `fixtures/ocr/expected_normalize/` (отдельный PR fixtures), `make golden-update`. +- Новые вопросы: OQ-144…OQ-151. + +### Срез 3b — маппинг форм, проверки, подтверждение (arm-core) + +- Маппинг: `financial_statement_meta/column/line/value` из `statementDocument` (общий для Excel и OCR); числа, скобки, confidence; ИНН/имя сверка. - Логические проверки §7 (чистые функции `domain/checks.py`), акцепт КА, «Подтвердить данные» → `CONFIRMED`, `RECOGNIZED_DATA_EDIT` с before/after. - Чек-лист `required_document_rule × периметр − document CONFIRMED` (API + модалка + CSV). - UI: экран «Проверка распознавания» (исходник слева — PDF.js, результат справа, блок «Логические ошибки»). - Тесты: пары `aep_N.pdf/json`; машиночитаемые PDF АО РБ 2024 как dev-путь без OCR (опционально pdfplumber). +- Открытые вопросы для 3b: редакция формы в контракте (OQ-145), словари §7/§12 по редакциям (OQ-146), строки с нечисловым кодом (OQ-149). ## Срез 4 — Консолидация: карточка, версии, периметр, периоды, сводная ОСВ diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index c98bc3b..cc29c7d 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -181,7 +181,10 @@ services: # Эмулятор ИС Сканирование: тот же образ, что у шлюза (собирает сервис scan-gateway, см. arm-core-migrate); общий # том hotfolder с шлюзом. Переменные MinIO/NATS не нужны. - # Фикстуры ответов ИС — ../../fixtures/ocr (только чтение): набор <имя>.json + <имя>_*.json для входного файла UUID_<имя>.ext. + # Фикстуры ответов ИС — ../../fixtures/ocr (только чтение). Входной файл шлюз называет . (имя файла + # фикстуры не несёт); эмулятор считает sha256 содержимого и ищет пару PDF + набор <имя>.json / <имя>_*.json по хешу + # (aep_3.pdf, aep_6.pdf, ...). Любое другое содержимое — ответ-ошибка INVALID_DOCUMENT, пока SCAN_EMULATOR_DEFAULT пуст; + # чтобы любой файл получал набор по умолчанию, задайте SCAN_EMULATOR_DEFAULT=<имя набора>, например aep_6. scan-emulator: profiles: ["services"] image: scan-gateway:dev @@ -192,6 +195,8 @@ services: SCAN_HOTFOLDER_PATH: /data/hotfolder SCAN_FIXTURES_DIR: /fixtures/ocr SCAN_EMULATOR_DELAY: "2" + # Набор по умолчанию для файлов без пары по содержимому. Пусто — такой файл получает INVALID_DOCUMENT. + SCAN_EMULATOR_DEFAULT: "" volumes: - hotfolder:/data/hotfolder - ../../fixtures/ocr:/fixtures/ocr:ro @@ -213,6 +218,9 @@ services: <<: *service-env SCAN_HOTFOLDER_MODE: local SCAN_HOTFOLDER_PATH: /data/hotfolder + # Очистка Output// и входящего файла после чтения ответа (dev-том принадлежит нам). На шаре контура по + # умолчанию выключена: права на удаление не подтверждены (OQ-144). + SCAN_CLEANUP: "true" volumes: - hotfolder:/data/hotfolder ports: diff --git a/dev/qa-guide.md b/dev/qa-guide.md index 6e6a0a0..dc84c6f 100644 --- a/dev/qa-guide.md +++ b/dev/qa-guide.md @@ -18,6 +18,8 @@ Node 24, uv 0.10): от пустого каталога до экрана адм - Загрузка файлов ОСВ через API (`POST /legal-entities/{id}/files`), распознавание реальным `tabular-parser`, разбор результата в документы и строки ОСВ, статусы файла и документа, автозамена версий, подтверждение документа, удаление файла (раздел 6.6); проверка парсера на произвольных форматах ОСВ и формах ББ/ОФР — раздел 6.7 (маппинг форм в ядре — Срез 3). Всё — через Swagger/curl, UI «Первичные документы» — подсрез 2d. +- Распознавание PDF в `scan-gateway` с эмулятором ИС Сканирование (Срез 3a, раздел 6.8): отдельно от ядра, без UI; загружать + `aep_3.pdf` / `aep_6.pdf` из `fixtures/ocr`. - Консолидация через API (Срез 4): версии за период, периметр, блокировка «взять в работу», статусы, привязка подтверждённых документов, чек-лист, сводная ОСВ, ручное сопоставление контрагента, экспорт xlsx (раздел 7; полный прогон на данных РБ и контрольные цифры — 7.1–7.3). UI консолидации — другой исполнитель. @@ -429,6 +431,31 @@ ENV_FILE=.env.test .venv/bin/python -m src.parser.cli "/путь/к/файлу.x отступов, `done` при реально не сходящихся суммах, падение (`RECOGNITION_FAILED`) на любом файле. К багу прикладывайте файл (или его обезличенную копию) и `result.json`. +### 6.8 Распознавание PDF: scan-gateway и эмулятор ИС Сканирование (Срез 3a) + +PDF и изображения обрабатывает `scan-gateway`; в ядре маппинг форм из PDF (`statementDocument`) — Срез 3b, поэтому из +UI/API ядра PDF-путь пока не проверяется. Сервис и нормализатор проверяются отдельно: поднимите профиль `services` +(`scan-gateway` и `scan-emulator` в `docs/dev/docker-compose.yml`) и пройдите сценарий из README `scan-gateway` («Полный круг против compose и эмулятора»: +файл в MinIO → `recognition.image.request` в NATS → ack и итоговый конверт). + +Что загружать на стенде: **PDF из `fixtures/ocr` — `aep_3.pdf` и `aep_6.pdf`**. Эмулятор узнаёт их **по содержимому** +(sha256), а не по имени: шлюз кладёт файл в горячую папку как `.`, и имя фикстуры в нём не сохраняется. +Любой другой PDF даёт ответ-ошибку `INVALID_DOCUMENT` («нет пары по содержимому»), если не задан `SCAN_EMULATOR_DEFAULT` +(в compose по умолчанию пуст). + +| Файл | Ожидание | +|---|---| +| `aep_3.pdf` | `needs_check`, два документа (`BALANCE`, `INCOME`); в `metrics.warnings` — предупреждения нормализации и строка `_ofr.json: INVALID_DOCUMENT: …` (второй файл набора — ответ-ошибка, штатно) | +| `aep_6.pdf` | `needs_check`, два документа; в предупреждениях «редакция fsbu4 не заполнена» (форма 2025 года, редакция `fsbu4` пока пуста — OQ-150) | +| `aep_5.PDF`, `aep_8.pdf` | `error`, `INVALID_DOCUMENT` с сообщением ИС (в фикстурах лежат ответы-ошибки) | +| любой другой PDF | `error`, `INVALID_DOCUMENT` эмулятора | + +Что смотреть: `raw/.json` (склейка всех файлов набора) и `results///result.json` в MinIO; подписи +колонок разобраны в даты, коды строк, суммы со скобками (минус), `is_recognized`, `string_value` только у неразобранных сумм. +`done` на реальных сканах почти не бывает — это ожидаемо. В dev-compose у шлюза `SCAN_CLEANUP=true`: после ответа +`Output//` и входящий файл удаляются. Что не считать багом: потеря строки с мусорным кодом (OQ-149), минус не поставлен +при `}` вместо `)` (OQ-147), `is_recognized = false` у прочерка «—» (OQ-148). + ## 7. Консолидация через API (Срез 4) Предусловия: ядро запущено (раздел 4.2); для сценариев с файлами — ещё `NATS_ENABLED=true` и tabular-parser на `:8001` @@ -599,6 +626,8 @@ Q3-only — `missing_quarters = ["Q1","Q2"]`. - Ручное сопоставление контрагента меняет строку ОСВ для всех версий, куда входит документ (OQ-123); рекомендация кандидата — только при точном совпадении нормализованного имени (OQ-127). +- PDF-путь (Срез 3a): ядро PDF в формы не маппит (Срез 3b); эмулятор отвечает только на PDF из `fixtures/ocr`, остальные — + `INVALID_DOCUMENT` (раздел 6.8); редакция `fsbu4` не заполнена (OQ-150). - Парсер ОСВ: границы и «не баги» — в конце раздела 6.7 (счёт 91 — OQ-132; скользящий период — OQ-131). - Удаление алиаса одновременно с его переносом ручным сопоставлением даёт 404 вместо 204 (arm-core #26) — так задумано. - Код `60.АВ` (синтетика s09) и буквенные забалансовые вида `РВ.1` в план счетов не резолвятся (arm-core #28, C23-1/C23-2).