Спеки применены: 5 новых capability (arm-core, arm-web, scan-gateway, statement-normalization, tabular-recognition-service) с Purpose, изменения quality-checks и recognition-contract. Ссылки в 05-domain-rules, DBML v5.19, entities, dev/checks.md и dev/qa-statements-v2.md ведут в архив. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
98 KiB
statement-normalization Specification
Purpose
Общий алгоритм нормализации форм ББ/ОФР (шаги 2–8) в пакете arm_statements, одинаковый для tabular-parser, scan-gateway и пересчёта в arm-core: колонки кодов и периодов, статусы строк, разбор сумм, код и редакция формы, проверки §7, признак «требует внимания», предупреждения и статус документа; копирование пакета в хосты скриптом sync-statements.
Requirements
Requirement: Пакет общего кода arm_statements и его копии
Общий код нормализации ББ/ОФР SHALL жить в репозитории arm-statements пакетом arm_statements/ с pyproject,
своими тестами и конфигом линтеров. Пакет SHALL содержать: типы шага 1 (FormCandidate, Cell; сериализуемые в
json dataclass-ы), шаги 2–8, общие разборы текста (двойники, даты, суммы, коды), словари — проверок §7
statement_checks.yaml (полный и упрощённый наборы), периодов statement_periods.yaml, заголовков форм и ОКУД,
подписей колонок («Код», «Наименование», исключения), букв-двойников, единиц измерения statement_units.yaml (написания
и коды ОКЕИ), упорядоченные перечни кодов форм по редакциям (66n, fsbu4) и вариантам (full, simplified) без
наименований — байтовая копия docs/contracts/statement-code-orders.yaml как rules/statement_code_orders.yaml,
диапазоны кодов — и загрузчик словарей. Словаря
«код → наименование» в пакете MUST NOT быть (Р-3). Пакет SHALL владеть своими типами входа и выхода; конвертацию
выхода в модель контракта делает хост. Golden-тесты пакета — внутри пакета на синтетических и обезличенных данных;
тесты на реальных фикстурах остаются в сервисах и соседние репозитории не читают.
В tabular-parser, scan-gateway и arm-core пакет SHALL лежать копией вместе с тестами и конфигом линтеров и
импортироваться по имени arm_statements; раскладку, проверку свежести и маркер версии задаёт требование
«sync-statements — копии пакета в потребителях».
Scenario: Наименований в пакете нет
- WHEN ревьюер ищет в пакете соответствие «код → наименование»
- THEN его нет: в перечне кодов только коды, наименования строк — словарь ядра
Requirement: sync-statements — копии пакета в потребителях
Копия пакета в хосте (arm-core, tabular-parser, scan-gateway) SHALL лежать в api/arm_statements/ (рядом с
api/src/, импорт import arm_statements) вместе с тестами пакета (api/arm_statements/tests/); конфиг линтеров пакета в копию не кладётся (применяется конфиг
хоста); ruff и pytest хоста SHALL охватывать копию (тесты пакета — в testpaths), mypy хоста — запускаться и по
ней (mypy src arm_statements, у arm-core — mypy src scripts arm_statements). Маркер версии — файл
api/arm_statements/VERSION с полным хэшем коммита репозитория arm-statements, из которого разложена копия; его пишет
только скрипт, сервис читает при старте и пишет в результат как service.statements_version. Прямые правки копии MUST
NOT делаться.
Копию SHALL раскладывать и проверять скрипт docs/scripts/sync-statements.sh (по образцу sync-web.sh; цель make -C docs sync-statements). Источник — локальный checkout arm-statements рядом с workspace или --source <dir>, ссылка —
origin/main или --ref; хост — --host <имя> или --host-dir <dir>. Без --check скрипт раскладывает копию коммита
--ref в новый worktree хоста на ветке фичи, пишет VERSION и готовит issue и PR (как sync-web.sh). С --in-place скрипт SHALL раскладывать
копию коммита --ref в рабочее дерево указанного хоста (существующий worktree) без issue, коммита и PR — для
подключения хоста и для PR фичи, которой нужна новая версия пакета; при прямой правке копии или недостающей зависимости —
отказ с кодом 20, ничего не пишется. Копия
сравнивается с деревом arm_statements/ источника без файла VERSION (его в источнике нет). --check SHALL
возвращать: 0 — копия равна дереву arm_statements/ коммита из VERSION, это коммит --ref, и копия перечня кодов
в пакете равна docs/contracts/statement-code-orders.yaml; 10 — копия равна дереву коммита из VERSION, но в
--ref есть более новый (копия отстала), или копия перечня кодов отличается от файла docs (перечень в docs правился
раньше arm-statements или пересинхронизации хоста — предупреждение называет файл); 20 — копия отличается от дерева
коммита из VERSION (прямая правка), VERSION нет в истории источника, зависимости пакета (список dependencies
его pyproject) отсутствуют в зависимостях хоста или (только у arm-core) код словаря имён
api/rules/statement_lines.yaml отсутствует в перечне своей редакции копии пакета; 2 — нет checkout источника
или хоста, неизвестный аргумент. Файлы и коды расхождения SHALL печататься. Порядок правки перечня кодов: PR в docs →
PR в arm-statements (байтовая копия yaml) → sync-statements в хостах «сначала ядро»; до конца этой цепочки хосты
получают 10, а не 20.
Код пакета SHALL проходить самый строгий конфиг ruff и mypy среди хостов (пакет проверяется им в своём
репозитории): конфиг хоста применяется к копии, а правка копии под хост запрещена. Тесты пакета SHALL не зависеть от
фикстур и conftest хоста: в api/arm_statements/tests/ свой conftest.py, а conftest хоста MUST NOT содержать
autouse-фикстур без ограничения каталогом tests/ хоста. Зависимости пакета — только те, что есть в зеркале банка
(сейчас PyYAML); хост держит их в своих зависимостях с версией зеркала.
Цель statements-check хоста (sync-statements.sh --check --host-dir ., docs — ../docs или ARM_KA_DOCS) SHALL
входить в make check-full (коды 0 и 10 — зелёная, 10 с предупреждением «копия отстала», вписывается в описание PR;
20 и 2 — красная) и MUST NOT входить в make check и пайплайн платформы.
Scenario: Прямая правка копии
- WHEN в
tabular-parserизменён файл копии пакета, а источник вarm-statements— нет - THEN
sync-statements --checkзавершается с кодом 20 и называет файл;make check-fullкрасный;make checkсервиса этого не проверяет
Scenario: Копия отстала
- WHEN копия в
scan-gatewayравна коммитуabc1234, а вarm-statementsorigin/mainужеdef5678 - THEN
--checkзавершается с кодом 10,make check-fullзелёный с предупреждением «копия отстала»
Scenario: Нет checkout источника
- WHEN рядом нет
arm-statementsи--sourceне задан - THEN
--checkзавершается с кодом 2 и подсказкой,make check-fullкрасный
Scenario: Перечень кодов в docs новее копии
- WHEN в docs в
statement-code-orders.yamlдобавлен код, аarm-statementsи копии в хостах ещё не обновлены - THEN
--checkв каждом хосте завершается с кодом 10 и называет файл перечня;make check-fullзелёный с предупреждением, посторонние PR в сервисы не краснеют
Scenario: Код словаря ядра вне перечня
- WHEN в
arm-core/api/rules/statement_lines.yamlредакцииfsbu4есть код 1120, а в перечнеfsbu4его нет - THEN
--checkвarm-coreзавершается с кодом 20 и называет код и редакцию
Scenario: Копия в PR фичи
- WHEN PR scan-gateway нужна версия пакета новее, чем в
mainхоста - THEN
sync-statements.sh --in-place --host-dir .в worktree фичи раскладывает копию и пишетVERSION, копия коммитится отдельным коммитом этого PR;--checkпосле раскладки —0
Scenario: Версия общего кода в результате
- WHEN сервис собирает результат копией пакета, у которой в
VERSIONкоммитabc1234репозиторияarm-statements - THEN в результате
service.statements_version = "abc1234"
Requirement: Шаг 1 — кандидат в форму
Каждый адаптер шага 1 SHALL выдавать на файл список FormCandidate, и только с ним работают шаги 2–8. Поля
кандидата: source (ocr | excel); locator (file:<имя json>;table:<i> у OCR, sheet:<A>[,<B>…] у Excel);
type_hint (BALANCE | INCOME | DISCLOSURE | null); evidence (ОКУД и заголовок формы как Cell); header
(поля organization_name, inn, report_date, units, okud, okpo, okopf, okfs, okei, address — каждое
Cell или null); rows — плотная сетка строк × ячеек, все страницы и листы формы уже в одной сетке, пустая позиция —
null; segment_breaks — номера строк сетки, с которых начинается новый лист Excel (у OCR пусто); warnings.
Cell SHALL быть одной для шапки и таблицы: text — как показывает источник; value — число или дата, если
источник их знает (у OCR всегда null; у Excel — значение по числовому формату ячейки, tp#42); confidence 0–100
(у Excel всегда 100); ref — позиция в источнике (OCR — row:<row_index ИС>, Excel — <лист>!<A1> по правилу Excel:
имя листа в одинарных кавычках, если в нём есть что-то кроме букв, цифр и _ или оно начинается с цифры, апостроф внутри
имени удваивается).
type_hint SHALL вычисляться одним правилом у обоих адаптеров: ОКУД по словарю (0710001 → BALANCE, 0710002 →
INCOME, прочие 0710xxx → DISCLOSURE) → иначе заголовок по словарю заголовков (у OCR заголовок — label таблицы
ИС; «Пояснения», «Расшифровка» → DISCLOSURE) → иначе null.
Scenario: Excel-ячейка с числовым форматом
- WHEN в Excel ячейка хранит 106311116.46 с форматом
#,##0, - THEN у
Celltext = "106 311",value = 106311,confidence = 100,ref = "Баланс!D27"(лист и адрес ячейки)
Scenario: Имя листа с пробелом
- WHEN код строки стоит в ячейке C14 листа «Бухгалтерский баланс. Лист 1»
- THEN
ref = "'Бухгалтерский баланс. Лист 1'!C14"
Scenario: OCR-ячейка
- WHEN в таблице ИС ячейка строки
row_index = 8содержит"[190 :"с уверенностью 25 - THEN у
Celltext = "[190 :",value = null,confidence = 25,ref = "row:8"
Scenario: Тип по ОКУД против заголовка
- WHEN у блока формы ОКУД 0710002, а заголовок «Бухгалтерский баланс»
- THEN
type_hint = INCOME; окончательный тип выбирается по кодам на шаге 6
Requirement: Шаг 1 — шапка кандидата
У каждого кандидата SHALL быть своя шапка. Текст каждого поля разбирается общим кодом по Cell.text: ИНН — только
цифры, без замены двойников, длина 10 или 12, иначе null; дата отчёта — словарём периодов (тот же, что для подписей
колонок, шаг 2); единицы — словарём единиц statement_units.yaml по тексту, а если по тексту не найдены — по коду ОКЕИ
тем же словарём (384 → «тыс. руб.», 383 → «руб.», 385 → «млн руб.»); ОКЕИ — только цифры. Сырой текст и уверенность
каждого поля SHALL доезжать до контракта (<поле>_text, <поле>_confidence).
Scenario: ИНН с мусором клеток
- WHEN поле ИНН шапки OCR —
"3|1 21 2|"с цифрами ИНН между вертикальными чертами - THEN ИНН собирается только из цифр,
|единицей не считается;inn_textхранит исходный текст
Scenario: Единицы только кодом ОКЕИ
- WHEN в шапке Excel-формы нет текста единиц, а в блоке «Коды» стоит ОКЕИ 384
- THEN
units = "тыс. руб.",okei = "384", предупрежденияUNITS_NOT_FOUNDнет
Requirement: Очистка OCR-текста — первая стадия типовых разборов
Очистка OCR SHALL выполняться не отдельным шагом, а первой стадией разборов даты, подписи колонки, кода строки,
разлепления склеенной строки и суммы — для любой ячейки без value (у Excel — только текстовые ячейки). Буквы-
двойники SHALL заменяться в датах, подписях, кодах и суммах (Р-11): О/о/O/o → 0, З/з → 3, б → 6, S/s → 5,
I/l/|/і → 1, Ч → 4 — только вплотную к цифре, в несколько проходов; слова не портятся. В суммах крайние |
(след границы таблицы) SHALL убираться, а не заменяться. ИНН двойники MUST NOT трогать. Сумма с заменой двойника
получает is_recognized = false; код с заменой считается неуверенным. Текст длиннее 200 символов разбором даты не
обрабатывается (null).
Scenario: Двойник в сумме
- WHEN ячейка суммы OCR —
"1 2О5"с уверенностью 100 - THEN
amount = 1205,is_recognized = false,text = "1 2О5"
Scenario: Двойник в коде
- WHEN ячейка кода —
"I1О0"с уверенностью 100 - THEN код разбирается как
1100и считается неуверенным (подтверждается структурой, шаг 8)
Scenario: Граница таблицы в сумме
- WHEN ячейка суммы —
"| : 122477" - THEN крайняя
|удаляется, а не становится единицей; сумма 122477,is_recognized = false
Requirement: Шаг 1 — многостраничная форма
Требование относится к адаптеру Excel: у OCR каждая таблица ИС — отдельный кандидат, склейки таблиц нет (повторная
шапка внутри таблицы ИС — сегмент шага 2). У Excel одна форма SHALL быть одной сеткой на выходе шага 1. Строки продолжения дописываются в конец сетки по маркеру
«Форма … с.2» или по позиции листа; граница листа записывается в segment_breaks. Тип из маркера продолжения SHALL
сверяться с type_hint формы: расхождение — отдельный кандидат и предупреждение CONTINUATION_TYPE_CONFLICT
(error). Лист без маркера и без шапки присоединяется, только если его коды в диапазоне открытой формы и, при трёх и
более кодах, число колонок сумм совпадает с формой; иначе — отдельный кандидат и предупреждение
SHEET_NOT_JOINED (error).
Scenario: Лист-продолжение с одной строкой
- WHEN после листа ОФР идёт лист без шапки с единственной строкой 2400
- THEN лист присоединяется к ОФР (код в диапазоне формы, кодов меньше трёх — число колонок не сверяется), граница
листа — в
segment_breaks
Scenario: Лист другой ширины с тремя кодами
- WHEN лист без маркера и шапки содержит три кода ББ и четыре колонки сумм при трёх у открытой формы
- THEN лист — отдельный кандидат, предупреждение
SHEET_NOT_JOINEDуровняerror
Requirement: Диапазоны кодов и разбор кода
Словарь диапазонов кодов SHALL содержать отрезки: ББ 1100–1799, ОФР 2100–2999, отчёт об изменениях капитала
3100–3699, отчёт о движении денежных средств 4100–4499, пояснения 5000–5999, отчёт о целевом использовании
6100–6499. Кодовые диапазоны для поиска столбца — объединение отрезков. Разбор кода: убрать пробелы и переносы →
заменить двойники (| → 1 только если результат попадает в кодовый диапазон) → убрать краевую пунктуацию → 4–5 цифр
(допускается хвост .0), первые четыре — в кодовом диапазоне. Многострочная ячейка — код, только если каждая её часть
код. «Диапазон столбца» — отрезок, в который попадает большинство кодов найденного столбца.
Scenario: Год и сумма — не код
- WHEN в ячейке «2024» или четырёхзначная сумма «1050» или «7450»
- THEN кодом это не считается (вне отрезков); «3450» — код (отрезок 3100–3699 отчёта об изменениях капитала)
Scenario: Краевая пунктуация и черта
- WHEN ячейка кода —
"1220 ,"или"| 140" - THEN коды
1220и1140
Requirement: Шаг 2 — столбец кодов
Столбец кодов SHALL находиться одним правилом: (а) столбец с подписью из словаря подписей кода («код», «коды», «код
строки», «код показателя» — после нормализации регистра и пунктуации) над первой строкой с кодом, если под подписью
доля кодов среди непустых ячеек ≥ 0,6 или кодов ≥ 3; при нескольких кандидатах — с наибольшим числом кодов; берётся
последняя такая подпись над первой строкой с кодом; (б) иначе — столбец с наибольшим числом ячеек-кодов при кодах ≥ 3
и доле ≥ 0,6; иначе предупреждение CODE_COLUMN_NOT_FOUND.
Scenario: Титульный блок 1С с подписью «Коды»
- WHEN под подписью «Коды» титульного блока выгрузки 1С стоят ОКУД, дата, ОКПО, ИНН, ОКОПФ 12267, ОКФС, ОКЕИ (один код из девяти, доля 0,11), а коды строк формы — в другом столбце
- THEN столбец под «Коды» не выбирается; столбец кодов — по правилу (б)
Scenario: Документ из двух строк
- WHEN форма содержит две строки с кодами под подписью «Код»
- THEN столбец кодов найден (два из двух, доля 1,0)
Requirement: Шаг 2 — сегменты, повторная шапка и границы листов
Раскладка SHALL состоять из сегментов. Новый сегмент открывает строка-шапка (в ней есть ячейка из словаря подписей
кода, или не менее двух её непустых ячеек совпадают после нормализации с ячейками шапки первого сегмента) или граница
листа из segment_breaks. У сегмента своя раскладка: по его шапке; без шапки — столбец кодов по правилу шага 2, а если
сегмент не первый и кодов в нём меньше трёх — ослабленный поиск: столбец с наибольшим числом кодов из диапазона
столбца первого сегмента, минимум один. Колонки документа (columns[]) SHALL задаваться первым сегментом; колонки
любого следующего сегмента новых columns[] не создают, а сопоставляются с колонками первого сегмента:
- сегмент со своей шапкой (повторная шапка OCR, лист-продолжение Excel со своей строкой заголовка) — по
period_date, если у каждой колонки сегмента дата есть и совпадает с датой ровно одной колонки первого сегмента; иначе по порядку; - сегмент без шапки — колонки значений — столбцы правее кода, где в строках с кодами есть числа, сопоставление по порядку.
Значения сегмента несут column_index сопоставленной колонки первого сегмента. При сопоставлении по порядку число
колонок сегмента не совпало с первым — предупреждение SEGMENT_COLUMNS_MISMATCH (error): значения по позиции в
пределах меньшего числа колонок,
лишние столбцы сегмента в columns[] не попадают, их номера в сетке перечислены в message. Столбец кодов не найден ни
так, ни так — предупреждение SEGMENT_WITHOUT_CODES (error) сегмента; строки сегмента не теряются (роль data,
статусы по шагу 3).
Scenario: Лист-продолжение НПЗ со своей шапкой
- WHEN у ББ НПЗ («Бухгалтерский баланс. Лист 4») своя строка заголовка, колонки периодов — в столбцах 8–10, у первого листа — в столбцах 16, 21, 24, даты подписей те же (30.09.2025, 31.12.2024, 31.12.2023)
- THEN новых колонок нет: значения листа 4 получают
column_index16, 21, 24 по совпадениюperiod_date; предупреждения нет, §7 ББ считается по одной колонке на дату, документ остаётсяdone
Scenario: Повторная шапка с другими подписями
- WHEN повторная шапка второй страницы скана содержит две колонки без разобранной даты, у первого сегмента — две колонки с датами
- THEN сопоставление по порядку, значения несут номера колонок первого сегмента, предупреждения нет
Scenario: Сегмент шире первого
- WHEN у сегмента со своей шапкой четыре колонки периодов, у первого — три
- THEN
SEGMENT_COLUMNS_MISMATCHуровняerror, значения первых трёх — по позиции, номер четвёртого столбца — вmessage
Scenario: Лист «_2» НПЗ со столбцом кодов в другой колонке
- WHEN второй лист формы без шапки содержит единственную строку 2400, и код стоит не в той колонке, что на первом листе
- THEN у сегмента свой столбец кодов (ослабленный поиск), значения строки 2400 не сдвинуты
Scenario: Повторная шапка на второй странице скана
- WHEN в таблице ИС посередине стоит повторная строка «Наименование показателя | Код | На 30.09.2025 | …»
- THEN с неё начинается новый сегмент со своей раскладкой; строка-шапка в данные не попадает
Requirement: Шаг 2 — строка заголовка, нумерация и роли строк
Строка заголовка SHALL быть последней над первой строкой с кодом, где в столбце кодов стоит подпись кода; иначе
последней над первой строкой с кодом с двумя и более подписями-периодами правее кода (или одной, не голым годом);
двухстрочный заголовок склеивается. Строка нумерации SHALL получать роль numbering и в данные не попадать; строка —
нумерация, если в ней не меньше трёх непустых ячеек, каждая после замены букв-двойников — целое число, и числа слева
направо возрастают на 1, начинаются с 1 или 2 и допускают не больше одного пропуска. Роль
title — только строки выше найденной строки заголовка; в сегменте без заголовка роли title нет. Заголовка нет ни у
одного сегмента документа → колонки значений первого сегмента — столбцы правее кода, где в строках с кодами числа,
label = "", period_date = null, period_kind = none, одно предупреждение HEADER_ROW_NOT_FOUND (error).
Заголовок есть хотя бы у одного сегмента — остальные сегменты берут его колонки без предупреждения.
Scenario: Нумерация в скане (AURORA-1517)
- WHEN под шапкой таблицы ИС строка «1 | 2 | 3 | 4 | 5»
- THEN роль
numbering, строки данных из неё нет
Scenario: Нумерация с двойником
- WHEN строка нумерации скана распознана как «1 | 2 | З | 4»
- THEN «З» читается как 3, роль
numbering; строкиunreadableс «суммами» 3 и 4 нет
Scenario: Нумерация без первой ячейки
- WHEN ИС потеряла первую ячейку нумерации: «2 | 3 | 4 | 5»
- THEN роль
numbering(начало с 2 допускается)
Scenario: Нумерация с пропуском
- WHEN строка «1 | 2 | 4 | 5» (тройка не распознана)
- THEN роль
numbering(один пропуск допускается); «1 | 3 | 5» — не нумерация
Scenario: Нумерация в Excel со «2» в столбце кодов
- WHEN в форме Межениновской под шапкой строка «1 | 2 | 3 | 4 | 5», где «2» стоит в столбце кодов
- THEN роль
numbering; «2» кодом не считается
Scenario: Листы 2–5 ББ без шапки
- WHEN у первого листа ББ заголовок есть, у листов 2–5 — нет
- THEN листы 2–5 берут колонки первого, предупреждения о заголовке нет, документ может быть
done
Requirement: Шаг 2 — подпись и дата колонки
Подпись колонки SHALL разбираться одним словарём периодов statement_periods.yaml, объединяющим словарь
tabular-parser и шаблоны dates.py scan-gateway; строки test_dates.py::test_parses_real_forms scan-gateway —
обязательные тесты словаря. Результат — period_date (конец периода колонки) и period_kind:
- абсолютная подпись («На 31 марта 2025 г.», «31.12.2024», «30/09/25», «30092025», «За 9 месяцев 2025 г.», «с
01.01.2025 по 31.03.2025», «2024», хвост «0:00:00», клетки «2|0|2|4|») →
absolute; - относительная: «На отчётную дату», «За отчётный период» →
relative:current(= эффективная дата); «На 31 декабря предыдущего года» →relative:prev_year_end(31.12 года эффективной даты − 1); «На 31 декабря года, предшествующего предыдущему» →relative:prev_prev_year_end(31.12 года − 2); «За аналогичный период предыдущего года» →relative:prev_period(эффективная дата − 1 год, 29 февраля → 28 февраля); - подписи нет →
none.
Эффективная дата документа SHALL быть датой отчёта из шапки, иначе самой поздней абсолютной датой колонок; нет ни
той, ни другой → у относительных колонок period_date = null, period_kind остаётся относительным.
Подпись из словаря исключений («Изменение», «Отклонение», «%», «Темп», «Доля», «Примечание») SHALL пропускаться с
предупреждением COLUMN_SKIPPED (info). Подпись, не разобранная ни как период, ни как исключение, при числах в
столбце в строках с кодами SHALL оставлять колонку с подписью как есть, period_date = null, period_kind = none и
предупреждением COLUMN_WITHOUT_DATE (error, column_index); без чисел — пропуск. Подпись OCR с уверенностью ниже
100, разобранная в дату, подтверждается, если дата входит в ожидаемые: эффективная дата, она же минус один и минус два
года, 31 декабря предыдущего и предшествующего ему года; иначе предупреждение COLUMN_DATE_UNCERTAIN (error,
column_index). Уверенность подписи — label_confidence (у Excel 100; подписи нет — null).
Если среди колонок значений сегмента есть хотя бы одна с подписью периода (абсолютной или относительной), столбец без
текста подписи правее последней такой колонки SHALL НЕ считаться колонкой сумм: его числа не попадают в значения
строк, COLUMN_WITHOUT_DATE не выдаётся, вместо этого выдаётся одно предупреждение NUMBERS_OUTSIDE_TABLE (info) с
адресами ячеек, собранными по всем сегментам формы (в тексте — первые 10, дальше «и ещё N»). Предупреждение SHALL НЕ
влиять на статус документа. Решение владельца 18.10.2026 (Р-44). Правило общее для Excel и OCR и живёт в пакете.
У сегмента без подписанных колонок (лист-продолжение без шапки) столбцы без текста подписи сверх числа колонок документа
SHALL считаться числами правее таблицы. Подписанный столбец без чисел в строках с кодами границей таблицы не служит.
Адрес ячейки — <лист>!<A1> у Excel и row:<n> у OCR. Без подписанных колонок, а также для столбца без подписи между
подписанными и для столбца с любым текстом подписи, не разобранным как период или исключение, поведение прежнее (COLUMN_WITHOUT_DATE).
Подтверждение SHALL опираться только на уверенные источники, чтобы неуверенное не подтверждало само себя: для
неуверенной подписи эффективная дата — дата отчёта с уверенностью 100, иначе самая поздняя абсолютная дата колонок с
уверенной подписью (label_confidence = 100); такой нет — подпись не подтверждена. Неуверенная дата отчёта (шаг 8, поля
шапки) подтверждается только самой поздней абсолютной датой колонок с уверенной подписью. Для периода документа и
относительных подписей эффективная дата считается, как выше, по всем источникам.
Scenario: Бланк 66н без впечатанных дат
- WHEN подписи колонок ББ — «На отчетную дату», «На 31 декабря предыдущего года», «На 31 декабря года, предшествующего предыдущему», дата отчёта шапки 30.09.2025
- THEN
period_dateколонок 2025-09-30, 2024-12-31, 2023-12-31,period_kind—relative:current,relative:prev_year_end,relative:prev_prev_year_end; предупреждений о дате колонок нет
Scenario: Неуверенная подпись не подтверждает сама себя
- WHEN даты отчёта в шапке нет, единственная колонка с абсолютной датой — подпись OCR «На 30.09.2О25» с уверенностью 60, остальные колонки без подписи
- THEN эффективная дата для подтверждения строится без этой подписи — уверенных источников нет, подпись не
подтверждена:
COLUMN_DATE_UNCERTAINуровняerror; период документа (period_known) по-прежнему определяется этой датой
Scenario: Мусорная подпись над числами
- WHEN подпись колонки —
"| | г"с уверенностью 17, а в колонке суммы - THEN колонка остаётся,
period_date = null,period_kind = none, предупреждениеCOLUMN_WITHOUT_DATEуровняerrorсcolumn_index, документneeds_check
Scenario: Числа справа от таблицы
- WHEN у ББ подписи периодов стоят над столбцами M, Q, S, а правее таблицы в строках с кодами лежат формулы аналитика (V45, W57, U58, V58, V72, W72) над пустой шапкой (Межениновская 2025-Q3)
- THEN колонок значений три, числа U–W в значения не попадают,
COLUMN_WITHOUT_DATEиSEGMENT_COLUMNS_MISMATCHнет; одно предупреждениеNUMBERS_OUTSIDE_TABLEуровняinfoс адресами этих ячеек; документdone, если нет других причин
Scenario: Столбец без подписи между подписанными
- WHEN между колонками с подписями периода стоит столбец без подписи с числами
- THEN он остаётся колонкой с
COLUMN_WITHOUT_DATEуровняerror,NUMBERS_OUTSIDE_TABLEнет
Scenario: Подписанных колонок нет
- WHEN ни у одного столбца значений нет подписи периода
- THEN правило «числа справа от таблицы» не применяется, столбцы с числами — колонки с
COLUMN_WITHOUT_DATE
Scenario: Колонка «Изменение»
- WHEN правее периодов колонка «Изменение, %»
- THEN колонка пропускается, предупреждение
COLUMN_SKIPPEDуровняinfo
Requirement: Шаг 2 — номер колонки
column_index колонки значений (columns[], values[], предупреждения) SHALL быть номером столбца в сетке кандидата
шага 1: у OCR — column_index ИС как есть (не обязательно подряд), у Excel — номер столбца листа первого сегмента с 0.
Номер уникален в документе; колонки сегментов без шапки сопоставляются с колонками первого сегмента по порядку и несут их
номера. Предупреждение о столбце, которого нет среди columns[] (COLUMN_SKIPPED), SHALL нести номер этого столбца в
сетке.
Scenario: Разреженные колонки ИС
- WHEN в таблице ИС код стоит в колонке 1, суммы — в колонках 2, 4, 5 (колонки 3 нет)
- THEN
columns[].column_index— 2, 4, 5, у значений — те же номера
Scenario: Пропущенная колонка Excel
- WHEN на листе Excel коды в столбце C, периоды в D и E, «Изменение, %» в F
- THEN
columns[].column_index— 3 и 4, предупреждениеCOLUMN_SKIPPEDсcolumn_index = 5
Requirement: Шаг 2 — столбец наименования
Столбец наименования SHALL быть столбцом с подписью, начинающейся с «наименование» (после нормализации), в строке заголовка; иначе — столбцом левее кода с наибольшим числом текстов, кроме столбца «Пояснения». Наименование строки — ячейка этого столбца; пустая — ближайшая текстовая ячейка левее кода, кроме «Пояснения».
Scenario: Наименование без подписи
- WHEN у формы нет подписи «Наименование показателя», а левее кода два столбца — «Пояснения» (короткие номера) и тексты статей
- THEN наименование берётся из столбца текстов статей
Requirement: Шаг 3 — строки данных и их статусы
Шаг 3 SHALL выдавать RawLine на каждую строку сетки с ролью data, у которой есть содержимое: в ячейке кода есть
хотя бы одна цифра (разобранный код или нет), или хотя бы одна ячейка значений даёт сумму (amount не null), или
непуста с уверенностью ниже 100, или её текст — «цифра под видом буквы»: после замены двойников без условия «вплотную к
цифре» в нём есть цифра и нет других букв ('I', '| О'). Строка только с текстом наименования — не строка данных;
текст из настоящих букв в колонке значений при уверенности 100 (подписи «Руководитель … Косарев», «(расшифровка
подписи)» под формой Excel) содержимым не считается — осознанное исключение из «ничего не выбрасывать молча». У строки — row_index (порядковый номер в документе с 0,
по порядку сетки после разлепления), ref (уникален в документе: OCR row:<row_index ИС>, разлепленные
row:<i>#<k> с k от 1; Excel — адрес ячейки столбца кодов строки <лист>!<A1> по правилу шага 1, без столбца кодов — первой непустой
ячейки строки), ячейка кода, line_status, ячейка наименования, ячейки значений по колонкам сегмента, parent_ref.
Статусы:
ok— код разобран;unreadable— в ячейке кода есть хотя бы одна цифра, но код не разобран; ячейка кода без цифр (в том числе только из букв-двойников вне соседства с цифрой: «изо :», «}», «:») считается пустой;detail— строка без кода с содержимым, над которой (в том же или в предыдущих сегментах документа) есть строкаok;parent_ref—refближайшей строкиokвыше (строкиunreadable/missingмежду ними пропускаются);missing— строка без кода с содержимым, над которой во всём документе нет строкиok; строки ролиtitleсюда не попадают.
Строка без кода и без содержимого — не строка данных. Счётчиков отброшенных строк MUST NOT быть. Повтор кода — не
статус и не предупреждение: все строки с кодом — ok, порядок как в файле.
Scenario: Код-мусор (AURORA-1462)
- WHEN ячейка кода —
"ИЧ,250", в колонках суммы - THEN строка
unreadableсline_code_text = "ИЧ,250", суммами иref; строка не пропадает
Scenario: Мусор в ячейке кода без значений
- WHEN в ББ aep_6 ячейка кода —
"44 30\nI |", ячейки значений пусты - THEN строка
unreadableбез значений: документneeds_check(не все строкиok/detail), подтверждение она не блокирует (LINES_UNRESOLVED— только строки со значениями)
Scenario: Расшифровка «в том числе»
- WHEN под строкой 1150 стоит строка «в том числе: Оборудование к установке» без кода с суммой
- THEN строка
detail,parent_ref=refстроки 1150
Scenario: Расшифровка под строкой-мусором
- WHEN порядок строк: 1150 (
ok), «ИЧ,250» (unreadable), строка без кода с суммой - THEN последняя —
detailсparent_refстроки 1150 (ближайшая строкаok,unreadableпропускается)
Scenario: Код из букв-двойников без цифр (aep_3)
- WHEN ячейка кода —
"изо :", в колонке значений'3'с уверенностью 0, выше есть строкиok - THEN цифр в ячейке кода нет — она пустая, строка
detail; если это потерянная строка 1130, её сумма не войдёт в 1100, и соотношение1100 = …подсветит раздел (цена правила, S-61)
Scenario: Цифра под видом буквы (aep_6)
- WHEN в строке без кода под строкой
okединственная непустая ячейка значений —'I'с уверенностью 100 - THEN строка данных
detail(раньше терялась молча); значениеamount = null,string_value = "I",is_recognized = false
Scenario: Потерянный код в начале таблицы
- WHEN первая строка данных «Выручка» без кода с суммами (код уехал в ячейку значения), выше строк с кодом нет
- THEN строка
missing
Scenario: Повтор кода (AURORA-1406)
- WHEN код 2410 стоит у двух строк с разными наименованиями
- THEN обе строки
ok, порядок как в файле, предупреждения нет
Requirement: Шаг 3 — разлепление склеенных строк
До разбора кода SHALL выполняться разлепление: ячейка кода содержит N ≥ 2 частей через перевод строки, и каждая
непустая ячейка значений — ровно N частей; разлепление делается, если хотя бы одна часть кода разобралась как код
диапазона столбца. Получается N строк с ref row:<i>#<k>; нечитаемая часть кода — строка unreadable; наименование —
k-я из непустых частей, если их тоже N, иначе целиком у каждой; line_code_text и text значений — k-е части. Значения разлепленных строк получают
is_recognized = false; разлепление — предупреждение ROW_SPLIT (info). Число частей не совпало или ни одна часть
не код — одна строка, многострочное значение → amount = null, string_value = текст, is_recognized = false.
Scenario: Склейка «1500\n1700» (OQ-156)
- WHEN ячейка кода —
"1500\n1700", суммы —"917 775\n2 371 333" - THEN две строки
ok(row:i#1,row:i#2) с суммами 917775 и 2371333,is_recognized = false, предупреждениеROW_SPLIT; при сошедшихся соотношениях §7 значения не подсвечиваются (Р-17)
Scenario: Часть не из диапазона
- WHEN в ББ ячейка кода —
"44 30\nI |" - THEN строка не разлепляется (4430 вне диапазона ББ)
Requirement: Шаг 4 — значение ячейки
Шаг 4 SHALL выдавать StatementValue {column_index, amount, string_value, text, confidence, is_recognized}: text —
всегда сырой текст источника; string_value — только когда amount = null и текст непуст и не прочерк. Правила:
value— число:amountквантуется до 2 знаков half-up; больше 16 целых цифр →amount = null,string_value= текст,is_recognized = false, предупреждениеVALUE_OUT_OF_RANGE(info);value— дата или логическое →amount = null,string_value= текст,is_recognized = false;valueнет →parse_amount(text, source)по стадиям:- пусто или прочерк (
-,–,—,−без цифр) →amount = null,is_recognized = true; многострочное значение в строке с однострочным кодом →amount = null,string_value= текст,is_recognized = false; - крайние
|(след границы таблицы) удаляются до замены двойников, а не становятся единицей ("|122477"→ 122477); затем буквы-двойники вплотную к цифре → цифры (Р-11); и то и другое — исправление (is_recognized = false); - ядро числа — участок от первого из символов «цифра,
(,-,−,+» до последнего из «цифра,),},]»; всё вне ядра — краевой мусор (|,:,=,■,•, кавычки, точки,{,[и любые другие символы): выбрасывается, а если среди выброшенного есть что-то кроме пробелов —is_recognized = false; - в ядре допустимы только цифры, пробелы (в том числе NBSP и тонкие),
,,., знак (-,−как минус,+) в начале и одна пара скобок вокруг всего числа:(первым символом и),}или]последним (}/]— исправление);{и[открывающей скобкой не считаются. Иначе — буква в ядре (после стадии 2), скобка без пары или не на краю ядра, знак не в начале, любой другой символ между цифрами (:,/,=,|) —amount = null,string_value= текст,is_recognized = false; - цифр нет вовсе →
amount = null,string_value= текст,is_recognized = false; - скобки → минус; разделители: у
ocrзапятая — тысячи при шаблоне[+-]?[1-9]\d{0,2}(,\d{3})+, точка — тысячи при[+-]?[1-9]\d*(\.\d{3})+, иначе десятичный разделитель, три и более знаков после него →amount = null,string_value= текст; уexcelзапятая и точка — десятичный разделитель; пробел допустим только как разделитель тысяч (группы ровно по три цифры), иначеamount— цифры подряд («лучшая догадка», не длиннее 16 цифр, иначеnull) иis_recognized = false;
- пусто или прочерк (
is_recognized = true— текст разобран как число без выброшенных и исправленных символов (замена двойника,}или]вместо скобки — исправление);confidence— от источника и вis_recognizedне входит;- позиции нет, текст пуст или прочерк →
amount = null,string_value = null,is_recognized = true,confidenceячейки (позиции нет — 100).
parse_amount — единственное место шагов 2–8, где разбор зависит от source. Разбор сумм ОСВ этим правилом MUST NOT
затрагиваться.
Scenario: Потерянная цифра (AURORA-1511)
- WHEN ячейка OCR —
"42831 1"с уверенностью 100 - THEN
amount = 428311,is_recognized = false,text = "42831 1"
Scenario: Скобка } (OQ-147)
- WHEN ячейка OCR —
"(7744536}" - THEN
amount = -7744536,is_recognized = false
Scenario: Скобка внутри цифр (aep_3)
- WHEN ячейка OCR —
"211(1 |" - THEN крайняя
|— краевой мусор; в ядре211(1скобка без пары и не на краю →amount = null,string_value = "211(1 |",is_recognized = false
Scenario: Буква в ядре числа (aep_6)
- WHEN ячейка OCR —
"(Ю4)" - THEN «Ю» — не двойник и стоит в ядре →
amount = null,string_value = "(Ю4)",is_recognized = false(не −4)
Scenario: Краевой мусор с двух сторон
- WHEN ячейка OCR —
": : 2856983 = ■"или"| : 122477" - THEN
amount = 2856983иamount = 122477,is_recognized = false(выброшен мусор вне ядра)
Scenario: Фигурная скобка не открывает
- WHEN ячейка OCR —
"{38378)" - THEN
{— краевой мусор, в ядре38378)закрывающая скобка без пары →amount = null,string_value = "{38378)",is_recognized = false
Scenario: Запятая по источнику
- WHEN текст
"4,200" - THEN у
ocramount = 4200; уexcelamount = 4.20
Scenario: Длинное тире (OQ-148, закрыт решением владельца 18.10.2026)
- WHEN ячейка OCR —
"—"с уверенностью 100 - THEN
amount = null,string_value = null,is_recognized = true
Requirement: Шаг 4 — единицы и дробные значения
При единицах «тыс. руб.» и доле значений с дробной частью ≥ 10 % SHALL выдаваться предупреждение документа
FRACTIONAL_IN_THOUSANDS (info); то же сочетание в книге xlsb без числовых форматов — XLSB_SCALE_SUSPECT
(error). Книга xlsb без числовых форматов сама по себе — XLSB_NO_FORMATS (info); формат ячейки не разобран —
NUMBER_FORMAT_UNPARSED (info, одно на документ с перечнем форматов).
Scenario: xlsb с копейками при «тыс. руб.»
- WHEN книга xlsb без стилей, в шапке «тыс. руб.», 40 % значений дробные
- THEN предупреждение
XLSB_SCALE_SUSPECTуровняerror, документneeds_check
Requirement: Шаг 5 — строка формы
Шаг 5 SHALL выдавать StatementLine {row_index, ref, line_status, line_code, line_code_text, line_code_confidence, line_name, line_name_confidence, parent_ref, needs_review, values}: line_code — разобранный код (4–5 цифр) или
null у unreadable / missing / detail; line_code_text — сырой текст ячейки кода; line_code_confidence — от
источника, у кода с заменой двойника — не выше 99; line_name — распознанный текст наименования как есть, с
уверенностью источника. Сервисы MUST NOT подставлять наименование из справочника и MUST NOT срезать уверенность кода
по справочнику (Р-3). Редакция (66n / fsbu4) в сервисах нужна только для перечня кодов (шаг 8): проверяются обе
редакции, эффективная дата (fsbu4 при дате после 2024-12-31) разрешает только равенство.
Scenario: Наименование из OCR (AURORA-1522)
- WHEN у строки 1230 OCR-наименование «Дебиторская задолжен-ность»
- THEN
line_name = "Дебиторская задолжен-ность"с уверенностью ячейки; справочное имя показывает ядро
Requirement: Шаг 6 — тип формы, вариант и документ
Шаг 6 SHALL собирать StatementDocument {document_type, form_variant, locator, header, columns, lines}. Тип —
большинство кодов строк ok в отрезке ББ → BALANCE, ОФР → INCOME, прочих форм и пояснений → DISCLOSURE;
совпало с type_hint или type_hint = null — тип по кодам; противоречит — тип по кодам и предупреждение
TYPE_CONFLICT (error); кодов нет — type_hint. Поиск столбца кодов (шаг 2) SHALL выполняться всегда, в том числе
при type_hint = DISCLOSURE: если большинство кодов в отрезке ББ или ОФР, тип — по кодам и TYPE_CONFLICT. Без столбца
кодов: type_hint есть → документ без строк (DISCLOSURE — done, иначе needs_check с CODE_COLUMN_NOT_FOUND
уровня error); type_hint нет → у ocr таблица ИС с содержимым — предупреждение файла TABLE_LOST (error,
locator таблицы), таблица ИС без содержимого — предупреждение файла TABLE_SKIPPED (info, locator таблицы);
у excel — лист «прочее», предупреждение файла SHEET_OTHER (info, locator = sheet:<имя>); документа во всех
этих случаях нет. Содержимое таблицы ИС — хотя бы одна ячейка с цифрой или с «цифрой под видом буквы» (шаг 3); таблица
только из текста (подписи, пояснительный текст) содержимого не имеет. У DISCLOSURE (по type_hint без кодов форм ББ/ОФР или по кодам прочих форм) шаги 3–7
не выполняются: columns и lines — пустые массивы, checks нет, документ done.
form_variant SHALL определяться только по структуре кодов всех строк ok документа (независимо от уверенности
кода; у ядра — заново при каждом пересчёте после правки, точка входа ниже): ББ — есть 1600 и нет ни 1100, ни 1200 → simplified;
ОФР — есть 2400 или 2110 и нет ни 2100, ни 2200, ни 2300 → simplified; иначе full; у DISCLOSURE — null. ОКУД
признаком варианта не служит. Шапка документа — поля header кандидата: text → <поле>_text, разобранное → поле,
confidence → <поле>_confidence.
Scenario: Упрощённый баланс
- WHEN в ББ есть 1150, 1210, 1600, 1310, 1700 и нет 1100 и 1200
- THEN
form_variant = simplified; шаг 7 и перечень кодов берут упрощённый набор
Scenario: «Пояснения» с кодами баланса
- WHEN ИС подписала таблицу «Пояснения к бухгалтерскому балансу» (
type_hint = DISCLOSURE), а в столбце кодов — 1110…1700 - THEN столбец кодов найден,
document_type = BALANCE, предупреждениеTYPE_CONFLICTуровняerror; данные не выброшены
Scenario: Лист «прочее»
- WHEN в книге Excel лист «Примечания» без заголовка формы, ОКУД и кодов
- THEN документа нет; в
metrics.warnings—SHEET_OTHERуровняinfoсlocator = "sheet:Примечания"
Scenario: Заголовок спорит с кодами
- WHEN
type_hint = BALANCE, а коды строк — 2110, 2120, 2100, 2200 - THEN
document_type = INCOME, предупреждениеTYPE_CONFLICTуровняerror
Requirement: Шаг 7 — самопроверки §7
Шаг 7 SHALL считаться у обоих источников одним словарём statement_checks.yaml и выдавать StatementChecks {relations_failed[], soft_relations_failed[]}; каждое нарушение — {column_index, relation, codes, expected, actual}, где codes — коды цели и членов соотношения. Правила:
- значение кода в колонке — сумма по всем строкам
okс этим кодом; строкиdetail,unreadable,missingв соотношения не входят; нарушение относится ко всем строкам его кодов; - соотношение посчитано, если непуста цель или хотя бы один член (есть строка
okс кодом иamountнеnull); пусто всё — соотношение пропускается; пустая цель при непустых членах и непустая цель при пустых членах — нарушение; - жёсткое соотношение не сошлось, если расхождение больше допуска ±1 в единицах формы и больше 0,001 % наибольшего
по модулю числа соотношения — тогда оно в
relations_failed; расхождение в пределах округления и мягкие соотношения — вsoft_relations_failed; - члены
abs_terms(2120, 2210, 2220, 2330, 2350, 2410) сравниваются и как записаны, и по модулю; такое соотношение подтверждает величину, но не знак (шаг 8); - набор соотношений — по
form_variant(полный или упрощённый);form_variant = null— полный. В упрощённом наборе соотношения ББ жёсткие, а2400 = 2110 − 2120 − 2330 + 2340 − 2350 − 2410ОФР — мягкое, как2400полного набора (знак 2410, OQ-12); - родитель пятизначной подстроки — ближайшая четырёхзначная строка выше с теми же первыми тремя цифрами, иначе код из первых четырёх цифр; «сумма подстрок = родитель» (мягкое) — только если хотя бы одна подстрока в колонке непуста.
checks SHALL быть у каждого BALANCE / INCOME со строками и отсутствовать у документа без строк.
duplicate_line_codes MUST NOT выдаваться.
Scenario: «БАЛАНС» без кода
- WHEN строка «БАЛАНС» ушла в
detail(код потерян), в колонке есть 1100 и 1200 - THEN соотношение
1600 = 1100 + 1200посчитано и нарушено (пустая цель при непустых членах), вrelations_failed
Scenario: Лист только с активом
- WHEN в ББ заполнены 1100, 1200, 1600, а 1300–1700 отсутствуют
- THEN
1600 = 1700нарушено (непустая цель при пустых членах), документneeds_check
Scenario: Повтор кода в соотношении
- WHEN две строки 2410 с суммами 100 и 649
- THEN в соотношениях код 2410 равен 749
Scenario: Упрощённый ОФР с доходом по налогу
- WHEN у упрощённого ОФР 2410 записан со знаком дохода, и
2400 = 2110 − 2120 − 2330 + 2340 − 2350 − 2410по модулю не сходится - THEN нарушение в
soft_relations_failed, документ из-за него неneeds_check
Scenario: Округление итогов
- WHEN
1600 = 1700расходится на 9,02 при итогах ~17 млрд - THEN нарушение в
soft_relations_failed, на статус и подсветку не влияет
Requirement: Шаг 8 — needs_review значения
Сервис SHALL вычислять needs_review у каждого значения строк ok, unreadable и missing. У значений строк
detail needs_review = false всегда (Р-23): у них нет кода, покрыть их соотношением нельзя, в проверки §7, агрегаты и
статус документа они не входят; сомнение остаётся видно в подсказке ячейки по text и confidence. Для остальных
значений:
- значение нуждается в покрытии, если
confidence < 100илиis_recognized = false; чистые значения в покрытии не нуждаются; - значение покрыто, если
amountзначения неnullи в его колонке есть соотношение с участием его кода, которое посчитано и сошлось: жёсткое — в пределах допуска или округления; мягкое — только в пределах допуска ±1, без «округления» (Р-21; так покрываются 2400, 2410, 2411, 2412, 2460, 25xx и пятизначные подстроки — у них жёстких соотношений нет); - знак: у члена
abs_terms, нуждающегося в покрытии, знак подтверждён, если форма записи знака совпадает с формой строгого большинства остальных членовabs_termsэтой колонки с непустымamount,is_recognized = trueи без правки аналитика (сама ячейка в голосование не входит); форма знака — по результату разбораparse_amount: «скобки» (пара скобок, в том числе с}/]), «минус», «без знака»; голосующих нет или равенство голосов — знак не подтверждён; needs_review = true, если значение нуждается в покрытии и не покрыто или его знак не подтверждён; а также у всех ячеек жёсткого соотношения, не сошедшегося вне допуска и округления, независимо от уверенности;- иначе
needs_review = false.
Scenario: Догадка подтверждена соотношением (Р-7)
- WHEN у 1230
"42831 1"→ 428311,is_recognized = false, и1200 = 1210 + 1220 + 1230 + …в колонке посчитано и сошлось - THEN
needs_review = false;textи уверенность остаются в значении
Scenario: Знак расхода в фигурной скобке
- WHEN в колонке ОФР 2210 и 2220 записаны в скобках, а 2120 —
"(7744536}", и2100 = 2110 − 2120сошлось - THEN у 2120 знак подтверждён,
needs_review = false
Scenario: Знак вне конвенции
- WHEN 2210 и 2220 в скобках, а сомнительная 2120 —
"38378"с уверенностью 60 (форма «без знака»), и2100 = 2110 − 2120сходится по модулю - THEN
needs_review = trueу 2120 независимо от соотношения
Scenario: Скобка, которая не открывает
- WHEN 2120 —
"{38378)" - THEN разбор даёт
amount = null(шаг 4), значение не покрыто,needs_review = true
Scenario: Расшифровки не держат документ (Р-23, РБ 1 кв. 2025)
- WHEN в ББ и ОФР РБ «Бухгалтерская отчетность 1 кв.25» все неуверенные значения строк с кодом покрыты сошедшимися
соотношениями, неуверенные коды подтверждены перечнем
fsbu4, а неуверенные значения остались только у строкdetail(в ОФР —'1 425 130'с уверенностью 78,'134 796'с уверенностью 71,'(15 567)'с уверенностью 75) - THEN у значений
detailneeds_review = false, они статус не держат; документ получаетdone, если выполнены остальные условия функции статуса (шапка, период, предупреждения); подсказка у ячеекdetailпоказываетtextи уверенность
Scenario: Справочная строка без проверки
- WHEN значение 2900 с уверенностью 80 (строк 29xx нет ни в одном соотношении)
- THEN
needs_review = true
Scenario: Чистая прибыль подтверждена мягким соотношением (Р-21)
- WHEN значение 2400 с уверенностью 66, а
2400 = 2300 − 2410 + 2420в колонке сошлось с расхождением 0 - THEN у 2400
needs_review = false
Scenario: Мягкое соотношение в пределах округления не подтверждает
- WHEN значение 2400 с уверенностью 66, а
2400 = 2300 − 2410 + 2420расходится на 9 при итогах ~17 млрд (в пределах 0,001 %, но больше ±1) - THEN у 2400
needs_review = true; нарушение вsoft_relations_failed
Requirement: Шаг 8 — подтверждение неуверенного кода структурой
Код строки SHALL считаться неуверенным, если line_code_confidence < 100 или при разборе заменён двойник; только к
неуверенным кодам применяется структурная проверка (Р-12). Данные — упорядоченные перечни кодов по редакциям (66n,
fsbu4) и вариантам (full, simplified), источник — docs/contracts/statement-code-orders.yaml; законные повторы
кода (Р-20) — часть перечня (repeats: у fsbu4 1220, 1520, 2410 печатной формы 1С; у 66n — раздел III НКО).
Четырёхзначные коды строк ok документа образуют последовательность; коды, которых нет в перечне редакции, в
последовательность этой редакции не входят. Для обеих редакций берётся лучшая подпоследовательность, согласованная
с порядком перечня варианта формы (итоги после слагаемых; код с повтором может войти столько раз, сколько он стоит в
перечне); «лучшая» — лексикографически: сначала наибольшее число уверенных кодов (уверенные коды — якоря: включаются
все, какие можно включить вместе), затем наибольшая длина. Якоря, противоречащие друг другу по порядку бланка, все
вместе войти не могут — входит наибольшее согласованное их множество; неуверенный код проверяется против выбранного.
Редакция — с лучшей подпоследовательностью (то же сравнение), при равенстве — по эффективной дате (fsbu4 после
2024-12-31). Неуверенный код подтверждён, только если он входит во все лучшие подпоследовательности выбранной
редакции. Вариант формы для перечня — form_variant документа (шаг 6, по всем строкам ok). Пятизначный код подтверждён, если
подтверждён его родитель и подстрока стоит после него. Повтор кода, которого нет в перечне редакции (или копий больше,
чем в перечне), у которого хотя бы одна копия неуверенна, — needs_review = true у неуверенной копии (Р-16); повтор из
перечня подтверждается структурой, как любой код. Неподтверждённый неуверенный код — StatementLine.needs_review = true. У Excel и чистых OCR-кодов needs_review строки по структуре MUST NOT ставиться.
Scenario: Неуверенный код на своём месте
- WHEN код 1230 с уверенностью 50 стоит между 1220 и 1240 (уверенными)
- THEN код подтверждён,
needs_review = false
Scenario: Переставленные неуверенные соседи
- WHEN порядок кодов 1210, 1230 (уверенность 50), 1220 (уверенность 50), 1240
- THEN у обеих неуверенных строк
needs_review = true, документneeds_check
Scenario: Якоря противоречат друг другу
- WHEN порядок кодов 1210 (уверенный), 1240 (уверенный), 1230 (уверенный), 1250 (уверенность 50)
- THEN 1240 и 1230 вместе войти не могут; лучшие подпоследовательности — 1210, 1240, 1250 и 1210, 1230, 1250; код 1250 входит в обе и подтверждён
Scenario: Законный повтор 1220 (Р-20, AURORA-1431)
- WHEN в ББ печатной формы 1С по ФСБУ 4/2023 стоят подряд 1215, 1220 «НДС по приобретенным ценностям» (уверенность 100), 1220 «НДС, начисленный по отгрузке» (уверенность 50), 1230
- THEN обе строки 1220 подтверждены структурой (повтор есть в перечне
fsbu4),needs_review = false
Scenario: Повтор вне перечня
- WHEN в ББ код 1230 стоит дважды подряд, у второй копии уверенность 50
- THEN у второй копии
needs_review = true, документneeds_check
Scenario: Бланк 66н в 2025 году
- WHEN скан ББ за 3 кв. 2025 на бланке 66н с неуверенным кодом 1120 между 1110 и 1130
- THEN проверяются обе редакции, подпоследовательность
66nдлиннее, код 1120 подтверждён
Scenario: Нестандартный код Excel
- WHEN в Excel-форме код 1231 или пятизначная подстрока 11101 вне перечня
- THEN
needs_reviewстроки не ставится; проверка «код не из редакции» —infoядра
Requirement: Шаг 8 — поля шапки
Дата отчёта с уверенностью ниже 100 SHALL подтверждаться совпадением с самой поздней абсолютной датой колонок с
уверенной подписью (label_confidence = 100; неуверенная подпись дату отчёта не подтверждает, требование «Шаг 2 —
подпись и дата колонки»); иначе
поле report_date попадает в needs_review_fields и выдаётся REPORT_DATE_UNCERTAIN (error, field = report_date). У бланка только с относительными подписями колонок абсолютных дат нет, и неуверенная дата отчёта
структурой не подтверждается никогда: документ needs_check, аналитик подтверждает или исправляет дату в ядре (PATCH
шапки). needs_review_fields — поля, которые держат документ в needs_check; ИНН и организация с уверенностью ниже 100
— предупреждения INN_UNCERTAIN / ORGANIZATION_UNCERTAIN (info, с field), в needs_review_fields не попадают
(подсветка у поля — по предупреждению); единицы и прочие поля на статус не влияют.
Scenario: Неуверенная, но верная дата
- WHEN дата отчёта 30.09.2025 с уверенностью 60, самая поздняя дата колонок с уверенной подписью 30.09.2025
- THEN
needs_review_fieldsпуст, предупреждения о дате нет
Scenario: Неуверенные дата и подпись не подтверждают друг друга
- WHEN дата отчёта 30.09.2025 с уверенностью 60, единственная абсолютная подпись колонки «На 30.09.2025» — с уверенностью 60
- THEN
needs_review_fields = ["report_date"],REPORT_DATE_UNCERTAINиCOLUMN_DATE_UNCERTAINуровняerror; снимаются подтверждением даты отчёта и даты колонки аналитиком
Scenario: Неуверенный год
- WHEN дата отчёта 30.09.2023 с уверенностью 60, колонки на 30.09.2025 и 31.12.2024
- THEN
needs_review_fields = ["report_date"],REPORT_DATE_UNCERTAINуровняerror, документneeds_check
Scenario: Бланк 66н с относительными подписями
- WHEN дата отчёта 30.09.2025 с уверенностью 60, подписи колонок — «На отчетную дату», «На 31 декабря предыдущего года»
- THEN
needs_review_fields = ["report_date"],REPORT_DATE_UNCERTAINуровняerror; снимается подтверждением даты аналитиком
Requirement: Шаг 8 — предупреждения
Предупреждение SHALL быть структурой {code, severity, message, line_refs?, column_index?, field?}: severity —
error | info; line_refs — список ref строк только из lines[] (о строках ролей title / header /
numbering — в message); column_index — номер столбца сетки (требование «Шаг 2 — номер колонки»); field — поле
шапки. Место в файле — locator документа, а не текст. Предупреждения уровня файла (metrics.warnings) дополнительно
имеют locator? (json набора ИС, таблица ИС или лист) и SHALL выдаваться там, где документа нет; предупреждение файла
уровня error делает конверт needs_check и блокирует RECOGNIZED всех документов файла в ядре. Уровни SHALL быть
такими:
| Код | Уровень | Когда |
|---|---|---|
JSON_FAILED |
error (файл) | json набора ИС ошибочен при успешном соседе |
TABLE_PARSE_ERROR |
error (файл) | исключение при разборе таблицы ИС (BACKEND_PARSING_ERROR); документа нет |
TABLE_LOST |
error (файл) | таблица ИС с содержимым (ячейка с цифрой или «цифрой под видом буквы») без колонки кодов и без type_hint |
TABLE_SKIPPED |
info (файл) | таблица ИС без содержимого (только текст), без колонки кодов и без type_hint; документа нет |
CODE_COLUMN_NOT_FOUND |
error | у формы с type_hint BALANCE/INCOME нет столбца кодов |
SEGMENT_WITHOUT_CODES |
error | сегмент с содержимым без столбца кодов (в том числе лист-продолжение) |
SHEET_NOT_JOINED |
error | лист с ≥ 3 кодами и другим числом колонок не присоединился |
CONTINUATION_TYPE_CONFLICT |
error | тип маркера продолжения противоречит форме |
SEGMENT_COLUMNS_MISMATCH |
error | колонки сегмента при сопоставлении по порядку не совпали по числу с первым |
COLUMN_WITHOUT_DATE |
error | числовая колонка без разобранной подписи |
COLUMN_DATE_UNCERTAIN |
error | неуверенная подпись не попала в ожидаемые даты |
HEADER_ROW_NOT_FOUND |
error | заголовка нет ни у одного сегмента |
TYPE_CONFLICT |
error | тип по кодам противоречит type_hint |
NO_ORGANIZATION_NO_INN |
error | в шапке нет ни организации, ни ИНН |
PERIOD_UNKNOWN |
error | нет ни даты отчёта, ни абсолютных дат колонок |
REPORT_DATE_UNCERTAIN |
error | неуверенная дата отчёта не совпала с датами колонок |
XLSB_SCALE_SUSPECT |
error | xlsb без форматов при «тыс. руб.» и ≥ 10 % дробных |
INN_NOT_FOUND, INN_UNCERTAIN |
info | ИНН не найден или неуверен при найденной организации |
ORGANIZATION_NOT_FOUND, ORGANIZATION_UNCERTAIN |
info | организация не найдена или неуверенна при найденном ИНН |
REPORT_DATE_UNPARSED |
info | дата отчёта не разобрана при наличии дат колонок |
UNITS_NOT_FOUND |
info | единицы не найдены |
FILE_TYPE_MISMATCH |
info (файл) | формат файла не совпал с file_type запроса |
XLS_NO_CODEPAGE |
info (файл) | книга xls без кодовой страницы — текст мог прочитаться неверно |
COLUMN_SKIPPED |
info | подпись из словаря исключений пропущена |
FRACTIONAL_IN_THOUSANDS |
info | дробные при «тыс. руб.» (кроме xlsb без форматов) |
XLSB_NO_FORMATS, NUMBER_FORMAT_UNPARSED |
info | числовые форматы недоступны или не разобраны |
VALUE_OUT_OF_RANGE |
info | число Excel длиннее 16 целых цифр |
SHEET_OTHER |
info (файл) | лист Excel «прочее» без type_hint и кодов; документа нет |
ROW_SPLIT |
info | разлеплена склеенная строка |
NUMBERS_OUTSIDE_TABLE |
info | числа правее последней подписанной колонки значений не учтены; адреса ячеек в тексте |
Предупреждения пути ОСВ — уровня info, код с префиксом OSV_. Новый код предупреждения вводится словарём пакета и
таблицей §11 05-domain-rules.md; code в контракте — строка ^[A-Z][A-Z0-9_]*$, не перечисление.
Scenario: Исключение при разборе таблицы ИС
- WHEN разбор второй таблицы json упал с исключением, первая разобрана
- THEN документ первой таблицы со своим статусом; в
metrics.warnings—TABLE_PARSE_ERRORуровняerrorсlocator = "file:<имя json>;table:1"; конвертneeds_check
Scenario: НПЗ ОФР без ИНН
- WHEN в шапке ОФР есть организация и нет ИНН
- THEN
INN_NOT_FOUNDуровняinfo, документ может бытьdone
Scenario: Привязка к ячейке
- WHEN не сошлось
1200 = 1210 + … + 1260в колонке 1 - THEN нарушение в
checks.relations_failedсcolumn_index = 1иcodes,needs_review = trueу ячеек этих кодов в колонке 1
Requirement: Шаг 8 — статус документа и конверта
Статус документа SHALL вычисляться одной функцией общего кода: done ⇔ раскладка найдена ∧ (организация или ИНН) ∧
период определён (дата отчёта или самая поздняя абсолютная дата колонок) ∧ все строки ok или detail ∧ нет
значений, строк и полей шапки с needs_review ∧ нет жёстких нарушений §7 вне допуска и округления ∧ нет
предупреждений документа уровня error; иначе needs_check. DISCLOSURE — done без условий о раскладке, строках и
проверках. Вход функции SHALL быть одинаковым у сервиса и ядра и не зависеть от источника: document_type,
layout_found (сервис — столбец кодов найден хотя бы у одного сегмента; ядро — у формы есть строка из источника,
row_index не null), has_organization_or_inn, period_known, строки {line_status, needs_review}, значения
{line_status строки, needs_review}, число жёстких нарушений вне допуска и округления, accepted, уровни
предупреждений документа (у ядра — записи document_issue вида warning). needs_review на входе уже учитывает правки
и подтверждения аналитика (правленое или подтверждённое значение и строка — false); accepted — жёсткие нарушения не
учитываются. Значения строк detail функция MUST NOT учитывать, даже если на входе у них needs_review = true (Р-23). Статус ОСВ —
done, когда контрольные суммы §1.4 листа сошлись. Статус конверта: done ⇔
все документы done ∧ нет предупреждений файла уровня error; иначе needs_check; документов нет — error
(RECOGNITION_FAILED у scan-gateway, INVALID_DOCUMENT у tabular-parser). Квартал периода, сверка ИНН и наименований
со справочником в done MUST NOT входить.
Scenario: Excel-форма без замечаний
- WHEN Excel-ББ с найденной раскладкой, организацией, датой, всеми строками
ok, сошедшимися соотношениями - THEN документ
done; уверенность 100 у Excel выполняет условия покрытия автоматически
Scenario: Строка без кода с суммой
- WHEN в форме есть строка
unreadableс суммой - THEN документ
needs_check
Scenario: Ядро считает статус той же функцией
- WHEN ядро после PATCH вызывает функцию статуса по строкам БД формы, у которой все строки
ok, правленая ячейка былаneeds_review, нарушений и предупрежденийerrorнет - THEN функция даёт
done— тот же результат, что дал бы сервис по тем же данным с подтверждённой ячейкой
Scenario: Соседний документ не тянет файл
- WHEN ББ
done, ОФР того же файлаneeds_checkиз-за одной ячейки, предупреждений файла нет - THEN конверт
needs_check, у ББdocument.status = done
Scenario: Сомнительная ячейка расшифровки
- WHEN у формы все строки
okилиdetail, нарушений иerrorнет, единственное значение с уверенностью ниже 100 — в строкеdetail - THEN документ
done
Requirement: Точка входа пакета для пересчёта по сохранённым данным
Пакет SHALL предоставлять функцию пересчёта формы по сохранённым данным без сетки и FormCandidate — ею ядро
пересчитывает форму после PATCH и при приёме результата 1.x, сервисы ею не пользуются. Вход — тип пакета
StoredForm, собираемый хостом из своих данных:
document_type;effective_date(дата отчёта, иначе самая поздняя абсолютная дата колонок — у ядраdocument.reporting_date);has_organization_or_inn;period_known;needs_review_fields(как хранит ядро: поле уходит из него только правкой или подтверждением даты отчёта — сам пересчёт поля шапки не переоценивает);accepted;warnings— предупреждения документа{code, severity}(у ядра — записиdocument_issueвидаwarning);columns[] {key, column_index, period_date, period_kind}(key— идентификатор хоста, у ядраcolumn_id);lines[] {key, row_index | null, line_status, line_code, line_code_confidence, line_name, code_fixed, parent_key, values[] {column_key, amount, text, confidence, is_recognized, value_fixed}}:row_index = null— строка, созданная правкой (участвует в §7, как строкаok, пока у неё есть сумма);code_fixed— код назначен, сменён или подтверждён аналитиком (line_code_modifiedилиis_confirmedстроки);value_fixed— значение правлено или подтверждено (is_modifiedилиis_confirmed);text— сырой текст источника (source_text), по нему считается форма знака; у значения, созданного правкой,text = null.
Выход — {form_variant, edition, checks {relations_failed, soft_relations_failed}, line_needs_review {key → bool}, value_needs_review {key → bool}, detail_parents {key → parent_key | null}, status}. Правила — те же шаги 6–8, что у
сервисов, с поправками: form_variant определяется заново по кодам строк ok (код мог смениться правкой); edition —
редакция с лучшей подпоследовательностью кодов документа по всем строкам ok (как в структурной проверке, при
равенстве — по effective_date; кодов нет — по effective_date); value_fixed ⇒ needs_review = false, такое значение
в голосование знака не входит; code_fixed ⇒ needs_review строки = false; значения строк detail — false;
accepted ⇒ жёсткие нарушения статус не держат; detail_parents — ближайшая строка ok выше по row_index (строки
row_index = null родителями не бывают), нет такой — null. Пересчёт не меняет line_status, суммы и коды — только
выводит признаки.
Scenario: Пересчёт после правки по строкам БД
- WHEN ядро передаёт форму: ячейка 1230 правлена (
value_fixed), код 1250 назначен строке бывшейunreadable(code_fixed), шаблонная строка 1260 с суммой (row_index = null), строкаdetailс уверенностью 60 - THEN
checksпосчитаны с 1250 и 1260 в1200 = …; у 1230 и у значенияdetailneeds_review = false, у строки 1250needs_review = false;status— по общей функции статуса;form_variantиedition— по кодам после правки
Scenario: Смена кода меняет вариант
- WHEN у упрощённого по структуре ББ (есть 1600, нет 1100 и 1200) аналитик назначает строке код 1100
- THEN точка входа возвращает
form_variant = full, нарушения считаются полным набором