AURORA-1734, AURORA-1735: разрез по смене формы и редкий столбец (#214)

This commit is contained in:
aep 2026-11-07 09:20:48 +03:00
commit e5274285ea
5 changed files with 285 additions and 1 deletions

View File

@ -1691,6 +1691,16 @@ file:<имя json>;table:<i>`; документа у такой таблицы
число колонок — `SEGMENT_COLUMNS_MISMATCH` (`error`): значения по позиции, лишние столбцы сегмента в колонки не число колонок — `SEGMENT_COLUMNS_MISMATCH` (`error`): значения по позиции, лишние столбцы сегмента в колонки не
попадают, их номера — в тексте предупреждения (S-56). Столбец кодов не найден ни так, ни так — попадают, их номера — в тексте предупреждения (S-56). Столбец кодов не найден ни так, ни так —
`SEGMENT_WITHOUT_CODES` (`error`); строки сегмента не теряются (роль `data`, статусы по §11.4). `SEGMENT_WITHOUT_CODES` (`error`); строки сегмента не теряются (роль `data`, статусы по §11.4).
**Смена формы внутри кандидата** (AURORA-1734, решение владельца 06.11.2026): ИС может вернуть ББ и ОФР одной
таблицей («Баланс АК-ЮЛ.pdf»: шапка ОФР «Коды | За I полугодие 2026 года | …» на строке 41 таблицы «Бухгалтерский
баланс»). До шагов 2–8 кандидат режется на части: сегмент **со своей шапкой**, первые `min_codes` (3) кодов которого —
одной формы и другой, чем у текущей части (тип части — по большинству кодов её первого сегмента с кодами), начинает
новую часть. Часть — обычный кандидат со своими колонками и периодами; первая сохраняет `locator`, `type_hint`,
`evidence` и предупреждения адаптера, следующие — `locator` `<исходный>;form:<k>`, без `type_hint` (тип только по
кодам, без `TYPE_CONFLICT`), из предупреждений адаптера — только `XLSB_NO_FORMATS`. Сегменты без шапки, с меньшим числом кодов или той же формы (повторная шапка второй
страницы ББ, раздел 25xx после ОФР) не режут; строки другой формы, приклеенные к сегменту без своей шапки, тоже.
Разрез — функция пакета `split_by_form`, хост вызывает её перед разбором каждой таблицы или листа; документы частей
различаются `file_ordinal` (Р-10), модель и контракт не меняются.
3. **Дата колонки — один словарь периодов** `statement_periods.yaml`, объединяющий прежний словарь парсера и шаблоны 3. **Дата колонки — один словарь периодов** `statement_periods.yaml`, объединяющий прежний словарь парсера и шаблоны
`dates.py` шлюза (порядок приоритета текстовых форм: диапазон «с 01.01.2025 по 30.09.2025» — конец; «30 сентября 2025 `dates.py` шлюза (порядок приоритета текстовых форм: диапазон «с 01.01.2025 по 30.09.2025» — конец; «30 сентября 2025
г./r./года»; «I квартал 2026» / «1-й кв.» / «первый квартал»; «1 полугодие» / «1 п/г»; «9 месяцев 2025»; «январь – г./r./года»; «I квартал 2026» / «1-й кв.» / «первый квартал»; «1 полугодие» / «1 п/г»; «9 месяцев 2025»; «январь –
@ -1756,6 +1766,14 @@ file:<имя json>;table:<i>`; документа у такой таблицы
меняется: подписанных колонок нет вовсе; столбец без подписи **между** подписанными — `COLUMN_WITHOUT_DATE`; столбец меняется: подписанных колонок нет вовсе; столбец без подписи **между** подписанными — `COLUMN_WITHOUT_DATE`; столбец
правее с **любым текстом подписи**, не разобранным ни как период, ни как исключение (OCR-мусор «| | г», «Проверка»), — `COLUMN_WITHOUT_DATE`, как раньше. Правило общее для Excel и правее с **любым текстом подписи**, не разобранным ни как период, ни как исключение (OCR-мусор «| | г», «Проверка»), — `COLUMN_WITHOUT_DATE`, как раньше. Правило общее для Excel и
OCR и живёт в пакете `arm_statements` (шаг 2), не в адаптере хоста. OCR и живёт в пакете `arm_statements` (шаг 2), не в адаптере хоста.
**Редкий столбец с неразобранной подписью** (AURORA-1735, решение владельца 06.11.2026; «Баланск Калтан.pdf»: подпись
«За I полугодие 2025 года» разорвана на «31» и «а I полугодие 2025 года», под «31» — постороннее «$301» в строке 2400):
столбец с текстом подписи, не разобранным ни как период, ни как исключение, стоящий **между** колонками периодов
(есть левее и правее), с числом не более чем в одной строке с кодом при трёх и более у каждой колонки периода
сегмента и с числами всех колонок периода в строке его числа (число лишнее, а не сдвинутое из пустой ячейки периода),
колонкой сумм не считается — его числа идут в то же `NUMBERS_OUTSIDE_TABLE` (`info`) с текстом «числа вне
колонок формы не учитываются: …» (если среди адресов есть такой столбец; иначе прежний текст «правее таблицы»).
Столбец с числами в двух и более строках с кодом остаётся колонкой с `COLUMN_WITHOUT_DATE`.
7. **Столбец наименования** — подпись, начинающаяся с «наименование», в строке заголовка; иначе столбец левее кода с 7. **Столбец наименования** — подпись, начинающаяся с «наименование», в строке заголовка; иначе столбец левее кода с
наибольшим числом текстов, кроме «Пояснения» (пустые или ≤ 3 символов); наименование строки — ячейка этого столбца, наибольшим числом текстов, кроме «Пояснения» (пустые или ≤ 3 символов); наименование строки — ячейка этого столбца,
пуста — ближайшая текстовая ячейка левее кода, кроме «Пояснения». пуста — ближайшая текстовая ячейка левее кода, кроме «Пояснения».
@ -2020,7 +2038,7 @@ III (aep_3). Алгоритм: четырёхзначные коды строк
| `VALUE_OUT_OF_RANGE` | info | число Excel длиннее 16 целых цифр | — | | `VALUE_OUT_OF_RANGE` | info | число Excel длиннее 16 целых цифр | — |
| `SHEET_OTHER` | info, файл | лист Excel «прочее» без типа и кодов (`locator = sheet:<имя>`; документа нет) | — | | `SHEET_OTHER` | info, файл | лист Excel «прочее» без типа и кодов (`locator = sheet:<имя>`; документа нет) | — |
| `ROW_SPLIT` | info | разлеплена склеенная строка | — | | `ROW_SPLIT` | info | разлеплена склеенная строка | — |
| `NUMBERS_OUTSIDE_TABLE` | info | числа правее последней подписанной колонки значений не учтены (шаг 2, п. 6); адреса ячеек — в тексте | — | | `NUMBERS_OUTSIDE_TABLE` | info | числа правее последней подписанной колонки значений или в редком столбце с неразобранной подписью между колонками периодов не учтены (шаг 2, п. 6); адреса ячеек — в тексте | — |
| `OSV_*` | info | предупреждения листа ОСВ (§1) | — | | `OSV_*` | info | предупреждения листа ОСВ (§1) | — |
| `CODE_NOT_IN_DICTIONARY`, `CODE_NOT_IN_EDITION`, `NAME_MISMATCH`, `STATEMENTS_VERSION_MISMATCH` | info, ядро | проверки ядра (§9.3, §9.5) | пересчёт | | `CODE_NOT_IN_DICTIONARY`, `CODE_NOT_IN_EDITION`, `NAME_MISMATCH`, `STATEMENTS_VERSION_MISMATCH` | info, ядро | проверки ядра (§9.3, §9.5) | пересчёт |
| `RESULT_STATUS_INCONSISTENT` | info, ядро | `document.status = done` из результата противоречит строкам, `needs_review`, нарушениям или предупреждениям документа — ядро считает его `needs_check` (§9.3) | пересчёт после правки | | `RESULT_STATUS_INCONSISTENT` | info, ядро | `document.status = done` из результата противоречит строкам, `needs_review`, нарушениям или предупреждениям документа — ядро считает его `needs_check` (§9.3) | пересчёт после правки |

View File

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-11-06

View File

@ -0,0 +1,52 @@
## Context
Решения владельца 06.11.2026 (выбраны рекомендованные варианты): 1734 — резать по смене формы у себя; 1735 — отсеивать
редкую колонку с неразобранной подписью. Разбор на JSON из задач (прогон нормализатора scan-gateway на пакете 0.1.3):
- АК-ЮЛ: один документ `BALANCE`, 54 кода (ББ + ОФР), `SEGMENT_COLUMNS_MISMATCH` у сегмента строки 19, десять жёстких
нарушений ББ. Строка 41 — шапка с «Коды» (столбец кодов сдвинут в столбец 2), сегмент открывается, но остаётся в ББ.
- Калтан, ОФР: колонки 4 / 5 («31») / 6, `COLUMN_WITHOUT_DATE` по столбцу 5; в столбце 5 разбирается одно число —
«$301 .» (тире «—», «——-» числами не считаются).
## Decisions
- **Разрез — отдельная функция до раскладки.** `normalize_candidate` возвращает один документ, хосты вызывают его на
каждый кандидат с изоляцией ошибок. Менять `CandidateResult` на список ломает API у трёх потребителей; разрез на
кандидаты сохраняет шаги 2–8 нетронутыми — каждая часть проходит их как обычный кандидат (свои колонки, периоды,
§7). Сегменты для разреза ищутся тем же кодом шага 2 (`_split_and_analyse`), что и для раскладки.
- **Условие разреза:** своя шапка + первые `min_codes` (3) кодов сегмента — одной формы, отличной от формы текущей части. Шапка нужна,
чтобы не резать по случайным строкам; порог — чтобы пара строк другой формы (OCR-мусор, «пояснения» в хвосте) не
создавала документ. Тип части — по первому сегменту части с определённым типом.
- **Часть `k ≥ 1`:** `type_hint = null`, пустые `evidence` и `warnings` — метка таблицы ИС относится к первой форме, а
предупреждения адаптера (единицы, шапка) уже есть у первой части; повторять их в каждой части — дубли.
- **Редкий столбец:** «между» — есть колонка периода левее и правее; «редкий» — число не более чем в одной строке с
кодом при ≥ 3 у каждой колонки периода сегмента. Правило применяется в `segment_columns` (у каждого сегмента со своей
шапкой), адреса идут в общее `NUMBERS_OUTSIDE_TABLE` формы.
## Decisions made without discussion
| Дата | Решение | Обоснование | Цена ошибки |
|---|---|---|---|
| 2026-11-06 | `locator` части — `<locator>;form:<k>` (k с 1); первая часть — прежний `locator` | Locator нигде не разбирается (ядро и UI показывают как строку); формат читаемый, у первой части ничего не меняется — golden и уже сохранённые документы не съезжают | Низкая: строка в карточке документа; поменять формат — одна строка кода |
| 2026-11-06 | Порог разреза — существующий `min_codes` словаря диапазонов (3), не новый ключ | Тот же смысл «достаточно кодов, чтобы считать это формой» уже есть в словаре | Низкая: порог — данные |
| 2026-11-06 | Разрез — общий для OCR и Excel | Правила шага 2 общие (как Р-44); у Excel формы обычно на разных листах — разрез почти не срабатывает, но лист «ББ + ОФР» с повторной шапкой тоже станет двумя документами | Средняя: golden tabular-parser может измениться — проверяется при подъёме пина, при сюрпризе — флаг источника |
| 2026-11-06 | Текст `NUMBERS_OUTSIDE_TABLE`: «числа вне колонок формы не учитываются», если есть адрес редкого столбца, иначе прежний | Прежний текст «правее таблицы» неверен для столбца посередине; прежние случаи и их golden не меняются | Нулевая: текст предупреждения |
| 2026-11-06 | «Редкий»: ≤ 1 число в столбце и ≥ 3 у каждой колонки периода | Калтан: 1 против 10+; столбец с двумя числами уже может быть данными — оставляем аналитику | Низкая: порог — константа пакета |
| 2026-11-06 | Реальные JSON АК-ЮЛ и Калтана в fixtures не кладём; тесты пакета — на синтетических кандидатах (`tests/builders.py`), прогон реальных файлов — вручную через нормализатор scan-gateway | Файлы заказчика с контура; синтетика воспроизводит обе раскладки строка в строку | Низкая |
| 2026-11-07 | Тип сегмента для разреза — по первым `min_codes` кодам (все одной формы), а не по большинству всех кодов сегмента (исполнение, Task 2) | Строки ОФР без своей шапки прилипают к сегменту пассива ББ с повторной шапкой; по большинству такой сегмент — «ОФР», и разрез по шапке ББ унёс бы пассив в ОФР | Низкая: сегмент, который открывается 1–2 кодами своей формы и дальше идёт чужой, не режется — как до изменения |
| 2026-11-07 | Для разреза — помощник `segments.split_segments` (первая строка, своя шапка, коды сегмента) вместо прямого импорта приватных функций модуля | `split.py` не зависит от приватных имён | Нулевая |
| 2026-11-07 | Редкий столбец отсеивается, только если в строке его числа у всех колонок периода есть числа (финальное ревью, I-1) | Иначе правило молча выбрасывает значение, которое OCR сдвинул из пустой ячейки периода, и документ становится `done` (пассив АК-ЮЛ: итог 1700 = 46922 в столбце «На 2»); сужение совпадает со смыслом решения владельца «постороннее число» | Низкая: обрывок с посторонним числом в строке с пустым периодом остаётся `COLUMN_WITHOUT_DATE` — как до изменения |
| 2026-11-07 | Части `k ≥ 1` получают из предупреждений адаптера `XLSB_NO_FORMATS` (финальное ревью, I-2) | Без него шаг 4 проверяет масштаб xlsb как у файла с форматами: `FRACTIONAL_IN_THOUSANDS` вместо `XLSB_SCALE_SUSPECT` | Нулевая |
## Risks / Trade-offs
- Пассив ББ АК-ЮЛ (прогон 07.11): столбец «На 2» с одним числом 46922 — сдвинутый OCR итог 1700 за 31.12.2024 (ячейка
столбца периода в этой строке пуста), поэтому после уточнения I-1 правило не срабатывает: поведение прежнее
(`SEGMENT_COLUMNS_MISMATCH`, `needs_check`).
- АК-ЮЛ после разреза: ОФР — отдельный документ, но подписи его колонок 5 («За I п / 20:») и 6 («юлугодие / 25 года»)
ИС тоже разорвала, и ни одна не разбирается как период — столбец 5 не «между периодами», правило редкого столбца не
срабатывает: обе колонки с `COLUMN_WITHOUT_DATE`, документ `needs_check`, аналитик ставит дату. Это честный результат
на испорченной шапке; ББ после разреза теряет 2xxx-строки и жёсткие нарушения, вызванные ими.
- Саракташагротехремонт — JSON в задаче нет (только PDF); ожидаем ту же раскладку, проверим после выкладки.
- Хостам нужен один вызов `split_by_form`; без него поведение прежнее — подъём пина без правки хоста ничего не ломает.

View File

@ -0,0 +1,42 @@
## Why
Замечания тестировщика заказчика 21.10.2026 (Яшин Д.О.):
- **AURORA-1734** — «Баланс АК-ЮЛ.pdf», «Баланс Саракташагротехремонт.pdf»: ИС Сканирование вернула ББ и ОФР **одной
таблицей** `table_structure[0]` с меткой «Бухгалтерский баланс». У нас таблица ИС — один кандидат, шапка ОФР на строке
41 — повторная шапка того же документа: строки ОФР попадают в ББ («Прочие строки»), под колонками и периодами ББ.
- **AURORA-1735** — «Баланск Калтан.pdf»: подпись «За I полугодие 2025 года» ИС разорвала на «31» (столбец 5) и «а I
полугодие 2025 года» (столбец 6); под обрывком «31» — одно постороннее число «$301» в строке 2400. Столбец 5 становится
колонкой без даты (`COLUMN_WITHOUT_DATE`, `error`), в ОФР три колонки вместо двух.
Оба дефекта — в ответе ИС, но владелец решил (06.11.2026) обрабатывать их у себя по общим правилам пакета.
## What Changes
- **Разрез кандидата по смене формы** (пакет `arm_statements`, новая функция `split_by_form`): сегмент со своей
шапкой, у которого не меньше `min_codes` кодов и тип формы по кодам другой, чем у текущей части, начинает новую часть —
отдельный документ со своими колонками и периодами. Хосты (scan-gateway, tabular-parser) вызывают разрез перед
`normalize_candidate`.
- **Редкий столбец с неразобранной подписью** между колонками периодов (число не более чем в одной строке с кодом при
трёх и более у колонок периодов) — не колонка сумм; его числа — в `NUMBERS_OUTSIDE_TABLE` (`info`) с текстом «числа вне
колонок формы не учитываются».
- Документы контекста: `05-domain-rules.md` §11 (п. 2 «Сегменты», п. 6 «Подпись без периода»), таблица предупреждений.
- Контракт, DBML, ядро — без изменений: части различаются `file_ordinal` (Р-10), `locator` части — строка с хвостом
`;form:<k>`.
## Non-goals
- Склейка разорванной подписи в период («31» + «а I полугодие 2025 года» → дата не восстанавливается из обрывков;
колонка 6 и так разбирается).
- Тип по метке файла ИС вместо метки таблицы (`TYPE_CONFLICT` у ОФР Калтана: `data_ocr.label` — «ОТЧЕТ О ФИНАНСОВЫХ
РЕЗУЛЬТАТАХ», метка таблицы — «Бухгалтерский баланс») — отдельное правило адаптера scan-gateway, не в этом change.
- Правило ИС «ОФР без ИНН — одним документом» (уточнение аналитика в AURORA-1734): мы режем по кодам, не по ИНН.
- Перераспознавание уже загруженных файлов.
## Impact
- Спека `statement-normalization`: новое требование «Шаг 2 — разрез кандидата по смене формы», изменены «Шаг 1 —
многостраничная форма» и «Шаг 2 — подпись и дата колонки».
- Репозитории: pylib_ai_arm_statements (0.1.5, RSHB/pylib_ai_arm_statements#5), arm-ka-docs (этот change); вызов
`split_by_form` и пин 0.1.5 в arm-scan-gateway и arm-tabular-parser — после merge AURORA-1677 (холд владельца до
выкатки гудвилла). arm-core — только пин (пересчёт не режет: документы уже разрезаны).

View File

@ -0,0 +1,170 @@
## ADDED Requirements
### Requirement: Шаг 2 — разрез кандидата по смене формы
Перед шагами 2–8 кандидат SHALL разрезаться на части по смене формы (AURORA-1734, решение владельца 06.11.2026).
Сегменты кандидата ищутся по правилу «Шаг 2 — сегменты, повторная шапка и границы листов». Тип формы сегмента — по
первым `min_codes` кодам (порог словаря диапазонов): все они одной формы (отрезок → `document_type`, как на шаге 6) —
это тип, которым сегмент открывается; иначе тип не определён. Новая часть SHALL начинаться с первой строки сегмента,
если у сегмента своя строка-шапка и он открывается формой, отличной от типа текущей части (тип части — по большинству
кодов её первого сегмента с кодами; после разреза — тип, которым открылась часть). Сегменты без шапки, с числом кодов
меньше порога или той же формы остаются в текущей части; строки другой формы, приклеенные к сегменту без своей шапки
(сегмент открывается кодами текущей формы), разреза не дают.
Часть — обычный `FormCandidate` для шагов 2–8: строки сетки с первой строки сегмента до начала следующей части,
`segment_breaks` — сдвинутые на начало части; `header` — тот же, что у исходного кандидата (шапка файла ИС одна на
таблицу); `source` тот же. Первая часть сохраняет `locator`, `type_hint`, `evidence` и `warnings` исходного кандидата;
следующая часть `k` (с 1) получает `locator = "<locator исходного>;form:<k>"`, `type_hint = null`, пустые `evidence`,
из `warnings` — только предупреждения источника `XLSB_NO_FORMATS` (без них шаг 4 неверно проверяет масштаб xlsb) — её
тип определяется на шаге 6 только по кодам, без `TYPE_CONFLICT` от заголовка таблицы. Кандидат без
смены формы SHALL оставаться одним кандидатом без изменений.
Пакет SHALL экспортировать функцию разреза (`split_by_form(candidate, rules) → tuple[FormCandidate, ...]`); хост
вызывает её на каждый кандидат и передаёт части в `normalize_candidate` по порядку. Документы частей — отдельные
документы результата (ядро различает их по `file_ordinal`, Р-10); модель данных и контракт не меняются. Правило общее
для OCR и Excel.
#### Scenario: ББ и ОФР одной таблицей ИС (АК-ЮЛ)
- **WHEN** таблица ИС «Бухгалтерский баланс» содержит строки 1110…1700 (две страницы ББ с повторной шапкой на строке
19), а со строки 41 — шапку «Наименование показателя | Коды | За I полугодие 2026 года | …» и строки 2110…2400
- **THEN** две части: ББ — строки 0–40, `locator = "file:<json>;table:0"`, колонки ББ; ОФР — строки 41–60,
`locator = "file:<json>;table:0;form:1"`, колонки и периоды по своей шапке, `TYPE_CONFLICT` нет; повторная шапка
строки 19 разреза не даёт (тот же тип)
#### Scenario: Раздел 25xx после ОФР
- **WHEN** в таблице ОФР повторная шапка открывает сегмент со строками 2510…2910
- **THEN** разреза нет: тип сегмента тот же (`INCOME`), сегмент — продолжение ОФР
#### Scenario: ОФР без своей шапки после пассива
- **WHEN** после повторной шапки ББ идут строки 1310, 1300, 1700, а следом без шапки — 2110, 2120, 2100, 2400
- **THEN** разреза нет: сегмент открывается кодами ББ (как и до изменения)
#### Scenario: Пара строк другой формы
- **WHEN** после строк ББ повторная шапка открывает сегмент с двумя кодами 2xxx (меньше порога `min_codes`)
- **THEN** разреза нет, сегмент остаётся в части ББ (как раньше)
## MODIFIED Requirements
### Requirement: Шаг 1 — многостраничная форма
Требование относится к адаптеру Excel: у OCR каждая таблица ИС — отдельный кандидат, склейки таблиц нет (повторная
шапка внутри таблицы ИС — сегмент шага 2; смена формы внутри таблицы — разрез кандидата, требование «Шаг 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: Шаг 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`),
кроме редкого столбца ниже.
**Редкий столбец с неразобранной подписью** (AURORA-1735, решение владельца 06.11.2026) SHALL НЕ считаться колонкой
сумм, если одновременно: у столбца есть текст подписи, не разобранный ни как период, ни как исключение; левее и правее
него в сегменте есть колонки значений с подписью периода; число стоит не более чем в одной строке с кодом этого
столбца, а в каждой колонке значений сегмента с подписью периода — не менее чем в трёх; в строке этого числа у всех
колонок периода тоже есть числа (число лишнее, а не сдвинутое OCR из пустой ячейки периода). Его числа не попадают в значения
строк, `COLUMN_WITHOUT_DATE` не выдаётся; адреса его чисел добавляются в то же предупреждение `NUMBERS_OUTSIDE_TABLE`
(`info`). Если среди адресов предупреждения есть адреса такого столбца, текст — «числа вне колонок формы не
учитываются: …», иначе прежний «числа правее таблицы формы не учитываются: …». Это разорванная OCR подпись периода
(«31» + «а I полугодие 2025 года») с посторонним числом под обрывком; столбец с числами в двух и более строках с кодом
остаётся колонкой с `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: Обрывок подписи периода (Калтан, AURORA-1735)
- **WHEN** у ОФР подписи «За 1 полугодие 2026 года» (столбец 4), «31» (столбец 5), «а I полугодие 2025 года» (столбец 6);
в столбцах 4 и 6 суммы в десятке строк с кодом, в столбце 5 — одно число «$301 .» в строке 2400 и тире
- **THEN** колонок значений две (4 и 6), `COLUMN_WITHOUT_DATE` нет; `NUMBERS_OUTSIDE_TABLE` уровня `info` с адресом
ячейки строки 2400 столбца 5 и текстом «числа вне колонок формы не учитываются: row:21»; число 301 в значения не
попадает
#### Scenario: Неразобранная подпись над двумя числами
- **WHEN** между двумя колонками периодов стоит столбец с подписью «Прим.» (не из словаря исключений), числа в нём — в
двух строках с кодом
- **THEN** столбец остаётся колонкой с `COLUMN_WITHOUT_DATE` уровня `error`
#### Scenario: Значение, сдвинутое в обрывок подписи
- **WHEN** у столбца «31» между колонками периодов одно число «55» в строке 2460, а ячейка колонки периода 2025 этой строки
пуста
- **THEN** столбец остаётся колонкой с `COLUMN_WITHOUT_DATE` уровня `error`, документ `needs_check` — значение не
теряется молча
#### Scenario: Колонка «Изменение»
- **WHEN** правее периодов колонка «Изменение, %»
- **THEN** колонка пропускается, предупреждение `COLUMN_SKIPPED` уровня `info`