Andrey Epifancev 90ea429642 docs(#116): proposal и дельты спек единой нормализации ББ/ОФР
- 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
2026-10-18 00:40:50 +04:00

54 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, заголовков форм и ОКУД, подписей колонок («Код», «Наименование», исключения), букв-двойников, упорядоченные перечни кодов форм по редакциям (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 у Cell text = "106 311", value = 106311, confidence = 100, ref = "Баланс!D27" (лист и адрес ячейки)

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); единицы — словарём единиц. Сырой текст и уверенность каждого поля 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 у ocr amount = 4200; у excel amount = 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