# Харнесс eom_check.py: вёрстки и питфоллы


Механическую часть (шаг 1 и подготовка шага 2) делает скрипт, а не руками:

```bash
python scripts/eom_check.py "том.pdf" -o "<папка>" --cipher "МП-18/1604-ИОС1.1"
```

Даёт `report.md` (протокол), `ntd_task.md` (задание для базы), `rows.csv`, `pages.json`.
Ловит: чужие штампы, пропуски/дубли таблиц, арифметику нагрузок, выпавшие из обоих
режимов строки, `Sр=√(Pр²+Qр²)`, cos/tg, перечень НТД с дефектами обозначений.

Обкатан на **трёх** комплектах, три разные вёрстки:

| Комплект | Стр. | Вёрстка | Результат |
|---|---|---|---|
| МП-18/1604 | 78 | A: Кол-во→Ру, сезон, «Итого ВРУ» | 17/17 |
| ШРКП | 50 | B: Ру→Кол-во, ввод×сезон×пожар | 7/7 |
| RGZ 2026.05-01 | 89 | C: **таблица нагрузок пустая**, данные в таблице УКРМ, секции ГРЩ | частично — см. питфоллы |

### Уроки третьего комплекта (вёрстка C)

- **Таблица нагрузок может быть ПУСТЫМ БЛАНКОМ.** Найдены заголовок, 15 пронумерованных
  строк, «Итого» — и ни одного числа. Текст при этом ссылается: «расчёт представлен
  в таблице 1». Скрипт обязан сообщать «таблица найдена и НЕ ЗАПОЛНЕНА» как критическое
  замечание, а не сваливаться в «таблицы не распознаны». Это разные вещи: первое — дефект
  тома, второе — дефект парсера.
- **Данные могут лежать не в таблице нагрузок.** В RGZ числа по вводам нашлись в таблице
  «Расчёт мощности УКРМ». Искать числовые строки нагрузок по всему тому, а не только
  после заголовка «нагрузки».
- **ИТОГ МОЖЕТ БЫТЬ МЕНЬШЕ СУММЫ СТРОК — это признак ошибки, а не нормы.** В RGZ
  ΣРр по строкам 1793,1 против итога 976,5 (на 817 кВт меньше). Сумма нагрузок по вводам
  не может быть меньше каждого слагаемого. При этом √(P²+Q²) итоговой строки сходится
  идеально — значит итог посчитан верно, но собран не из показанных строк.
- **Вёрсток минимум три, универсального парсера не будет.** Нужен режим
  «не распознал — покажи что нашёл», чтобы инженер доразобрал вручную: карта страниц,
  кандидаты таблиц, первая строка каждой. Не притворяться, что разобрал.
- **Повторяющиеся дефекты между комплектами одного проектировщика — проверять первыми.**
  У ГЕНПРОЕКТ в трёх томах подряд: ГОСТ Р 21.101-2020 (отменён 01.04.2026),
  п.8.23 СП 256 как «потери напряжения» (в действительности — про номинальные 220/380 В),
  СП 6.13130.2021 (отменён 29.06.2026). Это шаблон ПЗ, а не случайность: увидел автора —
  смотри эти три места в первую очередь.

### Две вёрстки таблиц нагрузок — обе поддержаны

| | Вёрстка A (МП-18/1604) | Вёрстка B (ШРКП) |
|---|---|---|
| Колонки | № · Наименование · **Кол-во · Ру** · Кс · … | № · Нагрузка · **Ру · Кол-во** · Кс · Кс(авар) · Кснм · … |
| Итоги | «Итого» / «Итого ВРУ с КРМ» | «Итого по вводу N» / «…с КРМ» |
| Структура | одна таблица на режим | режим × ВВОД × (норма/пожар) |
| Iр | нет | есть |

Порядок колонок и наличие Iр определяются по строке-шапке, а НЕ по всему тексту.

### Питфоллы харнесса (все проверены болью)

- **Заголовок таблицы рвётся на две строки**: «Таблица 2 Итоговый расчёт… Режим» +
  «“Зима”». Сезон искать в следующей строке (`pending_season`), иначе таблица получает
  метку `?` и сравнение сезонов не работает.
- **Таблица нагрузок не кончается сама.** После итогов идут таблицы учёта с теми же
  номерами — без явного сброса `current` строки «2 ГРЩ...» прилипают к зимней таблице
  и суммы врут. Сбрасывать на любой другой заголовок «Таблица N».
- **«Режим Пожар» в ОБОИХ таблицах — это норма, а не потеря.** Клапаны ДУ и
  противодымная вентиляция считаются отдельным пожарным режимом. Отделять их от
  настоящих потерь (свой сезон в одной таблице + пожар в другой), иначе в отчёт уйдут
  ложные 171 кВт.
- **Шифр сравнивать по СКЕЛЕТУ (только буквы и цифры).** `МП-18/1604-ИОС1.1` и
  `МП-181604-ИОС1.1` — один объект с потерянным разделителем, а не два разных.
  Сравнение строкой дало 26 ложных «чужих листов» вместо 3 настоящих.
  Дефект «181604» — отдельное замечание по оформлению ГОСТ Р 21.101.
- **Номера НТД рвутся пробелом в любом месте**: `ГОСТ 3214 4-2013`, `ГОСТ 28249- 93`,
  `СНиП 23 -05-95`. Искать по плоскому тексту (переносы → пробел) шаблоном с допуском
  пробелов ВНУТРИ номера, склеивать, и только потом классифицировать. Разбор сразу
  шаблоном «цифры-дефис-цифры» отрезает хвост и рождает ложные «без года».
  При склейке удалять ТОЛЬКО пробелы: снести дефисы/точки — превратить
  `ГОСТ 10434-82` в `ГОСТ 1043482`.
- **Один документ в разных написаниях** (`СП 256.1325800.2016` / `СП256.1325800.2016`)
  объединять по нормализованному ключу, иначе задание в базу пухнет дублями.
- **Рр/Qр/Sр брать С КОНЦА строки, а не по индексу.** Хвост всегда заканчивается
  одинаково: `…, Рр, Qр, Sр, [Iр]`. Фиксированный индекс уезжает на единицу, как только
  в шапке появляется лишняя колонка (Кснм). Сдвиг: с Iр — 4/3/2, без Iр — 3/2/1.
- **Флаг «есть Iр» — только из шапки самой таблицы нагрузок.** Поиск по всему тому
  даёт ложное срабатывание (Iр встречается в другом тексте) и сдвигает всю арифметику.
- **Статус строки в вёрстке B задаётся БЛОКОМ, а не словом в названии.**
  «Лифт пожарных подразделений» содержит «пожар», но это нагрузка нормы — 55 кВт
  выпадали из суммы, давая ложное расхождение ровно 54,9 кВт.
- **Строки «ИТ О ГО :» и подвиды («1-комнатные 73 11») — не нагрузки.**
  Первая — промежуточный итог (квартиры, гигантский Ру ломает сверку), вторые —
  раскладка состава. Обе отсекать по регулярке.
- **Слово в шифре ломается пробелом: «ВВО Д 1» вместо «ВВОД 1».** Регулярка
  `^В\s*В\s*О?\s*Д?\s*(\d)` с якорем конца строки (иначе ловит цифры из таблиц учёта).
- **К шифру в плотном штампе A3 приклеивается имя щита** (`ГРЩШРКП-ГП-ПИР-4-П-ИОС1.1`).
  Сравнивать по ВХОЖДЕНИЮ скелета (свой ⊂ шифр), а не равенством; наивная обрезка
  «до первого дефиса» срезает половину настоящего шифра и даёт 50 ложных чужих листов.
  Настоящий чужой объект — тот, где скелет НЕ содержит наш.
- **Пустая таблица важнее нераспознанной.** Если в таблице есть заголовок и нумерованные
  строки, но нет чисел — это заполненный бланк без данных, критическое замечание.
  Отличать от «текстового слоя нет» (тогда нужен OCR) и от «формат незнакомый».
- **Сумма строк ≠ «Итого», и это нормально.** В вёрстке B «Итого по вводу»
  включает подытог «ИТ О ГО» (квартиры) — его надо ДОБАВИТЬ к сумме строк.
  В вёрстке A квартиры сидят отдельной строкой и уже в Σ — их надо ВЫЧЕСТЬ.
  Коррекцию применять только если она реально уменьшает расхождение, иначе
  пожарные блоки дают ложный остаток.

**Балансовые проверки на двух реальных томах дали 24/24 контрольных совпадений
(17 на вёрстке A + 7 на вёрстке B).**