docs(context): Срез 3a — правки по ревью (OQ-149, устаревшие описания эмулятора, locator, решения №28–35)

- 07-open-questions: OQ-149 переписан (пустой код у «БАЛАНС»/«Выручка», предупреждения №29, `|` экранирован), новый OQ-152
  (повтор кода 2410 в aep_6), уточнены OQ-137 и OQ-144 (Input)
- 05-domain-rules: §11 locator `file:<имя json>;table:<i>`; §11.1 п. 2 (длина даты №31), п. 4 (предупреждения о пропущенных
  строках), п. 7 (все точки удаляются, Money и confidence №30), п. 8 (ИНН неуверенно), п. 9 (№28), п. 10 (редакция не определена)
- 03-architecture: формулировка про `.part`; 06-test-data, dev/README (сценарии 2/2а), repo-templates/scan-gateway.CLAUDE.md —
  раскладка `<job_id>.<ext>` и пары по sha256
- dev/compose, qa-guide §6.8/§8 (ожидаемые предупреждения по эталонам, OQ-151), 09-delivery-plan (статус 3a)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
Andrey Epifancev 2026-10-07 03:49:05 +04:00
parent a3f33fbe40
commit a2811ebc52
9 changed files with 94 additions and 62 deletions

View File

@ -35,8 +35,9 @@
границы периода ОСВ `period_start/period_end/is_from_year_start`). Правила распознавания границы периода ОСВ `period_start/period_end/is_from_year_start`). Правила распознавания
заголовков/группировок — `rules/*.yaml`. заголовков/группировок — `rules/*.yaml`.
- **scan-gateway** — адаптер ИС Сканирование: положить файл в горячую папку `Input/<job_id>.<ext>` (расширение — - **scan-gateway** — адаптер ИС Сканирование: положить файл в горячую папку `Input/<job_id>.<ext>` (расширение —
суффикс `original_name` в нижнем регистре; временный `.part` пишется в `Tmp`, затем публикуется во входящий каталог суффикс `original_name` в нижнем регистре; временный `.part` пишется в `Tmp`, а не в `Input` (решение владельца
без замены; временный файл остаётся в `Tmp`, в `Input` временных файлов не кладём — решение владельца 06.10.2026), 06.10.2026) и публикуется во входящий каталог без замены; публикация сама снимает `.part` независимо от флага очистки,
в `Tmp` остаются только осиротевшие `.part` и дубли),
дождаться набора `Output/<job_id>/*.json` (по одному json на документ; набор готов, когда имена и размеры не менялись дождаться набора `Output/<job_id>/*.json` (по одному json на документ; набор готов, когда имена и размеры не менялись
`SCAN_SETTLE_SECONDS`), сохранить склеенный сырой ответ в S3 `raw/{job_id}.json` (в БД не регистрируется), очистить `SCAN_SETTLE_SECONDS`), сохранить склеенный сырой ответ в S3 `raw/{job_id}.json` (в БД не регистрируется), очистить
`Output/<job_id>/` и входящий файл — **только под флагом `SCAN_CLEANUP`, по умолчанию выключен** (права на удаление `Output/<job_id>/` и входящий файл — **только под флагом `SCAN_CLEANUP`, по умолчанию выключен** (права на удаление
@ -389,8 +390,8 @@ JetStream** (3 subject `recognition.*`; аутентификация nkeys/JWT,
окружения. Healthcheck `GET /healthcheck` отвечает `503`, если NATS отключён или воркер остановлен — тот же принцип, окружения. Healthcheck `GET /healthcheck` отвечает `503`, если NATS отключён или воркер остановлен — тот же принцип,
что у нас. что у нас.
- Горячие папки ИС Сканирование: эталон — раскладка FileWorker (`Input/<job_id>.<ext>`, `Output/<job_id>/*.json`, - Горячие папки ИС Сканирование: эталон — раскладка FileWorker (`Input/<job_id>.<ext>`, `Output/<job_id>/*.json`,
очистка после чтения). Уточнение 06.10.2026: временный `.part` остаётся в `Tmp` (в `Input` временных файлов не очистка после чтения). Уточнение 06.10.2026: временный `.part` пишется в `Tmp`, а не в `Input` (публикация снимает
класть), очистка — под `SCAN_CLEANUP` (по умолчанию выключена); см. описание scan-gateway выше, OQ-137, OQ-144. его сама, остаются осиротевшие и дубли), очистка — под `SCAN_CLEANUP` (по умолчанию выключена); см. описание scan-gateway выше, OQ-137, OQ-144.
- FileWorker **не** реализует наш контракт DECISION-4: subject'ы `files.*`, конверт `task_id`/`file_id`/`source_s3_key`/ - 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 `expires_at` → `status ready|timeout|error` + `error_code`, без ack `processing`, без `Nats-Msg-Id`, без queue group
(один экземпляр), дедуп только в памяти процесса — доставка at-most-once. Это параллельный сервис старого АРМ, а не (один экземпляр), дедуп только в памяти процесса — доставка at-most-once. Это параллельный сервис старого АРМ, а не

View File

@ -758,7 +758,8 @@ OQ-92 (пустые «Организация»/«Группа компаний»
- `confidence` — **числа 0–100** (в ТЗ — доли 0..1; в реальных файлах проценты) → `numeric(5,2)` как есть. - `confidence` — **числа 0–100** (в ТЗ — доли 0..1; в реальных файлах проценты) → `numeric(5,2)` как есть.
Маппинг (Срез 3): таблица `table_structure[i]` с `label` «Бухгалтерский баланс» / «Отчет о финансовых результатах» → Маппинг (Срез 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`) + (ИНН, имя, units, report_date, okud — из `organization/inn/units/date.form_name`, с низкой уверенностью → `NEEDS_CHECK`) +
`financial_statement_column` (из шапки `rows[0]`, если она есть; `period_date` распарсить из «На 30 июня 2025 г», `is_current` = `financial_statement_column` (из шапки `rows[0]`, если она есть; `period_date` распарсить из «На 30 июня 2025 г», `is_current` =
первая; без шапки — колонки без дат, документ `NEEDS_CHECK`) + `financial_statement_line` (`cells[1]` код, `cells[0]` имя; первая; без шапки — колонки без дат, документ `NEEDS_CHECK`) + `financial_statement_line` (`cells[1]` код, `cells[0]` имя;
@ -791,7 +792,9 @@ OQ-92 (пустые «Организация»/«Группа компаний»
«30/09/25»), иначе цифры склеиваются: 8 цифр → ДДММГГГГ / ГГГГММДД / ГГГГДДММ (первый валидный вариант), 4 → год «30/09/25»), иначе цифры склеиваются: 8 цифр → ДДММГГГГ / ГГГГММДД / ГГГГДДММ (первый валидный вариант), 4 → год
(31.12, только 1990–2100), 6 → ДДММГГ; хвост «0:00:00» отбрасывается. Двузначный год → 20YY. Период квартала/полугодия/ (31.12, только 1990–2100), 6 → ДДММГГ; хвост «0:00:00» отбрасывается. Двузначный год → 20YY. Период квартала/полугодия/
N месяцев → последний день последнего месяца. Не дата (название организации) → `None`. Разборщик общий с §12 п. 4 N месяцев → последний день последнего месяца. Не дата (название организации) → `None`. Разборщик общий с §12 п. 4
(Excel), там на вход приходят чистые строки. (Excel), там на вход приходят чистые строки. **Отличие от источника (решение №31 спеки 3a):** текст длиннее 200
символов не разбирается (`None`, двойники к нему не применяются) — регулярки квартала и полугодия дают
экспоненциальное/кубическое backtracking на длинной строке; настоящая дата занимает десятки символов.
3. **Раскладка колонок** (`_build_layout`, `_infer_layout`): шапка — строка с ячейкой, начинающейся на «код» (после 3. **Раскладка колонок** (`_build_layout`, `_infer_layout`): шапка — строка с ячейкой, начинающейся на «код» (после
нормализации: нижний регистр, всё не-буквенно-цифровое → пробел); наименование — ячейка с «наименование», иначе ближайшая нормализации: нижний регистр, всё не-буквенно-цифровое → пробел); наименование — ячейка с «наименование», иначе ближайшая
заполненная слева от кода; колонки значений — правее кода, где в тексте есть четыре цифры года или текст начинается на заполненная слева от кода; колонки значений — правее кода, где в тексте есть четыре цифры года или текст начинается на
@ -805,23 +808,28 @@ OQ-92 (пустые «Организация»/«Группа компаний»
аналитик подтверждает руками. аналитик подтверждает руками.
4. **Классы строк** (`_classify_row`): пустой код → строка пропускается; нечисловой код → заголовок (не данные); 4. **Классы строк** (`_classify_row`): пустой код → строка пропускается; нечисловой код → заголовок (не данные);
наименование «Баланс» → `BALANCE`, начинается с «итого» → `TOTAL`, иначе обычная строка. Берутся только строки с наименование «Баланс» → `BALANCE`, начинается с «итого» → `TOTAL`, иначе обычная строка. Берутся только строки с
целым кодом (как в §11 выше); расшифровки без кода — OQ-36. Строка с **непустым нечисловым** (`1150 :`, `211(1 |`) или целым кодом (как в §11 выше); расшифровки без кода — OQ-36. Строки, отброшенные классификацией, **не восстанавливаются**
**пустым** кодом пропускается, как в источнике; на момент Среза 3a **предупреждения об этом нет** (строка с суммами, (поведение источника), но о них выдаётся предупреждение уровня таблицы (решение №29 спеки 3a): `строк с нечисловым
но с мусорным кодом, теряется молча и остаётся только в `raw/`) — OQ-149. **Повтор кода** в таблице: остаётся первая кодом пропущено: N` — код непустой и не целое число (`1150 :`); `строк без кода с суммами пропущено: N` — код пуст, но в
строка, остальные не попадают в документ, выдаётся предупреждение «код N повторяется: оставлена первая строка», документ колонках значений есть разобранная сумма (в `aep_3.json` «БАЛАНС» и «Выручка» имеют пустой код, мусор `211(1 |` стоит в
`needs_check` (как §12 п. 5: в БД `financial_statement_line` уникален по `(meta_id, line_code)`; старое ядро дубли не колонке значения; как в Excel-пути, OQ-141). Строки с пустым кодом без сумм (заголовки групп, «в том числе:»)
убирало — решение №15 спеки 3a). предупреждения не дают. Любая такая таблица — `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` → 5. **Тип таблицы** (`_resolve_table_type`): по первой цифре кодов строк данных — больше `1xxx` → BALANCE, больше `2xxx` →
INCOME; это запасной путь, когда `label.form_name` зашумлён; при расхождении с `label` — `needs_check`. INCOME; это запасной путь, когда `label.form_name` зашумлён; при расхождении с `label` — `needs_check`.
6. **Код и наименование строки**: код очищается до букв, цифр, пробела и точки (`_clear_value`: переносы строк и 6. **Код и наименование строки**: код очищается до букв, цифр, пробела и точки (`_clear_value`: переносы строк и
табуляции → пробел, остальное выбрасывается); наименование берётся **из справочника по коду** (`consts.codes_mapping`; табуляции → пробел, остальное выбрасывается); наименование берётся **из справочника по коду** (`consts.codes_mapping`;
у нас — `api/rules/statement_lines.yaml` **scan-gateway** с редакциями форм `66n` / `fsbu4`, см. п. 10, а не у нас — `api/rules/statement_lines.yaml` **scan-gateway** с редакциями форм `66n` / `fsbu4`, см. п. 10, а не
`rules/statement_lines.yaml` из §12 — в tabular-parser такого файла нет), а из OCR — только если кода нет в справочнике `rules/statement_lines.yaml` из §12 — в tabular-parser такого файла нет), а из OCR — только если кода нет в справочнике
выбранной редакции (`line_code_confidence < 100`, не выше 99). Код считается распознанным (`line_code_recognized`), если `confidence == 100`, очищенный текст равен сырому и код выбранной редакции (`line_code_confidence < 100`, не выше 99). Код считается распознанным (`line_code_recognized`), если
есть в справочнике. `confidence == 100`, очищенный текст равен сырому и код есть в справочнике.
7. **Суммы** (`_parse_amount`): запятая → точка, пробелы убираются; содержимое скобок — отрицательное; из строки 7. **Суммы** (`_parse_amount`): запятая → точка, пробелы убираются; содержимое скобок — отрицательное; из строки
удаляется всё, кроме цифр, `+`, `-`, `.`; несколько точек — разделители тысяч, остаётся последняя; `Decimal`, иначе удаляется всё, кроме цифр, `+`, `-`, `.`; если точек больше одной, **удаляются все** (запятая уже стала точкой:
`None`. Прочерк, «–», «—», пусто — пустое значение (NULL, не 0). `string_value` — текст ячейки как распознан `1.234,56` → `123456`, дробная часть не сохраняется, `is_recognized = false`); `Decimal`, иначе `None`. Прочерк, «–», «—», пусто — пустое значение (NULL, не 0). `string_value` — текст ячейки как распознан
(`\n`/`\t` → пробел), **только если сумма не разобрана** (`amount = null`) и ячейка не пуста и не прочерк (DBML: (`\n`/`\t` → пробел), **только если сумма не разобрана** (`amount = null`) и ячейка не пуста и не прочерк (DBML:
«как распознано, если не число»); у разобранных сумм — `null` (решение №16 спеки 3a). «как распознано, если не число»); у разобранных сумм — `null` (решение №16 спеки 3a).
**Перенесено дословно (расхождения с желаемым — OQ-147, OQ-148):** закрывающая скобка, распознанная как `}` или `]` **Перенесено дословно (расхождения с желаемым — OQ-147, OQ-148):** закрывающая скобка, распознанная как `}` или `]`
@ -829,12 +837,17 @@ OQ-92 (пустые «Организация»/«Группа компаний»
«—»/«–» при `confidence = 100` даёт `is_recognized = false` (тире вырезается как «лишний» символ), дефис «-» — `true` «—»/«–» при `confidence = 100` даёт `is_recognized = false` (тире вырезается как «лишний» символ), дефис «-» — `true`
(решение №20). Скобки сами `is_recognized` не портят: признак считается до постановки минуса (решение №13; в источнике (решение №20). Скобки сами `is_recognized` не портят: признак считается до постановки минуса (решение №13; в источнике
любая сумма в скобках получала `false`). любая сумма в скобках получала `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` **и** разбор суммы не потребовал выбрасывать символы (сырой текст без
скобок равен очищенному). Для ячеек шапки: `is_recognized` = `confidence == 100` и в тексте не было мусора; если дата скобок равен очищенному). Для ячеек шапки: `is_recognized` = `confidence == 100` и в тексте не было мусора; если дата
разобралась — `period_text` нормализуется в «На ДД.ММ.ГГГГ», `period_date` — результат п. 2. Ячейки под колонкой разобралась — `period_text` нормализуется в «На ДД.ММ.ГГГГ», `period_date` — результат п. 2. Ячейки под колонкой
заголовка нет — пустое значение с `is_recognized = false`, чтобы не сдвигать столбцы. заголовка нет — пустое значение с `is_recognized = false`, чтобы не сдвигать столбцы.
8. **Шапка документа**: ИНН — только цифры из `inn.form_name`, «распознан», если `confidence == 100` и в сыром тексте не 8. **Шапка документа**: ИНН — только цифры из `inn.form_name`, «распознан», если `confidence == 100` и в сыром тексте не
было ничего, кроме цифр; единицы — `_clear_value`, тот же критерий; дата отчёта — п. 2, «распознана», если было ничего, кроме цифр (предупреждение уровня файла: `ИНН не распознан` — в поле нет ни одной цифры; `ИНН распознан
неуверенно` — цифры извлечены, но `confidence` ≠ 100 или убран мусор); единицы — `_clear_value`, тот же критерий; дата отчёта — п. 2, «распознана», если
`confidence == 100` и дата разобралась. Нераспознанное → `needs_check` (как в §11). `confidence == 100` и дата разобралась. Нераспознанное → `needs_check` (как в §11).
9. **Ошибка разбора одной таблицы** (исключение в разборе `table_structure[i]`) не роняет файл: выдаётся предупреждение 9. **Ошибка разбора одной таблицы** (исключение в разборе `table_structure[i]`) не роняет файл: выдаётся предупреждение
уровня таблицы `BACKEND_PARSING_ERROR: <класс исключения>: <сообщение>`, остальные таблицы файла разбираются; уровня таблицы `BACKEND_PARSING_ERROR: <класс исключения>: <сообщение>`, остальные таблицы файла разбираются;
@ -842,13 +855,15 @@ OQ-92 (пустые «Организация»/«Группа компаний»
ни одной таблицы не разобрано — `error` с кодом `RECOGNITION_FAILED` («ни одной таблицы не разобрано», причины — в ни одной таблицы не разобрано — `error` с кодом `RECOGNITION_FAILED` («ни одной таблицы не разобрано», причины — в
`metrics.warnings`). Ошибочный файл набора ответа при успешном соседе (например, `<job_id>_ofr.json` с `metrics.warnings`). Ошибочный файл набора ответа при успешном соседе (например, `<job_id>_ofr.json` с
`INVALID_DOCUMENT`) документы успешных файлов не отменяет: ошибка попадает в `metrics.warnings` строкой `INVALID_DOCUMENT`) документы успешных файлов не отменяет: ошибка попадает в `metrics.warnings` строкой
`<имя json>: <код ИС>: <сообщение>`, статус результата — не выше `needs_check`, если часть файлов набора ошибочна. `<имя json>: <код ИС>: <сообщение>`, такой файл **понижает статус до `needs_check`** (`done` невозможен, решение №28 спеки 3a: потерянный документ, например ОФР,
не должен остаться незамеченным).
10. **Редакции форм** (`api/rules/statement_lines.yaml`): `66n` (Приказ Минфина № 66н, отчётность по 2024 год включительно) 10. **Редакции форм** (`api/rules/statement_lines.yaml`): `66n` (Приказ Минфина № 66н, отчётность по 2024 год включительно)
и `fsbu4` (ФСБУ 4/2023, с 2025 года; **пока пуста** — состав кодов даёт владелец, OQ-150). Редакция документа выбирается и `fsbu4` (ФСБУ 4/2023, с 2025 года; **пока пуста** — состав кодов даёт владелец, OQ-150). Редакция документа выбирается
**по дате отчёта** шапки, если её нет — по **самой поздней `period_date`** колонок значений, если нет и её — по **по дате отчёта** шапки, если её нет — по **самой поздней `period_date`** колонок значений, если нет и её — по
**числу совпадений кодов** строк со словарями редакций (с предупреждением «редакция определена по кодам»). Имя строки — **числу совпадений кодов** строк со словарями редакций (с предупреждением «редакция определена по кодам»); если и кодов
из словаря выбранной редакции по коду, из OCR — только если кода в нём нет. **Пустая** выбранная редакция → имена строк из не хватает — редакция **не определена**: имена строк из OCR, предупреждение «редакция не определена», `needs_check`.
OCR, одно предупреждение «редакция `<id>` не заполнена», `needs_check`. Предупреждение «код N из редакции X» Имя строки — из словаря выбранной редакции по коду, из OCR — только если кода в нём нет. **Пустая** выбранная
редакция → имена строк из OCR, одно предупреждение «редакция `<id>` не заполнена», `needs_check`. Предупреждение «код N из редакции X»
(код есть в другой редакции, но не в выбранной) выдаётся только для **непустой** выбранной редакции — иначе каждая (код есть в другой редакции, но не в выбранной) выдаётся только для **непустой** выбранной редакции — иначе каждая
строка формы 2025 года давала бы шум (решение №23 спеки 3a). Редакция формы в конверт не передаётся (OQ-145). строка формы 2025 года давала бы шум (решение №23 спеки 3a). Редакция формы в конверт не передаётся (OQ-145).

View File

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

View File

@ -142,21 +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-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-135 | Отчёт 1С «Анализ счета» с отбором по контрагентам группы (`fixtures/analysis-reports/`, в именах файлов «ОСВ_…ПоКомпаниямГруппы»): не ОСВ, но содержит ВГО по корреспонденции счетов. Нужен ли отдельный тип документа для Среза 5, или ВГО считаются только из ОСВ | пакет ДКБ 06.10.2026 | `document_type`, Срез 5 | открыт: допущение — не разбирать; парсер отвечает `INVALID_DOCUMENT` |
| OQ-136 | ИНН организаций пакета ДКБ (Цимлянский, НКЗ, Новоросметалл, Приманыческий, Агрокомплекс) в шапках ОСВ отсутствуют; для теста консолидации по этим группам нужны ИНН и состав ГК/ГСК (выгрузка ЕЦП или список от ДКБ) | пакет ДКБ 06.10.2026 | seed/фикстуры справочников, тесты Среза 4–5 на новых группах | открыт | | 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. **Уточнение 07.10.2026 (Срез 3a, решение владельца 06.10.2026):** временный `.part` остаётся в `Tmp` (в `Input` временных файлов не класть — ИС может подхватить недописанный файл, гарантии игнора `*.tmp` у нас нет; OQ-79 остаётся в силе); очистка `Output/<job_id>/` и входящего файла — под флагом `SCAN_CLEANUP`, по умолчанию выключена (OQ-144) | | 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-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 | открыт: нужен реальный файл без колонки кодов; допущение — не сопоставлять по именам; **связано (Срез 3a):** то же для PDF-пути — справочник кодов по редакциям, имена из OCR при пустой редакции (OQ-146, OQ-150) | | 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-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-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-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-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/<job_id>/`, `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-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-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-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-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-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-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-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-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` — архитектурные решения; Служебные идентификаторы в DBML/аналитике: `ANL-nnn` — замечания аналитика к макету; `ARCH-nnn` — архитектурные решения;
`DECISION-n` — решения по модели; `FIX-nn` — исправления дефектов v4. `DECISION-n` — решения по модели; `FIX-nn` — исправления дефектов v4.

View File

@ -173,7 +173,9 @@ Change'и: `arm-core/openspec/changes/slice-2c-osv-ingest` (issue arm-core#14,
### Срез 3a — scan-gateway: горячие папки и нормализатор OCR (репозиторий scan-gateway) ### Срез 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. **Статус: реализован в 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` - Горячие папки в раскладке FileWorker (OQ-137): `Input/<job_id>.<ext>`, `.part` в `Tmp` и запись без замены, `Output/<job_id>/*.json`
набором (по стабилизации), очистка — под флагом `SCAN_CLEANUP` (по умолчанию выключена, OQ-144); таймаут 5 мин. набором (по стабилизации), очистка — под флагом `SCAN_CLEANUP` (по умолчанию выключена, OQ-144); таймаут 5 мин.
@ -183,7 +185,7 @@ Change `slice-3a-ocr-normalizer` (arm-scan-gateway#6; docs#46, fixtures#12). Р
разрешённое исключение для `reference-old-arm`, решение владельца 06.10.2026) в `statementDocument` v1.2 — по документу на разрешённое исключение для `reference-old-arm`, решение владельца 06.10.2026) в `statementDocument` v1.2 — по документу на
таблицу; справочники `api/rules/` с редакциями форм `66n` / `fsbu4` (`fsbu4` заполняет владелец, OQ-150); `checks` не передаются. таблицу; справочники `api/rules/` с редакциями форм `66n` / `fsbu4` (`fsbu4` заполняет владелец, OQ-150); `checks` не передаются.
- Эталоны нормализатора — `fixtures/ocr/expected_normalize/` (отдельный PR fixtures), `make golden-update`. - Эталоны нормализатора — `fixtures/ocr/expected_normalize/` (отдельный PR fixtures), `make golden-update`.
- Новые вопросы: OQ-144…OQ-151. - Новые вопросы: OQ-144…OQ-152.
### Срез 3b — маппинг форм, проверки, подтверждение (arm-core) ### Срез 3b — маппинг форм, проверки, подтверждение (arm-core)
@ -193,7 +195,7 @@ Change `slice-3a-ocr-normalizer` (arm-scan-gateway#6; docs#46, fixtures#12). Р
- Чек-лист `required_document_rule × периметр − document CONFIRMED` (API + модалка + CSV). - Чек-лист `required_document_rule × периметр − document CONFIRMED` (API + модалка + CSV).
- UI: экран «Проверка распознавания» (исходник слева — PDF.js, результат справа, блок «Логические ошибки»). - UI: экран «Проверка распознавания» (исходник слева — PDF.js, результат справа, блок «Логические ошибки»).
- Тесты: пары `aep_N.pdf/json`; машиночитаемые PDF АО РБ 2024 как dev-путь без OCR (опционально pdfplumber). - Тесты: пары `aep_N.pdf/json`; машиночитаемые PDF АО РБ 2024 как dev-путь без OCR (опционально pdfplumber).
- Открытые вопросы для 3b: редакция формы в контракте (OQ-145), словари §7/§12 по редакциям (OQ-146), строки с нечисловым кодом (OQ-149). - Открытые вопросы для 3b: редакция формы в контракте (OQ-145), словари §7/§12 по редакциям (OQ-146), строки с нечисловым кодом (OQ-149), повтор кода (OQ-152).
## Срез 4 — Консолидация: карточка, версии, периметр, периоды, сводная ОСВ ## Срез 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` — ответы-ошибки Код `INVALID_DOCUMENT` здесь — **настоящий ответ ИС из фикстуры**: `aep_5.json` и `aep_8_form4.json` — ответы-ошибки
ИС Сканирование («Не удалось определить тип документа.», пустой `table_structure`); у `aep_3` и `aep_6` есть успешные ИС Сканирование («Не удалось определить тип документа.», пустой `table_structure`); у `aep_3` и `aep_6` есть успешные
ответы (сценарий 2а). Эмулятор нашёл набор ответов (в логе `scan-emulator`: `<job_id>_aep_5.PDF -> ответы (сценарий 2а). С Среза 3a шлюз кладёт файл во входящую папку как `<uuid>.<ext>` (расширение — из `original_name` в
/data/hotfolder/Output/<job_id>`; при отсутствии набора сообщение было бы «…: нет эталонного ответа в фикстурах»). нижнем регистре), а эмулятор находит пару **по sha256 содержимого**: имя и регистр расширения файла значения не имеют.
Успешные ответы ИС есть (`aep_3.json` + `aep_3_ofr.json`, `aep_6.json`); шлюз склеивает все `*.json` каталога ответа и В логе `scan-emulator`: `Эмулятор: <job_id>.pdf -> /data/hotfolder/Output/<job_id> (пара: hash)`; для файла без пары ответ —
отвечает `NOT_IMPLEMENTED` (нормализация — Срез 3), ошибочные файлы набора — в `metrics.warnings`. Имя файла — `aep_5.PDF` (расширение в верхнем регистре; на macOS `aep_5.pdf` тоже находится, на `INVALID_DOCUMENT` с текстом `<первые 12 символов sha256>: нет пары по содержимому в фикстурах` (в профиле `services`
Linux — нет). `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 с) после `processing` виден ~10 с: задержка эмулятора 2 с, затем до `SCAN_POLL_SECONDS + SCAN_SETTLE_SECONDS` (5 + 2 с) после
первого опроса, увидевшего набор. первого опроса, увидевшего набор.
**2а. `image` → успешный ответ ИС → `NOT_IMPLEMENTED`:** **2а. `image` → успешный ответ ИС → `needs_check` с документами (Срез 3a):**
```bash ```bash
new_job image PDF ocr/aep_3.pdf && poll 60 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'" 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 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`:** **3. Сервис недоступен → три повторные публикации → `dlq` / `NO_ACK`:**
```bash ```bash
@ -270,9 +274,9 @@ docker compose --profile services up -d --wait tabular-parser # вернут
перезапускается (replay — вне Среза 0). перезапускается (replay — вне Среза 0).
Что сквозная проверка **не** покрывает (Срез 0): успешные результаты (`done`/`needs_check`, регистрация файла Что сквозная проверка **не** покрывает (Срез 0): успешные результаты (`done`/`needs_check`, регистрация файла
`results/…` в `s3_object`) — заглушка парсера и нормализатор шлюза Среза 0 (`NOT_IMPLEMENTED`) дают только ошибки, `results/…` в `s3_object`) — заглушка парсера Среза 0 (`NOT_IMPLEMENTED`) даёт только ошибки; `scan-gateway` с Среза 3a
хотя фикстуры ИС (`aep_3`, `aep_6`) успешные ответы содержат; `stored_file.status` остаётся публикует и успешные результаты (сценарий 2а), но их приём ядром (`done`/`needs_check`) здесь не проверяется;
`UPLOADED`, `current_recognition_job_id` не заполняется (маппинг результата — `NoopMapper`, Срез 2); у ошибок `stored_file.status` остаётся `UPLOADED`, `current_recognition_job_id` не заполняется (маппинг результата — `NoopMapper`, Срез 2); у ошибок
`scan-gateway` в `recognition_job.metrics` только `warnings` — по строке `<имя файла>: <код>: <сообщение>` на ошибочный `scan-gateway` в `recognition_job.metrics` только `warnings` — по строке `<имя файла>: <код>: <сообщение>` на ошибочный
файл ответа ИС, если такие есть (`tabular-parser` присылает `duration_ms`). Тестовые строки и объекты остаются в БД и файл ответа ИС, если такие есть (`tabular-parser` присылает `duration_ms`). Тестовые строки и объекты остаются в БД и
MinIO; полная очистка — `docker compose --profile services down -v` (удаляет и общие тома `postgres-data`/`minio-data` MinIO; полная очистка — `docker compose --profile services down -v` (удаляет и общие тома `postgres-data`/`minio-data`

View File

@ -183,8 +183,9 @@ services:
# том hotfolder с шлюзом. Переменные MinIO/NATS не нужны. # том hotfolder с шлюзом. Переменные MinIO/NATS не нужны.
# Фикстуры ответов ИС — ../../fixtures/ocr (только чтение). Входной файл шлюз называет <uuid>.<ext> (имя файла # Фикстуры ответов ИС — ../../fixtures/ocr (только чтение). Входной файл шлюз называет <uuid>.<ext> (имя файла
# фикстуры не несёт); эмулятор считает sha256 содержимого и ищет пару PDF + набор <имя>.json / <имя>_*.json по хешу # фикстуры не несёт); эмулятор считает sha256 содержимого и ищет пару PDF + набор <имя>.json / <имя>_*.json по хешу
# (aep_3.pdf, aep_6.pdf, ...). Любое другое содержимое — ответ-ошибка INVALID_DOCUMENT, пока SCAN_EMULATOR_DEFAULT пуст; # (aep_3.pdf, aep_6.pdf, ...). Файл без пары по содержимому — ответ-ошибка INVALID_DOCUMENT, пока SCAN_EMULATOR_DEFAULT
# чтобы любой файл получал набор по умолчанию, задайте SCAN_EMULATOR_DEFAULT=<имя набора>, например aep_6. # пуст; чтобы такой файл получал набор по умолчанию, задайте SCAN_EMULATOR_DEFAULT=<имя набора>, например aep_6.
# PDF из fixtures/ocr без JSON-набора (aep_4/7/9/30–34) остаются INVALID_DOCUMENT и при заданном default (OQ-151).
scan-emulator: scan-emulator:
profiles: ["services"] profiles: ["services"]
image: scan-gateway:dev image: scan-gateway:dev
@ -196,6 +197,7 @@ services:
SCAN_FIXTURES_DIR: /fixtures/ocr SCAN_FIXTURES_DIR: /fixtures/ocr
SCAN_EMULATOR_DELAY: "2" SCAN_EMULATOR_DELAY: "2"
# Набор по умолчанию для файлов без пары по содержимому. Пусто — такой файл получает INVALID_DOCUMENT. # Набор по умолчанию для файлов без пары по содержимому. Пусто — такой файл получает INVALID_DOCUMENT.
# Файл, совпавший по хешу с PDF без JSON-набора, default не получает (OQ-151).
SCAN_EMULATOR_DEFAULT: "" SCAN_EMULATOR_DEFAULT: ""
volumes: volumes:
- hotfolder:/data/hotfolder - hotfolder:/data/hotfolder

View File

@ -440,21 +440,25 @@ UI/API ядра PDF-путь пока не проверяется. Сервис
Что загружать на стенде: **PDF из `fixtures/ocr` — `aep_3.pdf` и `aep_6.pdf`**. Эмулятор узнаёт их **по содержимому** Что загружать на стенде: **PDF из `fixtures/ocr` — `aep_3.pdf` и `aep_6.pdf`**. Эмулятор узнаёт их **по содержимому**
(sha256), а не по имени: шлюз кладёт файл в горячую папку как `<uuid>.<ext>`, и имя фикстуры в нём не сохраняется. (sha256), а не по имени: шлюз кладёт файл в горячую папку как `<uuid>.<ext>`, и имя фикстуры в нём не сохраняется.
Любой другой PDF даёт ответ-ошибку `INVALID_DOCUMENT` («нет пары по содержимому»), если не задан `SCAN_EMULATOR_DEFAULT` Файл без пары по содержимому получает ответ-ошибку `INVALID_DOCUMENT` («нет пары по содержимому»), если не задан
(в compose по умолчанию пуст). `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` — предупреждения нормализации и строка `<job_id>_ofr.json: INVALID_DOCUMENT: …` (второй файл набора — ответ-ошибка, штатно) | | `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`, два документа; в предупреждениях «редакция fsbu4 не заполнена» (форма 2025 года, редакция `fsbu4` пока пуста — OQ-150) | | `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` с сообщением ИС (в фикстурах лежат ответы-ошибки) | | `aep_5.PDF`, `aep_8.pdf` | `error`, `INVALID_DOCUMENT` с сообщением ИС (в фикстурах лежат ответы-ошибки) |
| любой другой 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; подписи Что смотреть: `raw/<job_id>.json` (склейка всех файлов набора) и `results/<file_id>/<job_id>/result.json` в MinIO; подписи
колонок разобраны в даты, коды строк, суммы со скобками (минус), `is_recognized`, `string_value` только у неразобранных сумм. колонок разобраны в даты, коды строк, суммы со скобками (минус), `is_recognized`, `string_value` только у неразобранных сумм.
`done` на реальных сканах почти не бывает — это ожидаемо. В dev-compose у шлюза `SCAN_CLEANUP=true`: после ответа `done` на реальных сканах почти не бывает — это ожидаемо. В dev-compose у шлюза `SCAN_CLEANUP=true`: после ответа
`Output/<job_id>/` и входящий файл удаляются. Что не считать багом: потеря строки с мусорным кодом (OQ-149), минус не поставлен `Output/<job_id>/` и входящий файл удаляются. Что не считать багом: строки с мусорным или пустым кодом не попадают в документ, но есть предупреждения
при `}` вместо `)` (OQ-147), `is_recognized = false` у прочерка «—» (OQ-148). `строк с нечисловым кодом пропущено` / `строк без кода с суммами пропущено` (OQ-149); при повторе кода остаётся первая
строка (OQ-152); минус не поставлен при `}` вместо `)` (OQ-147); `is_recognized = false` у прочерка «—» (OQ-148); сумма
с тремя знаками после запятой (`1,234`) — `amount = null`, текст в `string_value`.
## 7. Консолидация через API (Срез 4) ## 7. Консолидация через API (Срез 4)
@ -625,9 +629,9 @@ Q3-only — `missing_quarters = ["Q1","Q2"]`.
`PROCESSING` в чек-листе ставится по всем недостающим столбцам ЮЛ, пока файл не смаплен (OQ-119, OQ-120, OQ-122). `PROCESSING` в чек-листе ставится по всем недостающим столбцам ЮЛ, пока файл не смаплен (OQ-119, OQ-120, OQ-122).
- Ручное сопоставление контрагента меняет строку ОСВ для всех версий, куда входит документ (OQ-123); рекомендация кандидата — только - Ручное сопоставление контрагента меняет строку ОСВ для всех версий, куда входит документ (OQ-123); рекомендация кандидата — только
при точном совпадении нормализованного имени (OQ-127). при точном совпадении нормализованного имени (OQ-127).
- PDF-путь (Срез 3a): ядро PDF в формы не маппит (Срез 3b); эмулятор отвечает только на PDF из `fixtures/ocr`, остальные — - PDF-путь (Срез 3a): ядро PDF в формы не маппит (Срез 3b); эмулятор отвечает только на PDF из `fixtures/ocr`, остальные —
`INVALID_DOCUMENT` (раздел 6.8); редакция `fsbu4` не заполнена (OQ-150). `INVALID_DOCUMENT` (раздел 6.8, OQ-151); редакция `fsbu4` не заполнена (OQ-150).
- Парсер ОСВ: границы и «не баги» — в конце раздела 6.7 (счёт 91 — OQ-132; скользящий период — OQ-131). - Парсер ОСВ: границы и «не баги» — в конце раздела 6.7 (счёт 91 — OQ-132; скользящий период — OQ-131).
- Удаление алиаса одновременно с его переносом ручным сопоставлением даёт 404 вместо 204 (arm-core #26) — так задумано. - Удаление алиаса одновременно с его переносом ручным сопоставлением даёт 404 вместо 204 (arm-core #26) — так задумано.
- Код `60.АВ` (синтетика s09) и буквенные забалансовые вида `РВ.1` в план счетов не резолвятся (arm-core #28, C23-1/C23-2). - Код `60.АВ` (синтетика s09) и буквенные забалансовые вида `РВ.1` в план счетов не резолвятся (arm-core #28, C23-1/C23-2).

View File

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