## Context Issue RSHB/arm-tabular-parser#35 (входит #34). Правило — `../docs/context/05-domain-rules.md` §1.1 (реквизит `<группировка>.ИНН`, OQ-162) и §1.3 (ИНН контрагента: колонка → строка ИНН → хвост названия; OQ-100, OQ-130); новое правило заносится в §1.3 и новым OQ в `07-open-questions.md`. Восемь раскладок (ИНН над / под наименованием) × (в дереве / не в дереве) × (с суммами / без сумм). Поведение main на синтетике без подсказки в шапке: | Раскладка | main | |---|---| | над, не в дереве, без сумм | склеивается (OQ-130) | | над, не в дереве, с суммами | ИНН — отдельный контрагент, `needs_check` | | над, в дереве, с суммами | ИНН вместо наименования, `done` | | над, в дереве, без сумм | `needs_check` | | под, в дереве, с суммами | ИНН теряется, `done` (с подсказкой в шапке — склеивается, #32) | | под, в дереве, без сумм | `needs_check` | | под, не в дереве, с суммами | ИНН — отдельный контрагент, `needs_check` | | под, не в дереве, без сумм | **ИНН первого контрагента у второго, `done`** | Проверено при проектировании: стратегии иерархии `outline`, `indent`, `account_pattern` ставят строку ИНН (с суммами и без) на тот же уровень, что и наименования (OQ-130 отключался), то есть дерево до склейки корректно; стратегия `sums` не проверялась. Контрольная сумма ИНН на golden: реальные ИНН (≈ 5 852) верны, кроме трёх (физлица: Цимлянский, ОСВ 76, `611801003692`; Новопетровская `233370449085`; НПЗ `614002279560`), вся синтетика (`5001234567`, `7701234567`, `7812345678` и др.) неверна. ## Decisions Решения владельца в brainstorming 13.10.2026: 1. **Подход A — один проход по дереву.** Новый модуль `api/src/parser/inn_rows.py` работает между `resolve_levels` и `classify_rows`. Он заменяет `_glue_inn_rows` (OQ-130, до иерархии) и `glue_inn_attributes` (#32, после классификации). Классификатор получает дерево без строк ИНН и ИНН для строк-наименований. 2. **Контрольная сумма.** Без подсказки в шапке строка — строка ИНН только при верной контрольной сумме ИНН (алгоритм ФНС: 10 цифр — весовые коэффициенты 2, 4, 10, 3, 5, 9, 4, 6, 8; 12 цифр — два контрольных разряда, 7, 2, 4, 10, 3, 5, 9, 4, 6, 8 и 3, 7, 2, 4, 10, 3, 5, 9, 4, 6, 8), во всех восьми раскладках, включая прежнюю OQ-130. С подсказкой `<группировка>.ИНН` контрольная сумма не проверяется (файл банка #32 не меняется). 3. **Неверная контрольная сумма.** Строка из 10/12 цифр (по `inn_only_row`) рядом с наименованием, не прошедшая контрольную сумму без подсказки, — не строка ИНН: классифицируется как обычно, предупреждение «строка N похожа на ИНН …, контрольная сумма не сходится», документ `needs_check`. 4. **Ведущий ноль (#34).** Числовая ячейка строки ИНН из 9/11 цифр дополняется нулём слева (как R-24 для колонки), контрольная сумма проверяется после дополнения. 5. **«Над, в дереве»** — склейка только с единственным ребёнком; несколько детей — не склеивать, `needs_check`. 6. **Направление для соседей** (предложено в дизайне, не оспорено): пары с суммами — по совпадению сумм; без сумм — по направлению документа: порядок в шапке → пары по суммам → однозначные блоки. Противоречие или нет однозначных блоков — не склеивать, `needs_check`. 7. **«Под, в дереве»** — несколько строк ИНН под одним наименованием — `needs_check` (как #32). 8. **Строка ИНН — только ИНН**: 10/12 цифр числом или текстом, «ИНН …», «ИНН/КПП» (`inn_only_row`); счёт строкой ИНН не бывает. 9. **Синтетика для отказа «неверная контрольная сумма»** (решение владельца 13.10.2026, H-1): «синтетика для отказа «неверная контрольная сумма» — да». Файл `s28_inn_bad_checksum.xlsx`: раскладка `s20` («над, не в дереве, без сумм»), у ООО Альфа строка ИНН `7701234567`; ожидание — строка остаётся обычной, ИНН у Альфы нет, предупреждение «похожа на ИНН», `needs_check`, остальные контрагенты получают свои ИНН (направление — по блоку 60.02). 10. **Противоречие раскладки и подсказки шапки** (решение владельца 13.10.2026, H-2): «Противоречие раскладки и подсказки шапки — СКЛЕИВАТЬ, НО ПОДСВЕЧИВАТЬ». Пара найдена, но ИНН стоит не там, где указывает подсказка `<группировка>.ИНН` (в дереве или соседом по суммам) — склейка как обычно, отдельное предупреждение «строка N: ИНН … стоит <под/над> наименованием в строке M, а шапка («…») указывает <над/под> — проверьте», `ok = False`, `needs_check`. Закрывает T2.12. ### Алгоритм `inn_rows.py` Вход: строки `BodyRow`, `Levels` (уровень и родитель), подсказка шапки (`InnHint`: есть ли `<группировка>.ИНН` и направление по порядку элементов), схема группировок `group_schema` (для защиты, п. «Защита» ниже), словари. Выход: строки и `Levels` без склеенных строк ИНН, ИНН для строк-наименований (`row → inn`), множество строк ИНН без пары (классификатор ставит им `OTHER`), предупреждения, признак «всё склеено». 1. **Кандидаты.** Строка ИНН — не счёт, не «Итого», не строка Вал./Кол.; подпись совпадает с `inn_only_row` (числовая ячейка из 9/11 цифр — с ведущим нулём); без подсказки ИНН проходит контрольную сумму (иначе п. 3 решений). С подсказкой пустая подпись строки-ребёнка наименования (только положение «под, в дереве») — тоже кандидат, «ИНН не указан» (R-1 #32). 2. **Наименование** — строка не счёт, не «Итого», не Вал./Кол., не кандидат. 3. **Положение кандидата** (по `Levels`): - есть дети → **над, в дереве**: пара — единственный ребёнок-наименование; он и его поддерево поднимаются на уровень вверх, родителем становится родитель кандидата; - родитель — наименование → **под, в дереве**: пара — родитель; дети кандидата переходят к родителю; - иначе (родитель — счёт или корень, детей нет) → **не в дереве**: пара — сосед того же родителя, непосредственно выше или ниже. Связь в дереве важнее соседства. 4. **Суммы.** Если у кандидата есть суммы (`_has_sums`), шесть значений MUST совпасть с суммами пары (`None` = 0); иначе пары нет. 5. **Соседи с суммами**: пара — сосед-наименование с равными суммами; совпали оба соседа — по направлению документа. 6. **Направление документа** (лист ОСВ): подсказка шапки (п. 1), если она есть; иначе сведения пп. 2–3 вместе: 1. подсказка шапки: `<группировка>.ИНН` после группировки — под; перед — над; 2. пары соседей, найденные по суммам (п. 5, один совпавший сосед); 3. блоки: последовательность детей одного родителя из кандидатов «не в дереве» (I) и наименований (N), ещё не сопоставленных; начинается с I и не кончается I — над; кончается I и не начинается I — под; начинается и кончается I — блок сломан (его кандидаты без пары); ни то ни другое — нет сведений. Сведения пп. 2–3 противоречат друг другу или их нет — направление неизвестно. 7. **Соседи без сумм**: пара — соседнее наименование по направлению (над → следующий сосед, под → предыдущий). Направление неизвестно — пары нет. 8. **Конфликты**: два кандидата претендуют на одно наименование — оба без пары. ИНН в колонке ИНН у наименования отличается от ИНН строки — колонка побеждает, предупреждение, `needs_check`. ИНН в хвосте названия — строка ИНН побеждает (прежний порядок §1.3). 9. **Склейка**: строка ИНН уходит из строк и `Levels`, ИНН — наименованию, предупреждение «строка N: ИНН … отнесён к контрагенту в строке M» (пустой при подсказке — «ИНН пуст … без ИНН»). Если раскладка противоречит подсказке шапки (пара выше/ниже, чем указывает подсказка) — следом отдельное предупреждение о противоречии, `ok = False` (решение владельца 10, H-2). 10. **Без пары**: строка остаётся в `rows`, `group_type = OTHER` (в свод не попадает, суммы не задваиваются, как #32), предупреждение «строка N: ИНН … без пары — <причина>», признак «всё склеено» = `False` → документ `needs_check`. **Защита «над, в дереве».** Контрагент, названный ИНН, с одним договором неотличим по форме от строки ИНН над контрагентом. Проверка — по листу: если `group_schema` содержит больше одной группировки и ни одна строка листа (кроме Вал./Кол.) не стоит глубже схемы (позиция — число не-счётных предков после последнего счёта + 1 — не больше `len(group_schema) − 1`), лишнего уровня в листе нет: кандидаты «над, в дереве» не склеиваются, без предупреждения классифицируются по схеме. Схема из одного ACCOUNT — проверки нет, по пп. 1–10. **Нераспознанный реквизит при подсказке «под».** С подсказкой «под» (#32) единственный ребёнок наименования с теми же суммами, подпись которого не ИНН и не пуста («нерез.»), — строка реквизита с нераспознанным ИНН: без пары, `OTHER`, «ИНН не распознан», `needs_check` (поведение #32 сохраняется). ### Изменения в коде - `inn_rows.py` — новый модуль (пп. 1–10); контрольная сумма ИНН — `numbers.py` (`inn_checksum_ok`). - `header.py`: `InnAttribute` и позиции-реквизиты удаляются; `GroupSchema` получает подсказку (`inn_hint`: владелец и направление); `<группировка>.ИНН` распознаётся и **перед** группировкой-владельцем (сейчас только после). - `classify.py`: удаляются `_glue_inn_rows`, `glue_inn_attributes`, `_not_glued_reason`, поля `is_inn_attribute`, `attribute_inn`; `_classify_row` берёт ИНН строки из результата `inn_rows` вместо `inn_above` и ставит `OTHER` строкам без пары. - `hierarchy.py`: поле `BodyRow.inn_above` удаляется. - `osv.py`: вызов `inn_rows` между `resolve_levels` и `classify_rows`; признак «всё склеено» и предупреждения — в статус и `warnings`. ### Тесты - Юнит-тесты `inn_rows.py` на деревьях вручную: распознавание (10/12, число/текст, «ИНН …», «ИНН/КПП», ведущий ноль), контрольная сумма с подсказкой и без, все отказы пп. 4–8 и 10, защита «над, в дереве», направление (шапка, суммы, блоки, противоречие, нет сведений, сломанный блок), расхождение с колонкой, R-1. - Синтетика `s20`…`s27` (по одной на раскладку, ИНН с верной контрольной суммой, шапка без подсказки), golden — после ручной проверки: ИНН у всех контрагентов, итоги сходятся, `done`. - `s18`: ИНН с верной контрольной суммой (только в `s18`, общие узлы генератора не меняются). - Сравнение результатов main и ветки на всех файлах fixtures и синтетике; ожидаемые отличия — `s18`, новые `s20`…`s27`, файлы со строкой ИНН с неверной контрольной суммой; Новопетровская без изменений. Каждое отличие согласуется с владельцем до `make golden-update`. ## Decisions made without discussion | # | Дата | Решение | Обоснование | Цена ошибки | |---|---|---|---|---| | 1 | 13.10.2026 | Защита «над, в дереве»: при схеме из двух и более группировок склейка только если в листе есть строки глубже схемы (проверка по листу, не по поддереву) | Контрагент, названный ИНН, с одним договором иначе потерял бы договор (договор стал бы контрагентом) | При ветке глубже схемы контрагент, названный ИНН, с одним договором отдаёт ИНН договору без `needs_check` (защита работает только без подсказки и только когда лист не глубже схемы); файл с подсказкой-схемой, где 1С не добавила уровень, — строка ИНН без пары, `needs_check`; правка условия | | 2 | 13.10.2026 | Строка ИНН без пары — `group_type = OTHER` (в `rows`, не в своде) | Как #32: суммы строки ИНН с суммами иначе задвоились бы в своде ядра; документ всё равно на проверке | Аналитик не видит такую строку контрагентом и сопоставляет вручную; поменять тип — одна строка | | 3 | 13.10.2026 | ИНН колонки расходится с ИНН строки — побеждает колонка, `needs_check` | Колонка — явный реквизит отчёта, строка — вывод по соседству | Неверный ИНН у контрагента до проверки аналитиком; документ на проверке | | 4 | 13.10.2026 | Направление определяется на лист ОСВ (документ), не на файл и не на счёт | Один лист — один отчёт 1С с одной настройкой вывода | Смешанные раскладки в одном листе — `needs_check` вместо склейки | | 5 | 13.10.2026 | Подсказка шапки задаёт направление сильнее пар по суммам; пары по суммам склеиваются независимо от направления | Суммы однозначно указывают пару; шапка — явная настройка отчёта | Противоречие шапки и сумм не поднимается как ошибка | | 6 | 13.10.2026 | Суммы сравниваются с `None` = 0 (в #32 — точное равенство кортежей) | Пустая ячейка и 0 в ОСВ 1С значат одно и то же | Пара по суммам у строк, различающихся только пустотой ячейки; на Новопетровской не влияет (проверяется golden) | | 7 | 13.10.2026 | Соседом считается только непосредственный сосед в списке детей того же родителя | Иначе сопоставление через чужие строки неоднозначно | ИНН через строку от наименования не склеивается, `needs_check` | | 8 | 13.10.2026 | Пустая подпись — кандидат только с подсказкой и только в положении «под, в дереве» (R-1 #32) | Без подсказки пустая строка ничего не значит; другое положение у пустого ИНН в 1С не встречалось | Пустая строка ИНН «над» при подсказке — остаётся наименованием без текста | | 9 | 13.10.2026 | С подсказкой «под» единственный ребёнок наименования с теми же суммами и непустой подписью не-ИНН — нераспознанный реквизит: `OTHER`, `needs_check` | Сохраняет поведение #32 (тест «нерез.»): в 1С на этом месте стоит строка реквизита | Контрагент с одним договором с теми же суммами в файле с подсказкой «под», но без строки реквизита — `needs_check` вместо `done` | | 10 | 13.10.2026 | Строка ИНН с детьми под наименованием: подсказка «под» — родителю (дети — договоры); иначе — родителю, если он по схеме `COUNTERPARTY` (при подсказке «над» — с подсветкой H-2, R-7, design #23), иначе ребёнку («над») | Иначе строка реквизита с договорами под ней (#32 со схемой «Контрагенты, Контрагенты.ИНН, Договоры» без подсказки) отдала бы ИНН договору; строка ИНН между подразделением и контрагентом — «над» | Файл без подсказки и без схемы, где ИНН под контрагентом и у строки ИНН есть дети, — ИНН уйдёт ребёнку; `needs_check` не ставится | | 11 | 13.10.2026 | Task 1 (план): `__all__` модуля `numbers.py` дополнен `inn_checksum_ok` и `row_inn`; тело `inn_checksum_ok` при 12 цифрах — два булевых локала вместо одного `and`-выражения (ruff format не принял переносы из плана) | Публичные имена модуля экспортируются по соглашению файла; результат проверки не меняется | Нет: тест `test_inn_checksum_ok` покрывает оба разряда | | 12 | 13.10.2026 | `inn_checksum_ok`: проверка «только цифры» через `isascii() and isdigit()`, не `isdecimal()` (ревью T1.2) | `isdecimal()` пропускает не-ASCII десятичные цифры, а ИНН — только `[0-9]`; `isdigit()` без `isascii()` пропускает «²» | Нет: ИНН с не-ASCII цифрами не встречаются; такая строка даёт `False` и не становится ИНН | | 13 | 13.10.2026 | В `classify.py` локальная переменная `row_data_type` в `_classify_row` переименована в `data_type` (при публичной функции `row_data_type` иначе имя стало бы локальным и вызов упал с `UnboundLocalError`) | Переименование локальной переменной, поведение не меняется | Нет | | 14 | 13.10.2026 | R-1 (design #14, T2.1): «нераспознанный реквизит» при подсказке «под» — только ребёнок-лист (без детей в дереве); уточнено R-6 (design #22) | Ребёнок со своими детьми — подлинное наименование со своей строкой ИНН; иначе единственный контрагент подразделения с теми же суммами терял ИНН (уходил в OTHER) | Реквизит «нерез.» с дочерними строками (в 1С не встречалось) станет наименованием, а не `needs_check` | | 15 | 13.10.2026 | R-2 (design #15, T2.2): строка ИНН без детей под наименованием — пара родителю только при подсказке «под», либо если родитель по схеме COUNTERPARTY, либо схема не позволяет сказать (позиция вне схемы); иначе — сосед (`flat`) | Без этого ИНН соседа контрагента под подразделением отдавался подразделению; тот же помощник позиции в схеме, что в №10 | Файл без подсказки с ИНН-ребёнком подразделения и схемой, не совпадающей с реальностью, — строка в соседях, при неопределённом направлении без пары и `needs_check` | | 16 | 13.10.2026 | R-3 (design #16, T2.3): защита «над, в дереве» (нет лишнего уровня) применяется только без подсказки в шапке | Подсказка `<группировка>.ИНН` — прямое свидетельство дополнительного уровня 1С | Подсказка «над» в шапке при реально отсутствующем уровне: контрагент, названный ИНН, склеится с ребёнком; документ — на проверку не попадает | | 17 | 13.10.2026 | Направление листа без подсказки: сведения пар по суммам и блоков объединяются; противоречие — направление неизвестно (строки ИНН без пары). Спека («первое определённое») читается так же, как design §6 | Design §6 прямо задаёт «противоречат — неизвестно»; «первое» по порядку источников дало бы склейку, которой блоки противоречат | Лист с парами по суммам против блоков — строки ИНН без пары и `needs_check` вместо склейки по сумме; правка объединения — одна функция `_pair_flat` | | 18 | 13.10.2026 | Task 4: параметризованный `test_header_status_and_hierarchy[s18_inn_row_above.xlsx]` (ожидает `done`) и golden s18 не правятся в Task 4 — оба падают только из-за ИНН s18 с неверной контрольной суммой и чинятся вместе с файлом в Task 5–6; в коммите упавшие тесты выведены из прогона (`--deselect`), `test_s18_…` помечен strict xfail | Бриф Task 4 прямо запрещает обновлять golden; ожидание `done` в параметризации относится к тому же файлу | Если Task 5 забудет снять xfail/починить параметризацию, strict xfail упадёт (XPASS) и напомнит | | 19 | 13.10.2026 | H-2: направление фактической раскладки определяется по номерам строк пары (строка пары выше строки ИНН — ИНН «под», ниже — «над»), одинаково для пар в дереве и соседей; проверка выполняется только при подсказке и только для склеенных пар с непустым ИНН и без расхождения с колонкой ИНН | Номер строки однозначно отражает порядок на листе при любом пути поиска пары; пары без ИНН, без пары и с колонкой-победителем уже подсвечены своими предупреждениями | Нет: предупреждение лишь добавляется к уже состоявшейся склейке; результат склейки не меняется | | 20 | 13.10.2026 | H-1: в s28 строка «7701234567» после классификации попадает в `rows` как контрагент (обычная строка по правилам классификатора) — тест фиксирует это «как есть» и проверяет её наличие среди контрагентов без ИНН | Спека: строка «классифицируется как обычно»; подгонки парсера нет | Нет: поведение соответствует ожиданию владельца | | 21 | 13.10.2026 | Task 7: в tasks.md добавлены пункты 1.5 (H-2) и 2.2 (s28, H-1) сверх брифа; число golden в CLAUDE.md пересчитано по факту (123: 72 + 23 + 28, ранее значилось 113 = 72 + 22 + 19) | Бриф писался до H-1/H-2; счёт — по `find` | Нет: только документация | | 22 | 13.10.2026 | R-6 / I-1 (design #22): `_unrecognised_attributes` — ребёнок-реквизит только у наименования, чья позиция по схеме COUNTERPARTY; при такой позиции условие «ребёнок — лист» снимается (строка «нерез.» с договорами → OTHER, договоры остаются договорами); позиция по схеме не COUNTERPARTY — ребёнок не реквизит; позиции нет — прежнее условие «лист» | Иначе (регрессия #32) единственный контрагент подразделения с теми же суммами получал «ИНН не распознан» | Контрагент с единственным договором тех же сумм при подсказке «под» без строки реквизита — `needs_check` (как в №9) | | 23 | 13.10.2026 | R-7 / I-2 (design #23): `_owned_by_parent`: подсказка «под» — родитель; иначе родитель, если по схеме COUNTERPARTY; подсказка «над» не уводит ИНН ребёнку-договору | По спеке пара — родитель; противоречие подсказке ловит H-2 (предупреждение, `needs_check`) вместо молчаливой потери ИНН | Подсказка «над» и реальный ИНН над контрагентом, у которого в схеме родитель — контрагент: ИНН уходит родителю с предупреждением, `needs_check` | | 24 | 13.10.2026 | R-8 / I-3 (design #24): страховка в `osv.py` после классификации: ИНН склеен со строкой, не ставшей COUNTERPARTY, — предупреждение «ИНН … отнесён к строке M, но она не контрагент — проверьте», `needs_check` | ИНН иначе пропадает молча (подразделение/договор не хранят ИНН) | Лишнее `needs_check` на редкой раскладке; склейка не отменяется | | 25 | 13.10.2026 | R-9 / M-1 (design #25): `_without` — новый родитель = ближайший неудалённый предок по прежнему `levels.parent`, а не пересчёт `parents()` по уровням | Пропуск уровней outline давал неверного родителя | Нет: уровни (`by_row`) считаются как прежде | | 26 | 13.10.2026 | R-10 / T2.5 (design #26): порядок `_drop_conflicts` после `_check_sums` оставлен; формулировка спеки: «две строки ИНН с одной парой, прошедшие проверку сумм, — обе без пары» | Строка, не прошедшая суммы, пары не претендует; сохранение порядка не меняет результаты golden | Две строки ИНН с одной парой, одна из которых не прошла суммы: вторая склеивается | | 27 | 13.10.2026 | H.1 финального ревью отклонено как недостижимое: пустой кандидат бывает только «под, в дереве» и всегда склеивается с родителем, расхождение с колонкой для него невозможно (комментарий в `_result`). H.2: тест на сочетание «расхождение с колонкой + противоречие подсказке» — одно предупреждение о расхождении | Недостижимый код не добавляется | Нет |