AURORA-1734/1735: §11 — разрез по смене формы и редкий столбец (RSHB/pylib_ai_arm_statements#5)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-11-07 09:01:31 +04:00
parent 1b92f2b2ca
commit c509c5ed76
3 changed files with 37 additions and 6 deletions

View File

@ -1689,6 +1689,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`). Сегменты без шапки, с меньшим числом кодов или той же формы (повторная шапка второй
страницы ББ, раздел 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»; «январь –
@ -1754,6 +1764,13 @@ 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 символов); наименование строки — ячейка этого столбца,
пуста — ближайшая текстовая ячейка левее кода, кроме «Пояснения». пуста — ближайшая текстовая ячейка левее кода, кроме «Пояснения».
@ -2018,7 +2035,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

@ -14,7 +14,7 @@
каждый кандидат с изоляцией ошибок. Менять `CandidateResult` на список ломает API у трёх потребителей; разрез на каждый кандидат с изоляцией ошибок. Менять `CandidateResult` на список ломает API у трёх потребителей; разрез на
кандидаты сохраняет шаги 2–8 нетронутыми — каждая часть проходит их как обычный кандидат (свои колонки, периоды, кандидаты сохраняет шаги 2–8 нетронутыми — каждая часть проходит их как обычный кандидат (свои колонки, периоды,
§7). Сегменты для разреза ищутся тем же кодом шага 2 (`_split_and_analyse`), что и для раскладки. §7). Сегменты для разреза ищутся тем же кодом шага 2 (`_split_and_analyse`), что и для раскладки.
- **Условие разреза:** своя шапка + ≥ `min_codes` (3) кодов + другой `document_type` по большинству кодов. Шапка нужна, - **Условие разреза:** своя шапка + первые `min_codes` (3) кодов сегмента — одной формы, отличной от формы текущей части. Шапка нужна,
чтобы не резать по случайным строкам; порог — чтобы пара строк другой формы (OCR-мусор, «пояснения» в хвосте) не чтобы не резать по случайным строкам; порог — чтобы пара строк другой формы (OCR-мусор, «пояснения» в хвосте) не
создавала документ. Тип части — по первому сегменту части с определённым типом. создавала документ. Тип части — по первому сегменту части с определённым типом.
- **Часть `k ≥ 1`:** `type_hint = null`, пустые `evidence` и `warnings` — метка таблицы ИС относится к первой форме, а - **Часть `k ≥ 1`:** `type_hint = null`, пустые `evidence` и `warnings` — метка таблицы ИС относится к первой форме, а
@ -33,9 +33,17 @@
| 2026-11-06 | Текст `NUMBERS_OUTSIDE_TABLE`: «числа вне колонок формы не учитываются», если есть адрес редкого столбца, иначе прежний | Прежний текст «правее таблицы» неверен для столбца посередине; прежние случаи и их golden не меняются | Нулевая: текст предупреждения | | 2026-11-06 | Текст `NUMBERS_OUTSIDE_TABLE`: «числа вне колонок формы не учитываются», если есть адрес редкого столбца, иначе прежний | Прежний текст «правее таблицы» неверен для столбца посередине; прежние случаи и их golden не меняются | Нулевая: текст предупреждения |
| 2026-11-06 | «Редкий»: ≤ 1 число в столбце и ≥ 3 у каждой колонки периода | Калтан: 1 против 10+; столбец с двумя числами уже может быть данными — оставляем аналитику | Низкая: порог — константа пакета | | 2026-11-06 | «Редкий»: ≤ 1 число в столбце и ≥ 3 у каждой колонки периода | Калтан: 1 против 10+; столбец с двумя числами уже может быть данными — оставляем аналитику | Низкая: порог — константа пакета |
| 2026-11-06 | Реальные JSON АК-ЮЛ и Калтана в fixtures не кладём; тесты пакета — на синтетических кандидатах (`tests/builders.py`), прогон реальных файлов — вручную через нормализатор scan-gateway | Файлы заказчика с контура; синтетика воспроизводит обе раскладки строка в строку | Низкая | | 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` не зависит от приватных имён | Нулевая |
## Risks / Trade-offs ## Risks / Trade-offs
- Правило редкого столбца срабатывает и на ББ АК-ЮЛ (прогон 07.11): в сегменте пассива столбец 5 с обрывком «На 2» и
одним числом 46922 (итог 1700 за 31.12.2024, OCR сдвинул его из столбца 6) уходит в `NUMBERS_OUTSIDE_TABLE` (`row:40`).
До изменения этот сегмент сопоставлялся по порядку (`SEGMENT_COLUMNS_MISMATCH`, значения столбца 5 уезжали в колонку
6, настоящие значения столбца 6 терялись); теперь столбец 6 сопоставлен по дате, потеряно одно число, адрес виден
аналитику, жёсткая проверка `1600 = 1700` ловит пустой 1700.
- АК-ЮЛ после разреза: ОФР — отдельный документ, но подписи его колонок 5 («За I п / 20:») и 6 («юлугодие / 25 года») - АК-ЮЛ после разреза: ОФР — отдельный документ, но подписи его колонок 5 («За I п / 20:») и 6 («юлугодие / 25 года»)
ИС тоже разорвала, и ни одна не разбирается как период — столбец 5 не «между периодами», правило редкого столбца не ИС тоже разорвала, и ни одна не разбирается как период — столбец 5 не «между периодами», правило редкого столбца не
срабатывает: обе колонки с `COLUMN_WITHOUT_DATE`, документ `needs_check`, аналитик ставит дату. Это честный результат срабатывает: обе колонки с `COLUMN_WITHOUT_DATE`, документ `needs_check`, аналитик ставит дату. Это честный результат

View File

@ -3,10 +3,12 @@
### Requirement: Шаг 2 — разрез кандидата по смене формы ### Requirement: Шаг 2 — разрез кандидата по смене формы
Перед шагами 2–8 кандидат SHALL разрезаться на части по смене формы (AURORA-1734, решение владельца 06.11.2026). Перед шагами 2–8 кандидат SHALL разрезаться на части по смене формы (AURORA-1734, решение владельца 06.11.2026).
Сегменты кандидата ищутся по правилу «Шаг 2 — сегменты, повторная шапка и границы листов». Тип формы сегмента — по Сегменты кандидата ищутся по правилу «Шаг 2 — сегменты, повторная шапка и границы листов». Тип формы сегмента — по
большинству его кодов (отрезок → `document_type`, как на шаге 6). Новая часть SHALL начинаться с первой строки сегмента, первым `min_codes` кодам (порог словаря диапазонов): все они одной формы (отрезок → `document_type`, как на шаге 6) —
если одновременно: у сегмента своя строка-шапка; кодов в нём не меньше порога `min_codes` словаря диапазонов; тип по это тип, которым сегмент открывается; иначе тип не определён. Новая часть SHALL начинаться с первой строки сегмента,
его кодам определён и отличается от типа текущей части (тип части — по первому сегменту части с определённым типом). если у сегмента своя строка-шапка и он открывается формой, отличной от типа текущей части (тип части — по большинству
Сегменты без шапки, с малым числом кодов или того же типа остаются в текущей части. кодов её первого сегмента с кодами; после разреза — тип, которым открылась часть). Сегменты без шапки, с числом кодов
меньше порога или той же формы остаются в текущей части; строки другой формы, приклеенные к сегменту без своей шапки
(сегмент открывается кодами текущей формы), разреза не дают.
Часть — обычный `FormCandidate` для шагов 2–8: строки сетки с первой строки сегмента до начала следующей части, Часть — обычный `FormCandidate` для шагов 2–8: строки сетки с первой строки сегмента до начала следующей части,
`segment_breaks` — сдвинутые на начало части; `header` — тот же, что у исходного кандидата (шапка файла ИС одна на `segment_breaks` — сдвинутые на начало части; `header` — тот же, что у исходного кандидата (шапка файла ИС одна на
@ -31,6 +33,10 @@
- **WHEN** в таблице ОФР повторная шапка открывает сегмент со строками 2510…2910 - **WHEN** в таблице ОФР повторная шапка открывает сегмент со строками 2510…2910
- **THEN** разреза нет: тип сегмента тот же (`INCOME`), сегмент — продолжение ОФР - **THEN** разреза нет: тип сегмента тот же (`INCOME`), сегмент — продолжение ОФР
#### Scenario: ОФР без своей шапки после пассива
- **WHEN** после повторной шапки ББ идут строки 1310, 1300, 1700, а следом без шапки — 2110, 2120, 2100, 2400
- **THEN** разреза нет: сегмент открывается кодами ББ (как и до изменения)
#### Scenario: Пара строк другой формы #### Scenario: Пара строк другой формы
- **WHEN** после строк ББ повторная шапка открывает сегмент с двумя кодами 2xxx (меньше порога `min_codes`) - **WHEN** после строк ББ повторная шапка открывает сегмент с двумя кодами 2xxx (меньше порога `min_codes`)
- **THEN** разреза нет, сегмент остаётся в части ББ (как раньше) - **THEN** разреза нет, сегмент остаётся в части ББ (как раньше)