docs(context): Срез 3a — раскладка горячих папок, §11.1 п. 6/9/10, OQ по редакциям и очистке, compose (#46) #47

Merged
aep merged 2 commits from feat/46-slice-3a-docs into main 2026-10-07 04:04:44 +03:00
9 changed files with 184 additions and 56 deletions

View File

@ -34,13 +34,20 @@
`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/<job_id>.<ext>` (временный
`<имя>.<uuid>.tmp` рядом, затем атомарный rename; ИС игнорирует `*.tmp`), дождаться батча `Output/<job_id>/*.json`
(появляется целиком и больше не меняется), сохранить сырой ответ в S3 `raw/{job_id}.json` (в БД не регистрируется),
очистить `Output/<job_id>/` и входящий файл, нормализовать в тот же контракт результата, опубликовать
`recognition.result`. Таймаут 5 мин → `timeout`. Раскладка папок — как у проверенного в контуре PDFWorker старого АРМ
(`RSHB/armka_file_worker`, решение владельца 06.10.2026, OQ-137); текущий код шлюза именует файл `<job_id>_<original_name>`
и пишет временный файл в `Tmp/` — приводится к эталону в arm-scan-gateway #6.
- **scan-gateway** — адаптер ИС Сканирование: положить файл в горячую папку `Input/<job_id>.<ext>` (расширение —
суффикс `original_name` в нижнем регистре; временный `.part` пишется в `Tmp`, а не в `Input` (решение владельца
06.10.2026) и публикуется во входящий каталог без замены; публикация сама снимает `.part` независимо от флага очистки,
в `Tmp` остаются только осиротевшие `.part` и дубли),
дождаться набора `Output/<job_id>/*.json` (по одному json на документ; набор готов, когда имена и размеры не менялись
`SCAN_SETTLE_SECONDS`), сохранить склеенный сырой ответ в S3 `raw/{job_id}.json` (в БД не регистрируется), очистить
`Output/<job_id>/` и входящий файл — **только под флагом `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 +389,9 @@ JetStream** (3 subject `recognition.*`; аутентификация nkeys/JWT,
FileWorker — `fastapi-dkb-armka-pdf-worker`); наши сервисы держат `ROOT_PATH = /` по умолчанию и получают префикс из
окружения. Healthcheck `GET /healthcheck` отвечает `503`, если NATS отключён или воркер остановлен — тот же принцип,
что у нас.
- Горячие папки ИС Сканирование: эталон — раскладка FileWorker (`Input/<job_id>.<ext>`, `*.tmp` рядом,
`Output/<job_id>/*.json`, очистка после чтения), см. описание scan-gateway выше и OQ-137.
- Горячие папки ИС Сканирование: эталон — раскладка FileWorker (`Input/<job_id>.<ext>`, `Output/<job_id>/*.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. Это параллельный сервис старого АРМ, а не

View File

@ -758,7 +758,8 @@ OQ-92 (пустые «Организация»/«Группа компаний»
- `confidence` — **числа 0–100** (в ТЗ — доли 0..1; в реальных файлах проценты) → `numeric(5,2)` как есть.
Маппинг (Срез 3): таблица `table_structure[i]` с `label` «Бухгалтерский баланс» / «Отчет о финансовых результатах» →
`document` (BALANCE/INCOME, `locator = pages:…`) + `financial_statement_meta`
`document` (BALANCE/INCOME; `locator = file:<имя json>;table:<i>` — так его формирует scan-gateway для текстового
PDF-пути, решение №1 спеки 3a; `pages:…` — только если ИС отдаст номера страниц) + `financial_statement_meta`
(ИНН, имя, units, report_date, okud — из `organization/inn/units/date.form_name`, с низкой уверенностью → `NEEDS_CHECK`) +
`financial_statement_column` (из шапки `rows[0]`, если она есть; `period_date` распарсить из «На 30 июня 2025 г», `is_current` =
первая; без шапки — колонки без дат, документ `NEEDS_CHECK`) + `financial_statement_line` (`cells[1]` код, `cells[0]` имя;
@ -780,6 +781,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»
@ -787,7 +792,9 @@ OQ-92 (пустые «Организация»/«Группа компаний»
«30/09/25»), иначе цифры склеиваются: 8 цифр → ДДММГГГГ / ГГГГММДД / ГГГГДДММ (первый валидный вариант), 4 → год
(31.12, только 1990–2100), 6 → ДДММГГ; хвост «0:00:00» отбрасывается. Двузначный год → 20YY. Период квартала/полугодия/
N месяцев → последний день последнего месяца. Не дата (название организации) → `None`. Разборщик общий с §12 п. 4
(Excel), там на вход приходят чистые строки.
(Excel), там на вход приходят чистые строки. **Отличие от источника (решение №31 спеки 3a):** текст длиннее 200
символов не разбирается (`None`, двойники к нему не применяются) — регулярки квартала и полугодия дают
экспоненциальное/кубическое backtracking на длинной строке; настоящая дата занимает десятки символов.
3. **Раскладка колонок** (`_build_layout`, `_infer_layout`): шапка — строка с ячейкой, начинающейся на «код» (после
нормализации: нижний регистр, всё не-буквенно-цифровое → пробел); наименование — ячейка с «наименование», иначе ближайшая
заполненная слева от кода; колонки значений — правее кода, где в тексте есть четыре цифры года или текст начинается на
@ -800,28 +807,65 @@ OQ-92 (пустые «Организация»/«Группа компаний»
ядре, но документ без шапки всё равно отдавать `needs_check` с причиной в `metrics.warnings` — колонки без `period_date`
аналитик подтверждает руками.
4. **Классы строк** (`_classify_row`): пустой код → строка пропускается; нечисловой код → заголовок (не данные);
наименование «Баланс» → `BALANCE`, начинается с «итого» → `TOTAL`, иначе обычная строка. Берутся только строки с кодом
(как в §11 выше); расшифровки без кода — OQ-36.
наименование «Баланс» → `BALANCE`, начинается с «итого» → `TOTAL`, иначе обычная строка. Берутся только строки с
целым кодом (как в §11 выше); расшифровки без кода — OQ-36. Строки, отброшенные классификацией, **не восстанавливаются**
(поведение источника), но о них выдаётся предупреждение уровня таблицы (решение №29 спеки 3a): `строк с нечисловым
кодом пропущено: N` — код непустой и не целое число (`1150 :`); `строк без кода с суммами пропущено: N` — код пуст, но в
колонках значений есть разобранная сумма (в `aep_3.json` «БАЛАНС» и «Выручка» имеют пустой код, мусор `211(1 |` стоит в
колонке значения; как в Excel-пути, OQ-141). Строки с пустым кодом без сумм (заголовки групп, «в том числе:»)
предупреждения не дают. Любая такая таблица — `needs_check`; восстановление строк по наименованию — OQ-149.
**Повтор кода** в таблице: остаётся первая строка, остальные не попадают в документ, выдаётся предупреждение «код N
повторяется: оставлена первая строка», документ `needs_check` (как §12 п. 5: в БД `financial_statement_line`
уникален по `(meta_id, line_code)`; старое ядро дубли не убирало — решение №15 спеки 3a). Известный недостаток: в
`aep_6` (ОФР) код 2410 повторён, первая строка пуста, а отброшенная содержит `(649)` — OQ-152 (сумма отброшенной
строки остаётся в `raw/`).
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`).
удаляется всё, кроме цифр, `+`, `-`, `.`; если точек больше одной, **удаляются все** (запятая уже стала точкой:
`1.234,56` → `123456`, дробная часть не сохраняется, `is_recognized = false`); `Decimal`, иначе `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`).
**Отличия от источника под контракт** (решение №30 спеки 3a): сумма с **более чем двумя** знаками после запятой
(`1,234`, `0,125`) не укладывается в `Money` контракта (`numeric(18,2)`) — `amount = null`, `string_value` — текст
ячейки, `is_recognized = false` (иначе один артефакт OCR, например пробел тысяч как запятая, валил всю таблицу);
`confidence` ячейки и полей шапки приводится к диапазону 0–100, NaN → 0.
**`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 или убран мусор); единицы — `_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`). Ошибочный файл набора ответа при успешном соседе (например, `<job_id>_ofr.json` с
`INVALID_DOCUMENT`) документы успешных файлов не отменяет: ошибка попадает в `metrics.warnings` строкой
`<имя json>: <код ИС>: <сообщение>`, такой файл **понижает статус до `needs_check`** (`done` невозможен, решение №28 спеки 3a: потерянный документ, например ОФР,
не должен остаться незамеченным).
10. **Редакции форм** (`api/rules/statement_lines.yaml`): `66n` (Приказ Минфина № 66н, отчётность по 2024 год включительно)
и `fsbu4` (ФСБУ 4/2023, с 2025 года; **пока пуста** — состав кодов даёт владелец, OQ-150). Редакция документа выбирается
**по дате отчёта** шапки, если её нет — по **самой поздней `period_date`** колонок значений, если нет и её — по
**числу совпадений кодов** строк со словарями редакций (с предупреждением «редакция определена по кодам»); если и кодов
не хватает — редакция **не определена**: имена строк из OCR, предупреждение «редакция не определена», `needs_check`.
Имя строки — из словаря выбранной редакции по коду, из OCR — только если кода в нём нет. **Пустая** выбранная
редакция → имена строк из OCR, одно предупреждение «редакция `<id>` не заполнена», `needs_check`. Предупреждение «код N из редакции X»
(код есть в другой редакции, но не в выбранной) выдаётся только для **непустой** выбранной редакции — иначе каждая
строка формы 2025 года давала бы шум (решение №23 спеки 3a). Редакция формы в конверт не передаётся (OQ-145).
---

View File

@ -213,11 +213,12 @@ fixtures/
analysis-reports/<org>/<период>/ # отчёты «Анализ счета» (не ОСВ)
ocr/
aep_N.pdf|PDF + aep_N.json # пары вход/выход ИС Сканирование (эмулятор ИС
# в scan-gateway ищет `<stem>.json` по имени входного
# файла без расширения, регистр его расширения для
# эмулятора неважен; важен путь самого входного файла
# `aep_5.PDF`: на регистрозависимой ФС (Linux) он ищется
# буквально)
# в scan-gateway находит пару по sha256 содержимого
# входного PDF; имя и регистр расширения входного
# файла, `<uuid>.<ext>`, значения не имеют; набор —
# `<основа>.json` + `<основа>_*.json`; PDF без
# JSON-набора (aep_4/7/9/30–34) — `INVALID_DOCUMENT`)
expected_normalize/ # эталоны нормализатора scan-gateway (Срез 3a)
golden/
Консолидация_Группа РБ.xlsx # эталон КФО по датам
expected_osv_parse/<зеркало osv/ без расширения>.json # golden-снимки парсера ОСВ: 113 файлов (94 реальных: 72 в osv/ + 22 в osv-dkb/; + 19 синтетических)

View File

@ -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,22 @@
| 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/<job_id>_<original_name>` через временный `Tmp/.<имя>.part`, ничего не удаляет; PDFWorker старого АРМ (`RSHB/armka_file_worker`, проверен в контуре) кладёт `Input/<task_id>.<ext>` через временный `<назначение>.<uuid>.tmp` рядом (ИС игнорирует `*.tmp`), читает батч `Output/<task_id>/*.json` и удаляет `Output/<task_id>/` и входящий файл после чтения. Какая раскладка актуальна? | сравнение `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»** — имя `<job_id>.<ext>`, временный `*.tmp` рядом с назначением (каталог `Tmp` из OQ-79 не нужен), очистка `Output/<job_id>/` и входящего файла после чтения; `Output/<job_id>/*.json` батчем и стабилизация набора (OQ-78) остаются. Реализация — arm-scan-gateway #6 |
| OQ-137 | Раскладка горячих папок ИС Сканирование: scan-gateway кладёт `Input/<job_id>_<original_name>` через временный `Tmp/.<имя>.part`, ничего не удаляет; PDFWorker старого АРМ (`RSHB/armka_file_worker`, проверен в контуре) кладёт `Input/<task_id>.<ext>` через временный `<назначение>.<uuid>.tmp` рядом (ИС игнорирует `*.tmp`), читает батч `Output/<task_id>/*.json` и удаляет `Output/<task_id>/` и входящий файл после чтения. Какая раскладка актуальна? | сравнение `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»** — имя `<job_id>.<ext>`, очистка `Output/<job_id>/` и входящего файла после чтения; `Output/<job_id>/*.json` батчем и стабилизация набора (OQ-78) остаются. Реализация — arm-scan-gateway #6. **Уточнение (Срез 3a, решение владельца 06.10.2026):** вместо временного `*.tmp` рядом с назначением — `.part` в `Tmp` (в `Input` временных файлов не класть: ИС может подхватить недописанный файл, гарантии игнора `*.tmp` у нас нет; OQ-79 остаётся в силе), публикация без замены снимает `.part` сама; очистка — под флагом `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` — входящие файлы и `.done`, `Output/<job_id>/`, `Tmp`): можно ли сервису `scan-gateway` удалять файлы, и не удаляет ли их сама ИС Сканирование после обработки. Пока `SCAN_CLEANUP=false` — сервис ничего не удаляет (кроме `.part`, ставшего целью публикации записью без замены), в `Input`, `Output` и `Tmp` накапливаются остатки | решение владельца 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 | Строки таблицы OCR, отброшенные классификацией (поведение источника, §11.1 п. 4), не восстанавливаются: с непустым нечисловым кодом (`1150 :`) и с пустым кодом, но с суммами (в `aep_3.json` — «БАЛАНС» и «Выручка»: код пуст, мусор `211(1 \|` стоит в колонке значения). Нормализатор выдаёт предупреждения `строк с нечисловым кодом пропущено: N` и `строк без кода с суммами пропущено: N` (как OQ-141 для Excel), документ — `needs_check`, содержимое строк остаётся в `raw/`. Восстанавливать ли такие строки по наименованию (словарь редакции) или по структуре формы | `fixtures/ocr/aep_3.json`, `aep_6.json`, change `slice-3a-ocr-normalizer`, решение №29, §11.1 п. 4 | scan-gateway `rows.py`/`document.py`, полнота форм, OQ-36, OQ-139, OQ-141 | открыт: допущение — строки пропускаются с предупреждением; аналитик видит расхождение итогов в проверках 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` не класть |
| OQ-152 | Повтор кода строки в таблице OCR: остаётся **первая** строка (паритет с Excel-путём, §12 п. 5; решение №15), но в `aep_6.json` (ОФР) код 2410 повторён, оставленная первая строка пуста, а отброшенная содержит `(649)`. Выбрать правило «первая строка с суммой» вместо «первая» (для `aep_3` оно даёт иной результат, чем для `aep_6` — нужна проверка на стенде) | change `slice-3a-ocr-normalizer`, решение №34 (R-16), финальное ревью I-2 | scan-gateway `document.py`, §11.1 п. 4, Excel-путь §12 п. 5 | открыт: допущение — первая строка; предупреждение «код N повторяется» сигнализирует аналитику, сумма отброшенной строки остаётся в `raw/` |
Служебные идентификаторы в DBML/аналитике: `ANL-nnn` — замечания аналитика к макету; `ARCH-nnn` — архитектурные решения;
`DECISION-n` — решения по модели; `FIX-nn` — исправления дефектов v4.

View File

@ -169,16 +169,33 @@ 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)
**Статус: реализован в scan-gateway, октябрь 2026; ожидает merge.** Change `slice-3a-ocr-normalizer` (arm-scan-gateway#6;
docs#46, fixtures#12), спецификация — `scan-gateway/openspec/changes/slice-3a-ocr-normalizer/` (design D-1…D-9, «Decisions made
without discussion» №1–№35). Реализация — по `05-domain-rules.md` §11 и §11.1.
- Горячие папки в раскладке FileWorker (OQ-137): `Input/<job_id>.<ext>`, `.part` в `Tmp` и запись без замены, `Output/<job_id>/*.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-152.
### Срез 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), повтор кода (OQ-152).
## Срез 4 — Консолидация: карточка, версии, периметр, периоды, сводная ОСВ

View File

@ -228,27 +228,31 @@ dmc cat "local/arm-ka/raw/$JOB.json" # {"job_id": "<job_id>", "files": [{
Код `INVALID_DOCUMENT` здесь — **настоящий ответ ИС из фикстуры**: `aep_5.json` и `aep_8_form4.json` — ответы-ошибки
ИС Сканирование («Не удалось определить тип документа.», пустой `table_structure`); у `aep_3` и `aep_6` есть успешные
ответы (сценарий 2а). Эмулятор нашёл набор ответов (в логе `scan-emulator`: `<job_id>_aep_5.PDF ->
/data/hotfolder/Output/<job_id>`; при отсутствии набора сообщение было бы «…: нет эталонного ответа в фикстурах»).
Успешные ответы ИС есть (`aep_3.json` + `aep_3_ofr.json`, `aep_6.json`); шлюз склеивает все `*.json` каталога ответа и
отвечает `NOT_IMPLEMENTED` (нормализация — Срез 3), ошибочные файлы набора — в `metrics.warnings`. Имя файла — `aep_5.PDF` (расширение в верхнем регистре; на macOS `aep_5.pdf` тоже находится, на
Linux — нет).
ответы (сценарий 2а). С Среза 3a шлюз кладёт файл во входящую папку как `<uuid>.<ext>` (расширение — из `original_name` в
нижнем регистре), а эмулятор находит пару **по sha256 содержимого**: имя и регистр расширения файла значения не имеют.
В логе `scan-emulator`: `Эмулятор: <job_id>.pdf -> /data/hotfolder/Output/<job_id> (пара: hash)`; для файла без пары ответ —
`INVALID_DOCUMENT` с текстом `<первые 12 символов sha256>: нет пары по содержимому в фикстурах` (в профиле `services`
`SCAN_EMULATOR_DEFAULT` пуст). Шлюз склеивает все `*.json` каталога ответа, нормализует их (Срез 3a) и публикует результат;
ответ-ошибка по единственному файлу набора даёт `error` с кодом ИС, ошибочные файлы набора — в `metrics.warnings`.
Текст сценария обновлён 07.10.2026 по спеке Среза 3a и на живом стенде не перепрогонялся (прогон 04.10.2026 был на Срезе 0).
`processing` виден ~10 с: задержка эмулятора 2 с, затем до `SCAN_POLL_SECONDS + SCAN_SETTLE_SECONDS` (5 + 2 с) после
первого опроса, увидевшего набор.
**2а. `image` → успешный ответ ИС → `NOT_IMPLEMENTED`:**
**2а. `image` → успешный ответ ИС → `needs_check` с документами (Срез 3a):**
```bash
new_job image PDF ocr/aep_3.pdf && poll 60
# 11:22:07 queued
# 11:22:08 processing
# 11:22:18 error NOT_IMPLEMENTED
dpsql -c "select status, error_code, metrics from dkb.recognition_job where id = '$JOB'"
# error | NOT_IMPLEMENTED | {"warnings": ["<job_id>_ofr.json: INVALID_DOCUMENT: Не удалось определить тип документа."]}
new_job image PDF ocr/aep_6.pdf && poll 60
# error NOT_IMPLEMENTED, metrics пусто
```
Ожидание (по спеке и README `scan-gateway`; вывод не прогонялся): `aep_3.pdf` — результат `needs_check` с двумя документами
(`BALANCE`, `INCOME`), в `metrics.warnings` — предупреждения нормализации и строка
`<job_id>_ofr.json: INVALID_DOCUMENT: Не удалось определить тип документа.` (второй файл набора — ответ-ошибка, штатно);
`aep_6.pdf` — `needs_check` с двумя документами, в предупреждениях «редакция fsbu4 не заполнена». Маппинг результата в формы в
ядре — Срез 3b; что именно ядро делает с конвертом `needs_check` до этого, на стенде не проверялось.
Подробности и все ожидания — `qa-guide.md` §6.8.
**3. Сервис недоступен → три повторные публикации → `dlq` / `NO_ACK`:**
```bash
@ -270,9 +274,9 @@ docker compose --profile services up -d --wait tabular-parser # вернут
перезапускается (replay — вне Среза 0).
Что сквозная проверка **не** покрывает (Срез 0): успешные результаты (`done`/`needs_check`, регистрация файла
`results/…` в `s3_object`) — заглушка парсера и нормализатор шлюза Среза 0 (`NOT_IMPLEMENTED`) дают только ошибки,
хотя фикстуры ИС (`aep_3`, `aep_6`) успешные ответы содержат; `stored_file.status` остаётся
`UPLOADED`, `current_recognition_job_id` не заполняется (маппинг результата — `NoopMapper`, Срез 2); у ошибок
`results/…` в `s3_object`) — заглушка парсера Среза 0 (`NOT_IMPLEMENTED`) даёт только ошибки; `scan-gateway` с Среза 3a
публикует и успешные результаты (сценарий 2а), но их приём ядром (`done`/`needs_check`) здесь не проверяется;
`stored_file.status` остаётся `UPLOADED`, `current_recognition_job_id` не заполняется (маппинг результата — `NoopMapper`, Срез 2); у ошибок
`scan-gateway` в `recognition_job.metrics` только `warnings` — по строке `<имя файла>: <код>: <сообщение>` на ошибочный
файл ответа ИС, если такие есть (`tabular-parser` присылает `duration_ms`). Тестовые строки и объекты остаются в БД и
MinIO; полная очистка — `docker compose --profile services down -v` (удаляет и общие тома `postgres-data`/`minio-data`

View File

@ -181,7 +181,11 @@ services:
# Эмулятор ИС Сканирование: тот же образ, что у шлюза (собирает сервис scan-gateway, см. arm-core-migrate); общий
# том hotfolder с шлюзом. Переменные MinIO/NATS не нужны.
# Фикстуры ответов ИС — ../../fixtures/ocr (только чтение): набор <имя>.json + <имя>_*.json для входного файла UUID_<имя>.ext.
# Фикстуры ответов ИС — ../../fixtures/ocr (только чтение). Входной файл шлюз называет <uuid>.<ext> (имя файла
# фикстуры не несёт); эмулятор считает sha256 содержимого и ищет пару PDF + набор <имя>.json / <имя>_*.json по хешу
# (aep_3.pdf, aep_6.pdf, ...). Файл без пары по содержимому — ответ-ошибка INVALID_DOCUMENT, пока SCAN_EMULATOR_DEFAULT
# пуст; чтобы такой файл получал набор по умолчанию, задайте SCAN_EMULATOR_DEFAULT=<имя набора>, например aep_6.
# PDF из fixtures/ocr без JSON-набора (aep_4/7/9/30–34) остаются INVALID_DOCUMENT и при заданном default (OQ-151).
scan-emulator:
profiles: ["services"]
image: scan-gateway:dev
@ -192,6 +196,9 @@ services:
SCAN_HOTFOLDER_PATH: /data/hotfolder
SCAN_FIXTURES_DIR: /fixtures/ocr
SCAN_EMULATOR_DELAY: "2"
# Набор по умолчанию для файлов без пары по содержимому. Пусто — такой файл получает INVALID_DOCUMENT.
# Файл, совпавший по хешу с PDF без JSON-набора, default не получает (OQ-151).
SCAN_EMULATOR_DEFAULT: ""
volumes:
- hotfolder:/data/hotfolder
- ../../fixtures/ocr:/fixtures/ocr:ro
@ -213,6 +220,9 @@ services:
<<: *service-env
SCAN_HOTFOLDER_MODE: local
SCAN_HOTFOLDER_PATH: /data/hotfolder
# Очистка Output/<job_id>/ и входящего файла после чтения ответа (dev-том принадлежит нам). На шаре контура по
# умолчанию выключена: права на удаление не подтверждены (OQ-144).
SCAN_CLEANUP: "true"
volumes:
- hotfolder:/data/hotfolder
ports:

View File

@ -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,35 @@ 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), а не по имени: шлюз кладёт файл в горячую папку как `<uuid>.<ext>`, и имя фикстуры в нём не сохраняется.
Файл без пары по содержимому получает ответ-ошибку `INVALID_DOCUMENT` («нет пары по содержимому»), если не задан
`SCAN_EMULATOR_DEFAULT` (в compose по умолчанию пуст); при заданном `SCAN_EMULATOR_DEFAULT` такой файл получает набор по
умолчанию. Исключение — PDF из `fixtures/ocr`, для которых нет JSON-набора (`aep_4`, `aep_7`, `aep_9`, `aep_30`–`aep_34`):
они совпадают по хешу, но «нет пары» и **остаются `INVALID_DOCUMENT` даже при заданном default** (OQ-151).
| Файл | Ожидание |
|---|---|
| `aep_3.pdf` | `needs_check`, два документа (`BALANCE`, `INCOME`). В `metrics.warnings` (по эталону `fixtures/ocr/expected_normalize/aep_3.json`): файл — `ИНН распознан неуверенно`; таблица 0 — `шапка синтезирована: колонки без дат`, `строк с нечисловым кодом пропущено: 8`, `строк без кода с суммами пропущено: 2`, повторы кодов 1320/1350/1360/1370; таблица 1 — шапка синтезирована, пропущено 2 и 2; строка `<job_id>_ofr.json: INVALID_DOCUMENT: …` (второй файл набора — ответ-ошибка, штатно; понижает статус до `needs_check`) |
| `aep_6.pdf` | `needs_check`, два документа. Предупреждения: файл — `ИНН не распознан`, `дата отчёта не разобрана`; таблица 0 — пропущено 2 (нечисловой код) и 19 (без кода с суммами), `редакция fsbu4 не заполнена` (форма 2025 года, `fsbu4` пока пуста — OQ-150), повторы кодов 1220/1520; таблица 1 — пропущено 1 и 2, `fsbu4` не заполнена, повтор кода 2410 (OQ-152) |
| `aep_5.PDF`, `aep_8.pdf` | `error`, `INVALID_DOCUMENT` с сообщением ИС (в фикстурах лежат ответы-ошибки) |
| любой другой файл | `error`, `INVALID_DOCUMENT` эмулятора («нет пары по содержимому»), если не задан `SCAN_EMULATOR_DEFAULT`; PDF без JSON-набора — всегда (OQ-151) |
Что смотреть: `raw/<job_id>.json` (склейка всех файлов набора) и `results/<file_id>/<job_id>/result.json` в MinIO; подписи
колонок разобраны в даты, коды строк, суммы со скобками (минус), `is_recognized`, `string_value` только у неразобранных сумм.
`done` на реальных сканах почти не бывает — это ожидаемо. В dev-compose у шлюза `SCAN_CLEANUP=true`: после ответа
`Output/<job_id>/` и входящий файл удаляются. Что не считать багом: строки с мусорным или пустым кодом не попадают в документ, но есть предупреждения
`строк с нечисловым кодом пропущено` / `строк без кода с суммами пропущено` (OQ-149); при повторе кода остаётся первая
строка (OQ-152); минус не поставлен при `}` вместо `)` (OQ-147); `is_recognized = false` у прочерка «—» (OQ-148); сумма
с тремя знаками после запятой (`1,234`) — `amount = null`, текст в `string_value`.
## 7. Консолидация через API (Срез 4)
Предусловия: ядро запущено (раздел 4.2); для сценариев с файлами — ещё `NATS_ENABLED=true` и tabular-parser на `:8001`
@ -598,6 +629,8 @@ Q3-only — `missing_quarters = ["Q1","Q2"]`.
`PROCESSING` в чек-листе ставится по всем недостающим столбцам ЮЛ, пока файл не смаплен (OQ-119, OQ-120, OQ-122).
- Ручное сопоставление контрагента меняет строку ОСВ для всех версий, куда входит документ (OQ-123); рекомендация кандидата — только
при точном совпадении нормализованного имени (OQ-127).
- PDF-путь (Срез 3a): ядро PDF в формы не маппит (Срез 3b); эмулятор отвечает только на PDF из `fixtures/ocr`, остальные —
`INVALID_DOCUMENT` (раздел 6.8, OQ-151); редакция `fsbu4` не заполнена (OQ-150).
- Парсер ОСВ: границы и «не баги» — в конце раздела 6.7 (счёт 91 — OQ-132; скользящий период — OQ-131).
- Удаление алиаса одновременно с его переносом ручным сопоставлением даёт 404 вместо 204 (arm-core #26) — так задумано.

View File

@ -3,16 +3,18 @@
Общий контекст системы — `../CLAUDE.md` и `../docs/`. Этот файл — только про этот сервис.
## Что это
Адаптер внешней ИС Сканирование (Content AI) для PDF/PNG/JPG: переименовать файл `UUID_имя`, положить в «Входящие»,
забрать `Исходящие/UUID/UUID.json`, сохранить сырой ответ в S3 `raw/{job_id}.json` (в БД не регистрируется),
Адаптер внешней ИС Сканирование (Content AI) для PDF/PNG/JPG: положить файл во «Входящие» как `<job_id>.<ext>`
(временный `.part` — в `Tmp`), забрать все `Исходящие/<job_id>/*.json` (по одному json на документ), сохранить склеенный
сырой ответ в S3 `raw/{job_id}.json` (в БД не регистрируется), очистить горячую папку (только при `SCAN_CLEANUP=true`),
нормализовать в `../docs/contracts/recognition-result.schema.json` (`statementDocument`), опубликовать
`recognition.result`. Таймаут 5 мин → `timeout`. **Нет доступа к БД.**
`recognition.result`. Таймаут 5 мин → `TIMEOUT`. **Нет доступа к БД.**
## Правила
- Нормализация по фактическим ответам `../fixtures/ocr/aep_*.json`, а не по ТЗ (OQ-13, OQ-14): confidence → 0–100,
поля организации/даты — по тому, что реально приходит.
- Один PDF может содержать ББ и ОФР → два документа в результате.
- Dev-режим: эмулятор ИС Сканирование, возвращающий `aep_N.json` по имени файла (`SCAN_EMULATOR=1`).
- Один PDF может содержать ББ и ОФР → два документа в результате (по одному на таблицу ответа).
- Dev-режим: эмулятор ИС Сканирование (`python -m src.scan_emulator`) ищет пару PDF + JSON по sha256 содержимого входящего
файла; имя файла на выбор пары не влияет. Промах — набор `SCAN_EMULATOR_DEFAULT`, иначе `INVALID_DOCUMENT`.
## Соглашения шаблона
<!-- заполняет агент -->