Andrey Epifancev 47e6372cbe chore(openspec): архив unified-statement-normalization (#116)
Спеки применены: 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>
2026-10-19 09:25:43 +04:00

98 KiB
Raw Blame History

ADDED 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-statements origin/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 у Cell text = "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 у Cell text = "[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_index 16, 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) по стадиям:
    1. пусто или прочерк (-, –, —, − без цифр) → amount = null, is_recognized = true; многострочное значение в строке с однострочным кодом → amount = null, string_value = текст, is_recognized = false;
    2. крайние | (след границы таблицы) удаляются до замены двойников, а не становятся единицей ("|122477" → 122477); затем буквы-двойники вплотную к цифре → цифры (Р-11); и то и другое — исправление (is_recognized = false);
    3. ядро числа — участок от первого из символов «цифра, (, -, −, +» до последнего из «цифра, ), }, ]»; всё вне ядра — краевой мусор (|, :, =, ■, •, кавычки, точки, {, [ и любые другие символы): выбрасывается, а если среди выброшенного есть что-то кроме пробелов — is_recognized = false;
    4. в ядре допустимы только цифры, пробелы (в том числе NBSP и тонкие), ,, ., знак (-, − как минус, +) в начале и одна пара скобок вокруг всего числа: ( первым символом и ), } или ] последним (} / ] — исправление); { и [ открывающей скобкой не считаются. Иначе — буква в ядре (после стадии 2), скобка без пары или не на краю ядра, знак не в начале, любой другой символ между цифрами (:, /, =, |) — amount = null, string_value = текст, is_recognized = false;
    5. цифр нет вовсе → amount = null, string_value = текст, is_recognized = false;
    6. скобки → минус; разделители: у 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 у ocr amount = 4200; у excel amount = 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 у значений detail needs_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 и у значения detail needs_review = false, у строки 1250 needs_review = false; status — по общей функции статуса; form_variant и edition — по кодам после правки

Scenario: Смена кода меняет вариант

  • WHEN у упрощённого по структуре ББ (есть 1600, нет 1100 и 1200) аналитик назначает строке код 1100
  • THEN точка входа возвращает form_variant = full, нарушения считаются полным набором