From c509c5ed76f4949d0955c3f8bbd38df05cdec8bc Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 7 Nov 2026 09:01:31 +0400 Subject: [PATCH] =?UTF-8?q?AURORA-1734/1735:=20=C2=A711=20=E2=80=94=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B7=D1=80=D0=B5=D0=B7=20=D0=BF=D0=BE=20=D1=81?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D0=B5=20=D1=84=D0=BE=D1=80=D0=BC=D1=8B=20?= =?UTF-8?q?=D0=B8=20=D1=80=D0=B5=D0=B4=D0=BA=D0=B8=D0=B9=20=D1=81=D1=82?= =?UTF-8?q?=D0=BE=D0=BB=D0=B1=D0=B5=D1=86=20(RSHB/pylib=5Fai=5Farm=5Fstate?= =?UTF-8?q?ments#5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- context/05-domain-rules.md | 19 ++++++++++++++++++- .../changes/statement-form-split/design.md | 10 +++++++++- .../specs/statement-normalization/spec.md | 14 ++++++++++---- 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/context/05-domain-rules.md b/context/05-domain-rules.md index 18f02f7..e66a99c 100644 --- a/context/05-domain-rules.md +++ b/context/05-domain-rules.md @@ -1689,6 +1689,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`). Сегменты без шапки, с меньшим числом кодов или той же формы (повторная шапка второй + страницы ББ, раздел 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»; «январь – @@ -1754,6 +1764,13 @@ 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 символов); наименование строки — ячейка этого столбца, пуста — ближайшая текстовая ячейка левее кода, кроме «Пояснения». @@ -2018,7 +2035,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/design.md b/openspec/changes/statement-form-split/design.md index a019652..8fbc806 100644 --- a/openspec/changes/statement-form-split/design.md +++ b/openspec/changes/statement-form-split/design.md @@ -14,7 +14,7 @@ каждый кандидат с изоляцией ошибок. Менять `CandidateResult` на список ломает API у трёх потребителей; разрез на кандидаты сохраняет шаги 2–8 нетронутыми — каждая часть проходит их как обычный кандидат (свои колонки, периоды, §7). Сегменты для разреза ищутся тем же кодом шага 2 (`_split_and_analyse`), что и для раскладки. -- **Условие разреза:** своя шапка + ≥ `min_codes` (3) кодов + другой `document_type` по большинству кодов. Шапка нужна, +- **Условие разреза:** своя шапка + первые `min_codes` (3) кодов сегмента — одной формы, отличной от формы текущей части. Шапка нужна, чтобы не резать по случайным строкам; порог — чтобы пара строк другой формы (OCR-мусор, «пояснения» в хвосте) не создавала документ. Тип части — по первому сегменту части с определённым типом. - **Часть `k ≥ 1`:** `type_hint = null`, пустые `evidence` и `warnings` — метка таблицы ИС относится к первой форме, а @@ -33,9 +33,17 @@ | 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` не зависит от приватных имён | Нулевая | ## 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 не «между периодами», правило редкого столбца не срабатывает: обе колонки с `COLUMN_WITHOUT_DATE`, документ `needs_check`, аналитик ставит дату. Это честный результат diff --git a/openspec/changes/statement-form-split/specs/statement-normalization/spec.md b/openspec/changes/statement-form-split/specs/statement-normalization/spec.md index 3dbd082..3f78c51 100644 --- a/openspec/changes/statement-form-split/specs/statement-normalization/spec.md +++ b/openspec/changes/statement-form-split/specs/statement-normalization/spec.md @@ -3,10 +3,12 @@ ### Requirement: Шаг 2 — разрез кандидата по смене формы Перед шагами 2–8 кандидат SHALL разрезаться на части по смене формы (AURORA-1734, решение владельца 06.11.2026). Сегменты кандидата ищутся по правилу «Шаг 2 — сегменты, повторная шапка и границы листов». Тип формы сегмента — по -большинству его кодов (отрезок → `document_type`, как на шаге 6). Новая часть SHALL начинаться с первой строки сегмента, -если одновременно: у сегмента своя строка-шапка; кодов в нём не меньше порога `min_codes` словаря диапазонов; тип по -его кодам определён и отличается от типа текущей части (тип части — по первому сегменту части с определённым типом). -Сегменты без шапки, с малым числом кодов или того же типа остаются в текущей части. +первым `min_codes` кодам (порог словаря диапазонов): все они одной формы (отрезок → `document_type`, как на шаге 6) — +это тип, которым сегмент открывается; иначе тип не определён. Новая часть SHALL начинаться с первой строки сегмента, +если у сегмента своя строка-шапка и он открывается формой, отличной от типа текущей части (тип части — по большинству +кодов её первого сегмента с кодами; после разреза — тип, которым открылась часть). Сегменты без шапки, с числом кодов +меньше порога или той же формы остаются в текущей части; строки другой формы, приклеенные к сегменту без своей шапки +(сегмент открывается кодами текущей формы), разреза не дают. Часть — обычный `FormCandidate` для шагов 2–8: строки сетки с первой строки сегмента до начала следующей части, `segment_breaks` — сдвинутые на начало части; `header` — тот же, что у исходного кандидата (шапка файла ИС одна на @@ -31,6 +33,10 @@ - **WHEN** в таблице ОФР повторная шапка открывает сегмент со строками 2510…2910 - **THEN** разреза нет: тип сегмента тот же (`INCOME`), сегмент — продолжение ОФР +#### Scenario: ОФР без своей шапки после пассива +- **WHEN** после повторной шапки ББ идут строки 1310, 1300, 1700, а следом без шапки — 2110, 2120, 2100, 2400 +- **THEN** разреза нет: сегмент открывается кодами ББ (как и до изменения) + #### Scenario: Пара строк другой формы - **WHEN** после строк ББ повторная шапка открывает сегмент с двумя кодами 2xxx (меньше порога `min_codes`) - **THEN** разреза нет, сегмент остаётся в части ББ (как раньше)