diff --git a/context/05-domain-rules.md b/context/05-domain-rules.md index 1f07fe4..31adbe0 100644 --- a/context/05-domain-rules.md +++ b/context/05-domain-rules.md @@ -1691,6 +1691,16 @@ file:<имя json>;table:`; документа у такой таблицы число колонок — `SEGMENT_COLUMNS_MISMATCH` (`error`): значения по позиции, лишние столбцы сегмента в колонки не попадают, их номера — в тексте предупреждения (S-56). Столбец кодов не найден ни так, ни так — `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:`, без `type_hint` (тип только по + кодам, без `TYPE_CONFLICT`), из предупреждений адаптера — только `XLSB_NO_FORMATS`. Сегменты без шапки, с меньшим числом кодов или той же формы (повторная шапка второй + страницы ББ, раздел 25xx после ОФР) не режут; строки другой формы, приклеенные к сегменту без своей шапки, тоже. + Разрез — функция пакета `split_by_form`, хост вызывает её перед разбором каждой таблицы или листа; документы частей + различаются `file_ordinal` (Р-10), модель и контракт не меняются. 3. **Дата колонки — один словарь периодов** `statement_periods.yaml`, объединяющий прежний словарь парсера и шаблоны `dates.py` шлюза (порядок приоритета текстовых форм: диапазон «с 01.01.2025 по 30.09.2025» — конец; «30 сентября 2025 г./r./года»; «I квартал 2026» / «1-й кв.» / «первый квартал»; «1 полугодие» / «1 п/г»; «9 месяцев 2025»; «январь – @@ -1756,6 +1766,14 @@ file:<имя json>;table:`; документа у такой таблицы меняется: подписанных колонок нет вовсе; столбец без подписи **между** подписанными — `COLUMN_WITHOUT_DATE`; столбец правее с **любым текстом подписи**, не разобранным ни как период, ни как исключение (OCR-мусор «| | г», «Проверка»), — `COLUMN_WITHOUT_DATE`, как раньше. Правило общее для Excel и 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. **Столбец наименования** — подпись, начинающаяся с «наименование», в строке заголовка; иначе столбец левее кода с наибольшим числом текстов, кроме «Пояснения» (пустые или ≤ 3 символов); наименование строки — ячейка этого столбца, пуста — ближайшая текстовая ячейка левее кода, кроме «Пояснения». @@ -2020,7 +2038,7 @@ III (aep_3). Алгоритм: четырёхзначные коды строк | `VALUE_OUT_OF_RANGE` | info | число Excel длиннее 16 целых цифр | — | | `SHEET_OTHER` | info, файл | лист Excel «прочее» без типа и кодов (`locator = sheet:<имя>`; документа нет) | — | | `ROW_SPLIT` | info | разлеплена склеенная строка | — | -| `NUMBERS_OUTSIDE_TABLE` | info | числа правее последней подписанной колонки значений не учтены (шаг 2, п. 6); адреса ячеек — в тексте | — | +| `NUMBERS_OUTSIDE_TABLE` | info | числа правее последней подписанной колонки значений или в редком столбце с неразобранной подписью между колонками периодов не учтены (шаг 2, п. 6); адреса ячеек — в тексте | — | | `OSV_*` | info | предупреждения листа ОСВ (§1) | — | | `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) | пересчёт после правки | diff --git a/openspec/changes/statement-form-split/.openspec.yaml b/openspec/changes/statement-form-split/.openspec.yaml new file mode 100644 index 0000000..8a94d76 --- /dev/null +++ b/openspec/changes/statement-form-split/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-11-06 diff --git a/openspec/changes/statement-form-split/design.md b/openspec/changes/statement-form-split/design.md new file mode 100644 index 0000000..0ad8a33 --- /dev/null +++ b/openspec/changes/statement-form-split/design.md @@ -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` части — `;form:` (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`; без него поведение прежнее — подъём пина без правки хоста ничего не ломает. diff --git a/openspec/changes/statement-form-split/proposal.md b/openspec/changes/statement-form-split/proposal.md new file mode 100644 index 0000000..e50fa71 --- /dev/null +++ b/openspec/changes/statement-form-split/proposal.md @@ -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:`. + +## 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 — только пин (пересчёт не режет: документы уже разрезаны). diff --git a/openspec/changes/statement-form-split/specs/statement-normalization/spec.md b/openspec/changes/statement-form-split/specs/statement-normalization/spec.md new file mode 100644 index 0000000..34034df --- /dev/null +++ b/openspec/changes/statement-form-split/specs/statement-normalization/spec.md @@ -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 = ";form:"`, `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:;table:0"`, колонки ББ; ОФР — строки 41–60, + `locator = "file:;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 считаться числами правее таблицы. Подписанный столбец без чисел в строках с кодами границей таблицы не служит. +Адрес ячейки — `<лист>!` у Excel и `row:` у 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`