- proposal: зачем, что меняется, порядок выкладки «сначала ядро», non-goals - statement-normalization: шаги 1–8 общего кода arm_statements (кандидат, раскладка, статусы строк, значения, §7, needs_review, структурная проверка кода, уровни предупреждений, статус) - recognition-contract: файл результата и конверт v2.0, переход 1.x → 2.0 - tabular-recognition-service, scan-gateway: адаптеры шага 1 и что уходит из сервисов - arm-core: хранение v2.0, агрегат по коду, document_issue, RECOGNIZED, PATCH lines/columns/confirmed, порядок 409, миграция v5.19, crm_id ГСК/ГСЗ - arm-web: экран проверки формы по новым полям - design.md: автозамена между файлами — §9.5 п. 3, а не §12 п. 3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
54 KiB
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, заголовков форм и ОКУД,
подписей колонок («Код», «Наименование», исключения), букв-двойников, упорядоченные перечни кодов форм по редакциям
(66n, fsbu4) и вариантам (full, simplified) без наименований, диапазоны кодов — и загрузчик словарей. Словаря
«код → наименование» в пакете MUST NOT быть (Р-3). Пакет SHALL владеть своими типами входа и выхода; конвертацию
выхода в модель контракта делает хост. Golden-тесты пакета — внутри пакета на синтетических и обезличенных данных;
тесты на реальных фикстурах остаются в сервисах и соседние репозитории не читают.
В tabular-parser, scan-gateway и arm-core пакет SHALL лежать копией вместе с тестами и конфигом линтеров, подключённым
к pytest и ruff хоста как include, и импортироваться по имени arm_statements. Копию SHALL раскладывать и
проверять скрипт sync-statements репозитория docs (по образцу sync-web.sh): --check возвращает код 20, если
копия отличается от источника или правилась напрямую; скрипт вызывается из make check-full потребителя и MUST NOT
вызываться из make check и пайплайна платформы. Маркер коммита источника копии SHALL попадать в результат как
service.statements_version.
Scenario: Прямая правка копии
- WHEN в
tabular-parserизменён файл копии пакета, а источник вarm-statements— нет - THEN
sync-statements --checkзавершается с кодом 20 и называет файл;make checkсервиса этого не проверяет
Scenario: Версия общего кода в результате
- WHEN сервис собирает результат копией пакета, разложенной из коммита
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, 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>).
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: 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); единицы — словарём единиц. Сырой текст и уверенность каждого поля SHALL доезжать до контракта
(<поле>_text, <поле>_confidence).
Scenario: ИНН с мусором клеток
- WHEN поле ИНН шапки OCR —
"3|1 21 2|"с цифрами ИНН между вертикальными чертами - THEN ИНН собирается только из цифр,
|единицей не считается;inn_textхранит исходный текст
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 — многостраничная форма
Одна форма 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» или четырёхзначная сумма «3450»
- THEN кодом это не считается (вне отрезков)
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, а если
сегмент не первый и кодов в нём меньше трёх — ослабленный поиск: столбец с наибольшим числом кодов из диапазона
столбца первого сегмента, минимум один. Колонки значений сегмента без шапки — столбцы правее кода, где в строках с
кодами есть числа; они сопоставляются с колонками первого сегмента по порядку; число не совпало — предупреждение
SEGMENT_COLUMNS_MISMATCH (error) и значения по позиции. Столбец кодов не найден ни так, ни так — предупреждение
SEGMENT_WITHOUT_CODES (error) сегмента; строки сегмента не теряются (роль data, статусы по шагу 3).
Scenario: Лист «_2» НПЗ со столбцом кодов в другой колонке
- WHEN второй лист формы без шапки содержит единственную строку 2400, и код стоит не в той колонке, что на первом листе
- THEN у сегмента свой столбец кодов (ослабленный поиск), значения строки 2400 не сдвинуты
Scenario: Повторная шапка на второй странице скана
- WHEN в таблице ИС посередине стоит повторная строка «Наименование показателя | Код | На 30.09.2025 | …»
- THEN с неё начинается новый сегмент со своей раскладкой; строка-шапка в данные не попадает
Requirement: Шаг 2 — строка заголовка, нумерация и роли строк
Строка заголовка SHALL быть последней над первой строкой с кодом, где в столбце кодов стоит подпись кода; иначе
последней над первой строкой с кодом с двумя и более подписями-периодами правее кода (или одной, не голым годом);
двухстрочный заголовок склеивается. Строка нумерации — строка, где не меньше трёх непустых ячеек, каждая — целое число,
и числа идут подряд, начиная с 1, слева направо — SHALL получать роль numbering и в данные не попадать. Роль
title — только строки выше найденной строки заголовка; в сегменте без заголовка роли title нет. Заголовка нет ни у
одного сегмента документа → колонки значений первого сегмента — столбцы правее кода, где в строках с кодами числа,
label = "", period_end = null, period_kind = none, одно предупреждение HEADER_ROW_NOT_FOUND (error).
Заголовок есть хотя бы у одного сегмента — остальные сегменты берут его колонки без предупреждения.
Scenario: Нумерация в скане (AURORA-1517)
- WHEN под шапкой таблицы ИС строка «1 | 2 | 3 | 4 | 5»
- THEN роль
numbering, строки данных из неё нет
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_end и 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_end = null, period_kind остаётся относительным.
Подпись из словаря исключений («Изменение», «Отклонение», «%», «Темп», «Доля», «Примечание») SHALL пропускаться с
предупреждением COLUMN_SKIPPED (info). Подпись, не разобранная ни как период, ни как исключение, при числах в
столбце в строках с кодами SHALL оставлять колонку с подписью как есть, period_end = null, period_kind = none и
предупреждением COLUMN_WITHOUT_DATE (error, column_index); без чисел — пропуск. Подпись OCR с уверенностью ниже
100, разобранная в дату, подтверждается, если дата входит в ожидаемые: эффективная дата, она же минус один и минус два
года, 31 декабря предыдущего и предшествующего ему года; иначе предупреждение COLUMN_DATE_UNCERTAIN (error,
column_index). Уверенность подписи — label_confidence (у Excel 100; подписи нет — null).
Scenario: Бланк 66н без впечатанных дат
- WHEN подписи колонок ББ — «На отчетную дату», «На 31 декабря предыдущего года», «На 31 декабря года, предшествующего предыдущему», дата отчёта шапки 30.09.2025
- THEN
period_endколонок 2025-09-30, 2024-12-31, 2023-12-31,period_kind—relative:current,relative:prev_year_end,relative:prev_prev_year_end; предупреждений о дате колонок нет
Scenario: Мусорная подпись над числами
- WHEN подпись колонки —
"| | г"с уверенностью 17, а в колонке суммы - THEN колонка остаётся,
period_end = null,period_kind = none, предупреждениеCOLUMN_WITHOUT_DATEуровняerrorсcolumn_index, документneeds_check
Scenario: Колонка «Изменение»
- WHEN правее периодов колонка «Изменение, %»
- THEN колонка пропускается, предупреждение
COLUMN_SKIPPEDуровняinfo
Requirement: Шаг 2 — столбец наименования
Столбец наименования SHALL быть столбцом с подписью, начинающейся с «наименование» (после нормализации), в строке заголовка; иначе — столбцом левее кода с наибольшим числом текстов, кроме столбца «Пояснения». Наименование строки — ячейка этого столбца; пустая — ближайшая текстовая ячейка левее кода, кроме «Пояснения».
Scenario: Наименование без подписи
- WHEN у формы нет подписи «Наименование показателя», а левее кода два столбца — «Пояснения» (короткие номера) и тексты статей
- THEN наименование берётся из столбца текстов статей
Requirement: Шаг 3 — строки данных и их статусы
Шаг 3 SHALL выдавать RawLine на каждую строку сетки с ролью data, у которой есть содержимое: код, число или
непустой текст в колонке значений с уверенностью ниже 100. У строки — row_index (порядковый номер в документе с 0,
по порядку сетки после разлепления), ref (уникален в документе: OCR row:<row_index ИС>, разлепленные
row:<i>#<k> с k от 1; Excel — адрес ячейки столбца кодов строки <лист>!<A1>, без столбца кодов — первой непустой
ячейки строки), ячейка кода, line_status, ячейка наименования, ячейки значений по колонкам сегмента, parent_ref.
Статусы:
ok— код разобран;unreadable— в ячейке кода есть хотя бы одна цифра, но код не разобран; ячейка кода без цифр считается пустой;detail— строка без кода с содержимым под строкой с кодом (в том же или в предыдущих сегментах документа);parent_ref—refближайшей строки с кодом выше;missing— строка без кода с содержимым, над которой во всём документе нет строки с кодом; строки ролиtitleсюда не попадают.
Строка без кода и без содержимого — не строка данных. Счётчиков отброшенных строк MUST NOT быть. Повтор кода — не
статус и не предупреждение: все строки с кодом — ok, порядок как в файле.
Scenario: Код-мусор (AURORA-1462)
- WHEN ячейка кода —
"ИЧ,250", в колонках суммы - THEN строка
unreadableсline_code_text = "ИЧ,250", суммами иref; строка не пропадает
Scenario: Расшифровка «в том числе»
- WHEN под строкой 1150 стоит строка «в том числе: Оборудование к установке» без кода с суммой
- THEN строка
detail,parent_ref=refстроки 1150
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, иначе целиком у каждой. Значения разлепленных строк получают
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): двойники (Р-11), пробелы и тонкие пробелы, минус «−» (U+2212) как знак, скобки → минус,}и]— закрывающая скобка; прочерк (-,–,—,−без цифр) и пусто →amount = null,is_recognized = true; разделители: уocrзапятая — тысячи при шаблоне[+-]?[1-9]\d{0,2}(,\d{3})+, точка — тысячи при[+-]?[1-9]\d*(\.\d{3})+, иначе десятичный разделитель, три и более знаков после него →amount = null,string_value= текст; уexcelзапятая и точка — десятичный разделитель; пробел допустим только как разделитель тысяч (группы ровно по три цифры), иначеamount— цифры подряд («лучшая догадка», не длиннее 16 цифр, иначеnull) иis_recognized = false; многострочное значение в строке с однострочным кодом →amount = null,string_value= текст,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: Запятая по источнику
- WHEN текст
"4,200" - THEN у
ocramount = 4200; уexcelamount = 4.20
Scenario: Длинное тире (OQ-148)
- 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) в сервисах выбирается по эффективной дате (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. Без столбца кодов: type_hint есть → документ без строк
(DISCLOSURE — done, иначе needs_check с CODE_COLUMN_NOT_FOUND уровня error); type_hint нет → у ocr
таблица ИС с содержимым — предупреждение файла TABLE_LOST (error), у excel — лист «прочее», SHEET_OTHER
(info). DISCLOSURE по type_hint — шаги 2–7 не выполняются; DISCLOSURE по кодам — строки и колонки
отбрасываются, проверки не считаются, документ done.
form_variant SHALL определяться только по структуре кодов: ББ — есть 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 = 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(полный или упрощённый); - родитель пятизначной подстроки — ближайшая четырёхзначная строка выше с теми же первыми тремя цифрами, иначе код из первых четырёх цифр; «сумма подстрок = родитель» (мягкое) — только если хотя бы одна подстрока в колонке непуста.
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
1600 = 1700расходится на 9,02 при итогах ~17 млрд - THEN нарушение в
soft_relations_failed, на статус и подсветку не влияет
Requirement: Шаг 8 — needs_review значения
Сервис SHALL вычислять needs_review у каждого значения:
- значение нуждается в покрытии, если
confidence < 100илиis_recognized = false; чистые значения в покрытии не нуждаются; - значение покрыто, если в его колонке есть жёсткое соотношение с участием его кода, которое посчитано и сошлось
(в пределах допуска или округления), и
amountзначения неnull; - знак: у члена
abs_terms, нуждающегося в покрытии, знак подтверждён, если форма записи знака (скобки, минус, без знака — поtext) совпадает с формой у большинства членовabs_termsэтой колонки с непустымamount,is_recognized = trueи без правки аналитика; не совпадает или сравнивать не с чем — знак не подтверждён; 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)"без минуса после разбора - THEN
needs_review = trueу 2120 независимо от соотношения
Scenario: Справочная строка без проверки
- WHEN значение 2510 с уверенностью 80 (строк 25xx нет в жёстких соотношениях)
- THEN
needs_review = true
Requirement: Шаг 8 — подтверждение неуверенного кода структурой
Код строки SHALL считаться неуверенным, если line_code_confidence < 100 или при разборе заменён двойник; только к
неуверенным кодам применяется структурная проверка (Р-12). Четырёхзначные коды строк 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: Нестандартный код Excel
- WHEN в Excel-форме код 1231 или пятизначная подстрока 11101 вне перечня
- THEN
needs_reviewстроки не ставится; проверка «код не из редакции» —infoядра
Requirement: Шаг 8 — поля шапки
Дата отчёта с уверенностью ниже 100 SHALL подтверждаться совпадением с самой поздней абсолютной датой колонок; иначе
поле report_date попадает в needs_review_fields и выдаётся REPORT_DATE_UNCERTAIN (error, field = report_date). ИНН и организация с уверенностью ниже 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.2023 с уверенностью 60, колонки на 30.09.2025 и 31.12.2024
- THEN
needs_review_fields = ["report_date"],REPORT_DATE_UNCERTAINуровняerror, документneeds_check
Requirement: Шаг 8 — предупреждения
Предупреждение SHALL быть структурой {code, severity, message, line_refs?, column_index?, field?}: severity —
error | info; line_refs — список ref строк; column_index — колонка; field — поле шапки. Место в файле —
locator документа, а не текст. Предупреждения уровня файла (metrics.warnings) дополнительно имеют locator?
(json набора ИС или лист). Уровни SHALL быть такими:
| Код | Уровень | Когда |
|---|---|---|
JSON_FAILED |
error (файл) | json набора ИС ошибочен при успешном соседе |
TABLE_PARSE_ERROR |
error | исключение при разборе таблицы ИС (BACKEND_PARSING_ERROR) |
TABLE_LOST |
error (файл) | таблица ИС с содержимым без колонки кодов и без 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 запроса |
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 | разлеплена склеенная строка |
Предупреждения пути ОСВ — уровня info, код с префиксом OSV_. Новый код предупреждения вводится словарём пакета и
таблицей §11 05-domain-rules.md; code в контракте — строка ^[A-Z][A-Z0-9_]*$, не перечисление.
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 принимать на вход и подтверждения и акцепт аналитика (у ядра после правок): правленое или
подтверждённое значение и строка не имеют needs_review, акцепт снимает жёсткие нарушения. Статус конверта: 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 ББ
done, ОФР того же файлаneeds_checkиз-за одной ячейки, предупреждений файла нет - THEN конверт
needs_check, у ББdocument.status = done