docs(#116): финальная версия дизайна — решения Р-16…Р-19, находки W-1…W-16 и H-1…H-26, текст очищен от маркеров ревью

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
This commit is contained in:
Andrey Epifancev 2026-10-17 21:35:59 +04:00
parent f55f05efa1
commit 43b4b808ae

View File

@ -1,8 +1,9 @@
# Единый алгоритм нормализации ББ/ОФР (scan-gateway и tabular-parser) — дизайн
Issue: arm-ka-docs#116. Статус: целевая картина по шагам 1–9 согласована с владельцем (решения Р-1…Р-15, 17.10.2026);
внесены находки двух раундов ревью: F-1…F-25 (`review-2026-10-17.md`), V-1…V-13 (`review-2026-10-17-closure.md`),
G-1…G-30 (`review-2026-10-17-blind.md`); следующий шаг — спека (дельты OpenSpec, контракт v2.0, §11/§12 в
Issue: arm-ka-docs#116. Статус: **финальная версия** — целевая картина по шагам 1–9 согласована с владельцем
(решения Р-1…Р-19, 17.10.2026) и прошла три раунда независимого ревью (`review-2026-10-17.md`,
`review-2026-10-17-closure.md`, `review-2026-10-17-blind.md`, `review-2026-10-17-closure-2.md`,
`review-2026-10-17-blind-2.md`); следующий шаг — спека (дельты OpenSpec, контракт v2.0, §11/§12 в
`05-domain-rules.md`, DBML v5.19).
## Цель
@ -136,7 +137,7 @@ json: ИНН — только цифры, организация, дата (дв
| № | | scan-gateway | tabular-parser |
|---|---|---|---|
| 1 | Что считается кодом при поиске столбца | любое целое число (`int(text)`) | 4–5 цифр в диапазоне формы (словарь) |
| 1 | Что считается кодом при поиске столбца | столбец — по справочнику кодов (`infer_layout`), строка — любое целое (`int(text)`) | 4–5 цифр в диапазоне формы (словарь) |
| 2 | Повторная шапка внутри таблицы | перезаписывает раскладку | не ищется (одна раскладка на блок) |
| 3 | Дата колонки | «размером с дату» + двойники + `parse_date` | словарь периодов `period_end` |
| 4 | Без шапки | синтез шапки, колонки без дат, предупреждение | колонки по числам, подпись пустая |
@ -318,73 +319,74 @@ sheet:<A>,<B>`. `DISCLOSURE` и форма без колонки кодов —
## Целевая картина
Каждый шаг ниже: **вход = выход предыдущего шага**, единый выход для обоих сервисов, список «различие → к чему
приводим → что меняется в scan-gateway → что меняется в tabular-parser». Решения владельца помечены датой; сводка
решений Р-1…Р-15 — в конце (все приняты 17.10.2026). Находки ревью внесены в текст с маркерами: первый раунд F-1…F-25
(`review-2026-10-17.md`), второй раунд V-1…V-13 (`review-2026-10-17-closure.md`) и G-1…G-30
(`review-2026-10-17-blind.md`). Имена полей — рабочие, уточняются при написании спеки.
приводим → что меняется в scan-gateway → что меняется в tabular-parser». Решения владельца собраны в конце
(Р-1…Р-19, все приняты 17.10.2026). Текст прошёл три раунда независимого ревью (`review-2026-10-17*.md` рядом с этим
файлом); находки внесены. Имена полей — рабочие, уточняются при написании спеки.
Принципы, из которых выведены решения по строкам:
1. **Один выход на шаг.** Код, зависящий от источника, живёт только в шаге 1 (адаптеры «json ИС → кандидат», «лист
Excel → кандидат»). Шаги 2–8 — общий код с общими словарями (форма общего кода — Р-1).
Excel → кандидат»). Шаги 2–8 — общий код с общими словарями (форма общего кода — Р-1). Единственное место, где
общий код смотрит на источник, — разбор запятой и точки в сумме (шаг 4).
2. **Ничего не выбрасывать молча.** Строка, колонка или ячейка, которую не удалось истолковать, идёт дальше со
статусом, а не превращается в счётчик; пользователь её видит и исправляет. Положение в источнике (`ref`), порядок
строк (`row_index`) и сырой текст ячейки (`text`) доезжают до базы и экрана (F-4, G-3). Подтвердить документ, не
разобрав строки с суммами без кода, нельзя (Р-13).
строк (`row_index`) и сырой текст ячейки (`text`) доезжают до базы и экрана. Подтвердить документ, не разобрав
строки с суммами без кода, нельзя (Р-13).
3. **Качество — структурно и с привязкой к месту:** уверенность ячейки (от источника), `is_recognized` (разбор числа),
статус строки, нарушения §7 по ячейкам, предупреждения с уровнем и привязкой. Уверенность, `is_recognized` и §7
сервис сводит в один признак ячейки `needs_review` (шаг 8) и из него, статусов строк и предупреждений выводит
статус документа. Ядро при загрузке не пересчитывает ни проверки, ни статус, ни `needs_review`: хранит их как
пришли и добавляет только предупреждения `info` по своему словарю строк (G-26). После правок аналитика ядро считает
тем же общим кодом, добавляя своё знание: правка или подтверждение ячейки снимает `needs_review`, акцепт снимает
жёсткие нарушения, квартал периода и статус конверта остаются условиями `RECOGNIZED` (F-8, F-18, G-7, G-12).
сервис сводит в один признак `needs_review` у значения, строки и поля шапки (шаг 8) и из них выводит статус
документа. Ядро при загрузке не пересчитывает ни проверки, ни статус, ни `needs_review`: хранит их как пришли и
добавляет только предупреждения `info` по своему словарю имён. После правок аналитика ядро считает тем же общим
кодом, добавляя своё знание: правка или подтверждение снимает `needs_review`, акцепт снимает жёсткие нарушения,
квартал периода, сверка ИНН и наименований с справочником — условия `RECOGNIZED`.
4. **Один словарь — одно место:** проверки §7 (полный и упрощённый наборы), периоды, заголовки форм, упорядоченные
перечни кодов форм по редакциям (без наименований) — в общем коде сервисов; соответствие «код → наименование» —
только в ядре, сервисы его не знают и отдают распознанный текст (Р-3). Порядок и состав редакций ядро берёт из
своей копии общего кода, словарь ядра хранит только имена; `sync-statements --check` сверяет, что все коды словаря
ядра есть в перечне (V-11).
перечни кодов форм по редакциям и вариантам (без наименований), диапазоны кодов — в общем коде сервисов;
соответствие «код → наименование» — только в ядре, сервисы его не знают и отдают распознанный текст (Р-3). Порядок и
состав редакций ядро берёт из своей копии общего кода, словарь ядра хранит только имена; `sync-statements --check`
сверяет, что все коды словаря ядра есть в перечне.
5. **Очистка OCR (§11.1) — не отдельный шаг, а первая стадия типовых разборов текста** в общем коде, там, где уже
известен смысл ячейки: дата шапки (шаг 1), подписи колонок (шаг 2), код строки и разлепление склеенных строк
(шаг 3), сумма (шаг 4). Буквы-двойники («О» → 0, «З» → 3, «б» → 6, «S» → 5, «I», «l», «|» → 1) заменяются в датах,
подписях, кодах и суммах — **решение владельца 17.10.2026 (Р-11)**, отменяет §11.1 п. 1 и решение № 22 среза 3a
(«только даты»): сумма или код с заменой получает `is_recognized = false`, а дальше её судьбу решают контрольные
соотношения §7 (Р-7 с оговорками F-1, F-2). ИНН двойники не трогают: только цифры, как сейчас — иначе «3|1 21 2|»
из aep_3 даёт лишние единицы и `MISMATCH` с ЮЛ (G-18). Разбор вызывается для любой ячейки без `value`; у Excel он
срабатывает только на текстовых ячейках и почти всегда ничего не меняет. Из `normalizer/ocr/` шлюза `text`,
подписях, кодах и суммах (Р-11, отменяет §11.1 п. 1 и решение № 22 среза 3a «только даты»): сумма с заменой
получает `is_recognized = false`, код с заменой считается неуверенным (эффективная уверенность ниже 100), а дальше
их судьбу решают контрольные соотношения §7 и структурная проверка кода. В суммах крайние «|» (след границы
таблицы: «| : 122477») убираются, а не заменяются. ИНН двойники не трогают: только цифры, как сейчас — иначе
«3|1 21 2|» из aep_3 даёт лишние единицы и `MISMATCH` с ЮЛ. Разбор вызывается для любой ячейки без `value`; у Excel
он срабатывает только на текстовых ячейках и почти всегда ничего не меняет. Из `normalizer/ocr/` шлюза `text`,
`dates`, `amounts` становятся общими разборами, `layout`, `rows`, `header`, `document` — шагами 2–6 общего кода.
### Шаг 1 — Подготовка данных
**Решения владельца (17.10.2026):** 1) кто размечает — различие остаётся; 2) на выходе одинаковый контракт;
3) шапка своя у каждой таблицы; 4) тип формы — единый подход; 5) уверенность есть и у парсера, всегда 100;
6) парсер склеивает продолжения в шаге 1; 7) ячейка — один формат.
**Решения владельца:** 1) кто размечает — различие остаётся; 2) на выходе одинаковый контракт; 3) шапка своя у
каждой таблицы; 4) тип формы — единый подход; 5) уверенность есть и у парсера, всегда 100; 6) парсер склеивает
продолжения в шаге 1; 7) ячейка — один формат.
**Вход:** байты файла (шаг 0).
**Единый выход:** список кандидатов в форму на файл, `FormCandidate`:
- `source` — `ocr` | `excel`;
- `locator` — откуда взят: `file:<имя json>;table:<i>` (как в §11 и golden-эталонах, F-23) / `sheet:<A>[,<B>]`;
- `locator` — откуда взят: `file:<имя json>;table:<i>` (как в §11 и golden-эталонах) / `sheet:<A>[,<B>]`;
- `type_hint` — `BALANCE` | `INCOME` | `DISCLOSURE` | `null`, по единому правилу (п. 4);
- `evidence` — ОКУД и заголовок как ячейки (для сверки с кодами в шаге 6);
- `header` — поля `header` контракта (`organization_name`, `inn`, `report_date`, `units`, `okud`, `okpo`, `okopf`,
`okfs`, `address`), каждое — ячейка или `null`;
- `rows` — плотная сетка строк × ячеек; все страницы и листы формы уже в одной сетке; пустая позиция — `null`;
- `segment_breaks` — номера строк сетки, с которых начинается новый лист Excel (у OCR пусто): граница листа — всегда
граница сегмента раскладки (шаг 2, F-16);
граница сегмента раскладки (шаг 2);
- `warnings` — предупреждения шага (формат — шаг 8).
Ячейка `Cell` одна для шапки и таблицы: `text` (как показывает источник), `value` (число или дата, если источник их
знает; у OCR — `null`), `confidence` 0–100 (у Excel — 100), `ref` — позиция в источнике, уникальная в документе
(G-9): OCR — `row:<row_index>`, при разлеплении строки (шаг 3 п. 2) — `row:<row_index>#<k>`; Excel — `<лист>!<A1>`.
знает; у OCR — `null`), `confidence` 0–100 (у Excel — 100), `ref` — позиция в источнике: OCR — `row:<row_index>`,
Excel — `<лист>!<A1>`. Уникальность в документе требуется от `ref` строки (шаг 3), не ячейки.
- **1. Кто размечает** — остаётся, фиксируется полем `source`
- scan-gateway: без изменений
- tabular-parser: без изменений
- **2. Единица разметки** — файл → список кандидатов; шаги 2–8 работают только с кандидатом (Р-1 — форма общего кода)
- **2. Единица разметки** — файл → список кандидатов; у OCR каждая таблица ИС — кандидат, у Excel — каждый блок формы на листе; шаги 2–8 работают только с кандидатом (Р-1 — форма общего кода)
- scan-gateway: адаптер «таблица json → кандидат»
- tabular-parser: адаптер «блок формы на листе → кандидат»
- **3. Шапка** — у каждого кандидата своя; поля = поля `header` контракта, каждое — `Cell`; разбор текста поля — общий код по `text`: ИНН → только цифры, без двойников (G-18); дата → словарь периодов (шаг 2 п. 3), который включает все шаблоны сегодняшнего `dates.py` шлюза (G-4); единицы → словарь
- **3. Шапка** — у каждого кандидата своя; поля = поля `header` контракта, каждое — `Cell`; разбор текста поля — общий код по `text`: ИНН → только цифры, без двойников; дата → словарь периодов (шаг 2 п. 3), который включает все шаблоны сегодняшнего `dates.py` шлюза; единицы → словарь. Уверенность и сырой текст каждого поля доезжают до контракта (`*_confidence`, `*_text`)
- scan-gateway: шапку json копирует в каждую свою таблицу
- tabular-parser: добавляет сырой текст и уверенность 100
- **4. Тип формы** — одно правило: ОКУД → заголовок по одному словарю → `null`; окончательно — по кодам строк в шаге 6, у обоих
@ -393,9 +395,9 @@ sheet:<A>,<B>`. `DISCLOSURE` и форма без колонки кодов —
- **5. Уверенность** — поле `Cell.confidence`; Excel всегда 100
- scan-gateway: без изменений
- tabular-parser: добавляет поле
- **6. Многостраничная форма** — одна форма = одна сетка на выходе шага 1; строки продолжения дописываются в конец сетки по маркеру «Форма … с.2» или по позиции листа, граница листа запоминается в `segment_breaks`; тип из маркера сверяется с `type_hint`, расхождение — отдельный кандидат + предупреждение `error`. Лист без маркера и без шапки с одним-двумя кодами присоединяется только если его коды в диапазоне `type_hint` открытой формы (как `_weak_blocker` сейчас, G-8); иначе — отдельный кандидат. Раскладка у каждого сегмента своя (шаг 2 п. 2), поэтому разная ширина колонок или объединённые ячейки на листе-продолжении без шапки не сдвигают значения (F-16); число колонок сегментов сверяется там же
- **6. Многостраничная форма** — одна форма = одна сетка на выходе шага 1; строки продолжения дописываются в конец сетки по маркеру «Форма … с.2» или по позиции листа, граница листа запоминается в `segment_breaks`; тип из маркера сверяется с `type_hint`, расхождение — отдельный кандидат + предупреждение `error`. Лист без маркера и без шапки присоединяется, только если его коды в диапазоне открытой формы (как `_weak_blocker` сейчас) и, при трёх и более кодах, число колонок сумм совпадает с формой; иначе — отдельный кандидат с предупреждением `error`. Раскладка у каждого сегмента своя (шаг 2 п. 2), поэтому разная ширина колонок или объединённые ячейки на листе-продолжении без шапки не сдвигают значения
- scan-gateway: без изменений
- tabular-parser: склейка переезжает до раскладки; `join_blocker` остаётся по типу и по диапазону кодов слабого продолжения; проверка числа колонок переезжает в шаг 2 п. 2
- tabular-parser: склейка переезжает до раскладки; `join_blocker` сохраняет проверки по типу, диапазону кодов и числу колонок
- **7. Ячейка** — один формат `Cell`; Excel: `text` — как показывает числовой формат, `value` — число по формату (tp#42; формат не разобран — сырое значение + предупреждение); OCR: `text` сырой, `value` `null`, `confidence` от ИС
- scan-gateway: обёртка над `value` + `confidence` ИС
- tabular-parser: отображаемый текст и значение считаются здесь, а не в шаге 4
@ -405,29 +407,35 @@ sheet:<A>,<B>`. `DISCLOSURE` и форма без колонки кодов —
**Вход:** `FormCandidate` (сетка, `segment_breaks`, `type_hint`).
**Единый выход:** `TableLayout` — сегменты сетки (участки между повторными шапками и границами листов), у каждого:
столбец кода, столбец наименования, колонки значений `[{col, label, period_end, period_kind}]`, сопоставленные с
первым сегментом по порядку; роль каждой строки сетки (`title` — выше строки заголовка сегмента, а без заголовка —
выше первой строки с кодом, V-8 | `header` | `numbering` | `data`).
столбец кода, столбец наименования, колонки значений `[{col, label, label_confidence, period_end, period_kind}]`,
сопоставленные с первым сегментом по порядку; роль каждой строки сетки (`title` | `header` | `numbering` | `data`).
- **1. Столбец кодов** — одно правило (уточнено по вопросу владельца 17.10.2026; F-5, F-21, F-24, V-1, G-5, G-19): код — текст без пробелов, переносов и краевой пунктуации («1220 ,» → 1220, G-22), после замены двойников (порядок: убрать пробелы и переносы → двойники → проверка; `|` → 1 только если результат попадает в диапазон, G-18), 4–5 цифр (допускается `.0`), первые четыре в диапазонах словаря кодов: 1xxx — ББ, 2xxx — ОФР, 3xxx–6xxx — прочие формы и пояснения (`type_hint` неизвестен — объединение диапазонов). Многострочная ячейка — код, если каждая её часть код (V-9). (а) Столбец, над данными которого стоит подпись «Код» / «Коды» / «Код строки» (словарь подписей расширяется, G-28) **и под которой доля кодов среди непустых ячеек ≥ 0,6 или кодов ≥ 3**; несколько кандидатов — столбец с наибольшим числом кодов; берётся последняя такая подпись над первой строкой с кодом. Защита обязательна: в выгрузках 1С под подписью «Коды» титульного блока стоят ОКУД, дата, ОКПО, ИНН, ОКОПФ (12267 у АО, 12300 у ООО — по первым четырём цифрам это «коды баланса»), ОКЕИ: один код из девяти, доля 0,11 (V-1, G-5). Документ из двух строк остаётся документом: два из двух, доля 1,0. (б) Подписи нет → столбец с наибольшим числом ячеек-кодов, кодов ≥ 3, доля ≥ 0,6 (так парсер сейчас); нет — «колонка кодов не найдена». Статусы строк с нечитаемым или пустым кодом — новое поведение для обоих: шлюз сегодня такие строки пропускает со счётчиком, а кодом считает любое целое (F-24)
- scan-gateway: проход по подписи получает защиту по доле; в запасном подсчёте `int(text)` → правило словаря (закрывает AURORA-1517 вместе с п. 5)
- tabular-parser: добавляет проход по подписи до подсчёта; диапазон формы → объединение
- **2. Повторная шапка и границы листов** — одно правило: новый сегмент открывает строка с признаком шапки («Код» / та же сигнатура) **или** граница листа из `segment_breaks` (F-16); у сегмента своя раскладка: по его шапке, а без шапки — столбец кодов по правилу п. 1, и если сегмент не первый и кодов в нём меньше трёх, столбец кодов наследуется от предыдущего сегмента при условии, что его коды в диапазоне формы (так `relaxed` + `_weak_blocker` парсера сейчас: лист «_2» НПЗ с единственной строкой 2400, V-4, G-8); колонки значений без шапки — по числам в строках с кодами; колонки значений сегмента сопоставляются с первым сегментом по порядку; число не совпало — предупреждение `error`, значения по позиции; столбец кодов не найден ни так, ни так — предупреждение `error` сегмента, строки сегмента не теряются (роль `data`, статусы по шагу 3)
**Диапазоны кодов** (словарь, границы уточняются в спеке): ББ 1100–1799, ОФР 2100–2999, отчёт об изменениях
капитала 3100–3699, отчёт о движении денежных средств 4100–4499, отчёт о целевом использовании 6100–6499,
пояснения 5000–5999. «Кодовые диапазоны» для поиска столбца — объединение этих отрезков (годы 2000–2099 и
четырёхзначные суммы на 1–6 вне отрезков кодами не считаются). «Диапазон столбца» — отрезок, в который попадает
большинство кодов найденного столбца; им проверяются разлепление строк (шаг 3), слабые продолжения и наследование
столбца в сегментах.
- **1. Столбец кодов** — одно правило: код — текст без пробелов, переносов и краевой пунктуации («1220 ,» → 1220), после замены двойников (порядок: убрать пробелы и переносы → двойники → проверка; «|» → 1 только если результат попадает в кодовый диапазон: «| 140» → 1140), 4–5 цифр (допускается `.0`), первые четыре в кодовых диапазонах. Многострочная ячейка — код, если каждая её часть код. (а) Столбец, над данными которого стоит подпись «Код» / «Коды» / «Код строки» (словарь подписей) **и под которой доля кодов среди непустых ячеек ≥ 0,6 или кодов ≥ 3**; несколько кандидатов — столбец с наибольшим числом кодов; берётся последняя такая подпись над первой строкой с кодом. Защита обязательна: в выгрузках 1С под подписью «Коды» титульного блока стоят ОКУД, дата, ОКПО, ИНН, ОКОПФ (12267 у АО, 12300 у ООО — по первым четырём цифрам это «коды баланса»), ОКЕИ: один код из девяти, доля 0,11. Документ из двух строк остаётся документом: два из двух, доля 1,0. (б) Подписи нет → столбец с наибольшим числом ячеек-кодов, кодов ≥ 3, доля ≥ 0,6 (так парсер сейчас); нет — «колонка кодов не найдена». Статусы строк с нечитаемым или пустым кодом — новое поведение для обоих: шлюз сегодня такие строки пропускает со счётчиком
- scan-gateway: столбец по подписи получает защиту по доле; запасной подсчёт — по кодовым диапазонам, как сейчас; классификация строк `int(text)` → статусы
- tabular-parser: добавляет проход по подписи до подсчёта; диапазон формы → кодовые диапазоны
- **2. Повторная шапка и границы листов** — одно правило: новый сегмент открывает строка с признаком шапки («Код» / та же сигнатура) **или** граница листа из `segment_breaks`; у сегмента своя раскладка: по его шапке, а без шапки — столбец кодов по правилу п. 1, и если сегмент не первый и кодов в нём меньше трёх, ослабленный поиск: столбец с наибольшим числом кодов из диапазона столбца первого сегмента, минимум один (так `relaxed` + `_weak_blocker` парсера сейчас: лист «_2» НПЗ с единственной строкой 2400, у которого столбец кодов в другой колонке, чем на листе 1); колонки значений без шапки — по числам в строках с кодами, сопоставление с первым сегментом по порядку; число не совпало — предупреждение `error`, значения по позиции; столбец кодов не найден ни так, ни так — предупреждение `error` сегмента, строки сегмента не теряются (роль `data`, статусы по шагу 3)
- scan-gateway: как сейчас, через общее правило
- tabular-parser: бывший «лист-продолжение со своей раскладкой» становится сегментом; бывшая проверка числа колонок в `join_blocker` — предупреждение сегмента
- **3. Дата колонки** — один словарь периодов (`statement_periods.yaml`), **объединяющий** сегодняшний yaml парсера и шаблоны `dates.py` шлюза (G-4): «На 31 марта 2025 г.», «За январь – март 2025 г.», «31.12.2024», «30/09/25», «30092025», «За 9 месяцев 2025 г.», «за I квартал 2025 г.», «1 полугодие 2026», «с 01.01.2025 по 31.03.2025», «2024», двузначный год, хвост «0:00:00», клетки «2|0|2|4|»; строки `test_dates.py::test_parses_real_forms` шлюза — обязательные тесты словаря. Результат — `period_end` и `period_kind`: абсолютные подписи → `absolute`; относительные («На отчётную дату», «На 31 декабря предыдущего года», «На 31 декабря года, предшествующего предыдущему», «За отчётный период», «За аналогичный период прошлого года») → `relative:<вид>`, дата считается от **эффективной даты документа**: дата отчёта из шапки, иначе самая поздняя абсолютная дата колонок (F-6); нет ни той ни другой → `period_end = null`, `period_kind` остаётся относительным, ядро досчитает после правки даты отчёта (F-10); подписи нет → `period_kind = none`. Перед разбором — очистка двойников OCR (общий шаг, у Excel не срабатывает). Относительные подписи — решение владельца 17.10.2026 (Р-6): это бланки по приказу 66н без впечатанных дат, у шлюза сейчас «колонка 0 без даты: На отчетную дату»
- **3. Дата колонки** — один словарь периодов (`statement_periods.yaml`), **объединяющий** сегодняшний yaml парсера и шаблоны `dates.py` шлюза: «На 31 марта 2025 г.», «За январь – март 2025 г.», «31.12.2024», «30/09/25», «30092025», «За 9 месяцев 2025 г.», «за I квартал 2025 г.», «1 полугодие 2026», «с 01.01.2025 по 31.03.2025», «2024», двузначный год, хвост «0:00:00», клетки «2|0|2|4|»; строки `test_dates.py::test_parses_real_forms` шлюза — обязательные тесты словаря. Результат — `period_end` и `period_kind`: абсолютные подписи → `absolute`; относительные («На отчётную дату», «На 31 декабря предыдущего года», «На 31 декабря года, предшествующего предыдущему», «За отчётный период», «За аналогичный период прошлого года») → `relative:<вид>`, дата считается от **эффективной даты документа**: дата отчёта из шапки, иначе самая поздняя абсолютная дата колонок; нет ни той ни другой → `period_end = null`, `period_kind` остаётся относительным, ядро досчитает после правки даты отчёта; подписи нет → `period_kind = none`. Подпись OCR с уверенностью ниже 100, разобранная в дату, подтверждается, если дата равна одной из ожидаемых (эффективная дата, минус год, минус два года) — иначе `error` «дата колонки неуверенна». Перед разбором — очистка двойников OCR. Относительные подписи — Р-6: это бланки по приказу 66н без впечатанных дат, у шлюза сейчас «колонка 0 без даты: На отчетную дату»
- scan-gateway: `parse_date` → словарь, его шаблоны переезжают в словарь
- tabular-parser: добавляет очистку двойников (no-op), относительные подписи и шаблоны шлюза
- **4. Строка заголовка и её отсутствие** — одно правило (обе ветки парсера сохраняются, F-5): строка заголовка — последняя над первой строкой с кодом, где в столбце кодов стоит подпись «Код»; иначе последняя над первой строкой с кодом с двумя и более подписями-периодами правее кодов (или одной, не голым годом); двухстрочный заголовок склеивается. Всё выше строки заголовка — роль `title`, в данные не попадает (титульный блок «Дата 30 | 9 | 2025» Межениновской, V-8). Не найдена → колонки значений = столбцы правее кода, где в строках с кодами стоят числа; `label` пустая, `period_end` `null`, `period_kind = none`; одно предупреждение `error` «строка заголовка не найдена»; роль `title` — всё выше первой строки с кодом
- **4. Строка заголовка и её отсутствие** — одно правило (обе ветки парсера сохраняются): строка заголовка — последняя над первой строкой с кодом, где в столбце кодов стоит подпись «Код»; иначе последняя над первой строкой с кодом с двумя и более подписями-периодами правее кодов (или одной, не голым годом); двухстрочный заголовок склеивается. Роль `title` — только строки выше найденной строки заголовка (титульный блок «Дата 30 | 9 | 2025» Межениновской); в сегменте без заголовка роли `title` нет, строки выше первого кода с содержимым идут в данные (шаг 3). Заголовка нет ни у одного сегмента документа → колонки значений первого сегмента = столбцы правее кода, где в строках с кодами стоят числа; `label` пустая, `period_end` `null`, `period_kind = none`; одно предупреждение `error` «строка заголовка не найдена». Заголовок есть хотя бы у одного сегмента — остальные сегменты берут его колонки (п. 2) без предупреждения: листы 2–5 ББ НПЗ без шапки остаются `done`
- scan-gateway: синтез шапки уходит (он эквивалентен); добавляет ветку «заголовок по периодам»
- tabular-parser: добавляет предупреждение
- tabular-parser: добавляет предупреждение для документа без единого заголовка
- **5. Строка нумерации «1 2 3 4 5»** — одно правило `is_numbering_row` → роль `numbering`, в данные не попадает
- scan-gateway: добавляет (AURORA-1517)
- tabular-parser: как сейчас
- **6. Подпись без периода** — **решение владельца (17.10.2026, Р-6):** подпись — период по словарю (абсолютный или относительный, п. 3) → колонка с датой; подпись из словаря исключений («Изменение», «Отклонение», «%», «Темп», «Доля», «Примечание») → пропуск с предупреждением `info`; иначе, если в столбце есть числа в строках с кодами (OCR-мусор вроде «| | г» с уверенностью 17 в aep_3, обрезанная или самодельная подпись) — колонка остаётся с подписью как есть, `period_end = null`, `period_kind = none`, предупреждение `error` с привязкой к колонке, документ `needs_check`; аналитик ставит дату колонке через PATCH (G-13) или подтверждает после сверки с источником; иначе пропуск. Ядру колонка без даты не мешает для периода формы (эффективная дата, п. 3), но межформенная проверка и гудвилл «на отчётную дату» по такой колонке не считаются, пока дата не проставлена (G-13). Пропуск числовой колонки с нераспознанной подписью отвергнут: для OCR это тихая потеря периода с возможным автоподтверждением
- **6. Подпись без периода** — Р-6: подпись — период по словарю (абсолютный или относительный, п. 3) → колонка с датой; подпись из словаря исключений («Изменение», «Отклонение», «%», «Темп», «Доля», «Примечание») → пропуск с предупреждением `info`; иначе, если в столбце есть числа в строках с кодами (OCR-мусор вроде «| | г» с уверенностью 17 в aep_3, обрезанная или самодельная подпись) — колонка остаётся с подписью как есть, `period_end = null`, `period_kind = none`, предупреждение `error` с привязкой к колонке, документ `needs_check`; аналитик ставит дату колонке через PATCH или подтверждает после сверки с источником; иначе пропуск. Ядру колонка без даты не мешает для периода формы (эффективная дата, п. 3), но межформенная проверка и гудвилл «на отчётную дату» по такой колонке не считаются, пока дата не проставлена. Пропуск числовой колонки с нераспознанной подписью отвергнут: для OCR это тихая потеря периода с возможным автоподтверждением
- scan-gateway: добавляет словарь исключений и относительные периоды
- tabular-parser: перестаёт молча пропускать неизвестные числовые колонки; добавляет относительные периоды
- **7. Столбец наименования** — одно правило (G-29): подпись «Наименование (показателя)» в строке заголовка → этот столбец; иначе столбец левее кодов с наибольшим числом текстов, кроме столбца «Пояснения»; наименование строки — ячейка этого столбца, пусто → ближайшая текстовая ячейка левее кода
- **7. Столбец наименования** — одно правило: подпись «Наименование (показателя)» в строке заголовка → этот столбец; иначе столбец левее кодов с наибольшим числом текстов, кроме столбца «Пояснения»; наименование строки — ячейка этого столбца, пусто → ближайшая текстовая ячейка левее кода, кроме «Пояснения»
- scan-gateway: «наименование», иначе слева от кода → общее правило
- tabular-parser: `_name_column` / `_line_name` → общее правило
@ -435,34 +443,38 @@ sheet:<A>,<B>`. `DISCLOSURE` и форма без колонки кодов —
**Вход:** `FormCandidate` + `TableLayout`.
**Единый выход:** `RawLine[]` — по строке на каждую строку сетки с ролью `data`, у которой есть код или хотя бы одно
число: `row_index` (порядковый номер в документе, F-4), `ref`, ячейка кода, `code_status`, ячейка наименования, ячейки
значений по колонкам раскладки сегмента, `parent_ref` (для `detail`). Статусы (`ok` | `unreadable` | `missing` |
`detail`):
**Единый выход:** `RawLine[]` — по строке на каждую строку сетки с ролью `data`, у которой есть содержимое: код,
число или непустой текст в колонке значений с уверенностью ниже 100 (признак потерянной суммы: aep_3 row 8 «[190 :»
с «Ё», «" :»). У строки: `row_index` (порядковый номер в документе), `ref` (уникален в документе; у разлепленных строк
`row:<i>#<k>`), ячейка кода, `code_status`, ячейка наименования, ячейки значений по колонкам раскладки сегмента,
`parent_ref` (для `detail`). Статусы (`ok` | `unreadable` | `missing` | `detail`):
- `ok` — код разобран;
- `unreadable` — ячейка кода непуста, но кодом не разобралась («ИЧ,250», «изо :», «| 140»);
- `detail` — **решение владельца 17.10.2026 (Р-8):** строка без кода с числами, стоящая под строкой с кодом в том же
сегменте, — расшифровка («в том числе: …», «из них …», «Оборудование к установке», «Товар (41.01)»); родитель —
ближайшая строка с кодом выше. Сервисы её отдают, ядро хранит, в проверки §7, агрегаты и критерий `done` она не
входит, экран по умолчанию не показывает (F-3). Отменяет OQ-36 / OQ-141 («строки без кода не отдаются»); OQ-149
(«восстановление строк по наименованию») остаётся открытым;
- `missing` — строка без кода с числами между заголовком сегмента и первой строкой с кодом (кода не было или он
потерян в самом начале таблицы); строки роли `title` сюда не попадают (V-8).
- `unreadable` — ячейка кода содержит хотя бы одну цифру, но кодом не разобралась («ИЧ,250», «изо :», «[190 :»);
ячейка кода без единой цифры («}», «:») считается пустой;
- `detail` — Р-8: строка без кода с содержимым, стоящая под строкой с кодом (в том же сегменте или в предыдущих
сегментах документа — родитель продолжается через разрыв страницы), — расшифровка («в том числе: …», «из них …»,
«Оборудование к установке», «Товар (41.01)»); родитель — ближайшая строка с кодом выше. Сервисы её отдают, ядро
хранит, в проверки §7, агрегаты и критерий `done` она не входит, экран по умолчанию не показывает. Отменяет OQ-36 /
OQ-141 («строки без кода не отдаются»); OQ-149 («восстановление строк по наименованию») остаётся открытым;
- `missing` — строка без кода с содержимым, над которой во всём документе нет строки с кодом (кода не было или он
потерян в самом начале таблицы: «Выручка» aep_3, где 2110 уехал в ячейку значения); строки роли `title` сюда не
попадают.
Строка без кода и без чисел (заголовок раздела «АКТИВ», «в том числе:» без сумм) — не строка. Счётчиков отброшенных
нет — есть статусы. Повтор кода — не статус: по правилам отчётности один код может стоять у нескольких строк с
разными наименованиями, все они `ok`. Строка формы с пустой ячейкой кода под другой строкой с кодом попадёт в `detail`
и будет скрыта; её значения не войдут в сумму раздела, нарушение §7 подсветит родителя (цель соотношения отсутствует
при непустых членах — нарушение, G-2), аналитик раскроет расшифровки и назначит код (Р-13) — осознанная цена Р-8.
Строка без кода и без содержимого (заголовок раздела «АКТИВ», «в том числе:» без сумм) — не строка. Счётчиков
отброшенных нет — есть статусы. Повтор кода — не статус: по правилам отчётности один код может стоять у нескольких
строк с разными наименованиями, все они `ok`. Строка формы с пустой ячейкой кода под другой строкой с кодом попадёт в
`detail` и будет скрыта; её значения не войдут в сумму раздела, нарушение §7 подсветит родителя (цель соотношения
отсутствует при непустых членах — нарушение, шаг 7), аналитик раскроет расшифровки и назначит код (Р-13) — осознанная
цена Р-8.
- **1. Код-мусор** — статус `unreadable`, строка сохраняется с сырым текстом кода (AURORA-1462: «ИЧ,250» видна, аналитик назначает 1250). Разбор кода — по правилу шага 2 п. 1: пробелы, переносы и краевая пунктуация, двойники, 4–5 цифр («1 100» в aep_3 — это 1100, F-21; «| 140» → 1140, G-18)
- **1. Код-мусор** — статус `unreadable`, строка сохраняется с сырым текстом кода (AURORA-1462: «ИЧ,250» видна, аналитик назначает 1250). Разбор кода — по правилу шага 2 п. 1
- scan-gateway: счётчик → статус
- tabular-parser: отличает «нет кода» от «не разобран»
- **2. Склейка двух строк в одну** — одно правило, выполняется **до** разбора кода (V-9): ячейка кода содержит N частей через перевод строки и ячейки значений — N частей → N строк (OQ-156) с `ref` `row:<i>#<k>`; разлепление делается, если хотя бы одна часть кода разобралась как код (G-21: «44 30\nI |» aep_6 не множится), нечитаемая часть становится строкой `unreadable` (F-25); число частей не совпало или ни одна часть не код — одна строка, многострочное значение → `amount = null`, `string_value` = текст, `is_recognized = false`; у Excel не срабатывает
- scan-gateway: как сейчас, через общее правило; разлепление при нечитаемой части — новое
- **2. Склейка двух строк в одну** — одно правило, выполняется **до** разбора кода: ячейка кода содержит N частей через перевод строки и ячейки значений — N частей → N строк (OQ-156) с `ref` `row:<i>#<k>`; разлепление делается, если хотя бы одна часть кода разобралась как код диапазона столбца («44 30\nI |» aep_6 не множится: 4430 не в диапазоне ББ), нечитаемая часть становится строкой `unreadable`; число частей не совпало или ни одна часть не код — одна строка, многострочное значение → `amount = null`, `string_value` = текст, `is_recognized = false`. Значения разлепленных строк получают `is_recognized = false` и нуждаются в покрытии проверками §7 (Р-17: сошлись — подсветки нет, иначе `needs_review`); у Excel не срабатывает
- scan-gateway: как сейчас, через общее правило; разлепление при нечитаемой части — новое; статус документа после разлепления — по общему правилу, а не безусловный `needs_check`
- tabular-parser: правило общее, no-op
- **3. Повтор кода** — **решение владельца (17.10.2026):** повтор кода — законный случай (один код, разные наименования), не ошибка и не предупреждение. Все строки сохраняются как есть со статусом `ok`; текстовое предупреждение и `checks.duplicate_line_codes` уходят. Для проверок §7, подтверждения и всего, что идёт после, значения одного кода идут суммой (см. «Что уходит из ядра»; договорённость ДБД в AURORA-1431; закрывает AURORA-1406). Неверно прочитанный код, случайно совпавший с существующим, ловит §7 через сумму
- **3. Повтор кода** — Р-4: повтор кода — законный случай (один код, разные наименования), не ошибка и не предупреждение. Все строки сохраняются как есть со статусом `ok`; текстовое предупреждение и `checks.duplicate_line_codes` уходят. Для проверок §7, подтверждения и всего, что идёт после, значения одного кода идут суммой (см. «Что уходит из ядра»; договорённость ДБД в AURORA-1431; закрывает AURORA-1406). Неверно прочитанный код, случайно совпавший с существующим, ловит структурная проверка кода: повтор, у которого хотя бы одна копия с уверенностью ниже 100, — `needs_review` у этой строки (Р-16)
- scan-gateway: «оставлена первая» → все строки
- tabular-parser: `duplicates` уходит
- **4. Отброшенная строка** — строки не отбрасываются: `unreadable` / `missing` / `detail` идут дальше со статусом, `row_index` и `ref` (AURORA-1634, 1462); ядро не достраивает вместо них пустую шаблонную строку молча; подтвердить документ, пока у `unreadable` / `missing` есть суммы, нельзя (Р-13). Отмена OQ-36 / OQ-141 — Р-8
@ -474,22 +486,22 @@ sheet:<A>,<B>`. `DISCLOSURE` и форма без колонки кодов —
**Вход:** ячейки значений `RawLine` по колонкам раскладки (`Cell` или `null`).
**Единый выход:** `StatementValue {column_index, amount, string_value, text, confidence, is_recognized}`: `text` —
сырой текст источника всегда, как `Cell.text` (G-3, V-3); `string_value` — как сейчас, только когда сумма не
разобрана (`amount = null`, непустой текст); `needs_review` досчитывается в шаге 8.
сырой текст источника всегда, как `Cell.text`; `string_value` — как сейчас, только когда сумма не разобрана
(`amount = null`, непустой текст); `needs_review` досчитывается в шаге 8.
- **1. Предобработка текста** — у ячейки после шага 1 либо есть `value`, либо только `text`. `value` — число → `amount = value`, квантование до 2 знаков half-up, не больше 16 целых цифр (`numeric(18,2)`), иначе `amount = null`, `string_value` = текст, `is_recognized = false` и предупреждение (F-20); `value` — дата или логическое → `amount = null`, `string_value` = текст, `is_recognized = false` (F-20); `value` нет → один `parse_amount(text, source)` для обоих: двойники (Р-11), пробелы и тонкие пробелы, минус «−» (U+2212) как знак, скобки → минус, причём `}` и `]` принимаются как закрывающая скобка (OQ-147, F-1), прочерк → `null`; запятая: у `excel` — десятичный разделитель («4,200» = 4,20, как `to_money` сейчас), у `ocr` — разделитель тысяч при группе из трёх цифр (AURORA-1405, sg#35) (G-16); «лучшая догадка» из цифр ограничена 16 цифрами, иначе `null` (F-20); Excel-ячейки «число текстом» идут тем же путём
- **1. Предобработка текста** — у ячейки после шага 1 либо есть `value`, либо только `text`. `value` — число → `amount = value`, квантование до 2 знаков half-up, не больше 16 целых цифр (`numeric(18,2)`), иначе `amount = null`, `string_value` = текст, `is_recognized = false` и предупреждение; `value` — дата или логическое → `amount = null`, `string_value` = текст, `is_recognized = false`; `value` нет → один `parse_amount(text, source)` для обоих: двойники (Р-11), пробелы и тонкие пробелы, минус «−» (U+2212) как знак, скобки → минус, причём `}` и `]` принимаются как закрывающая скобка (OQ-147), прочерк → `null`; разделители: у `ocr` запятая — тысячи при группе из трёх цифр (AURORA-1405, sg#35) и точка — тысячи при группах из трёх цифр («837.994», sg#37), текст с тремя и более знаками после разделителя — `amount = null`, `string_value` = текст (решение № 30 среза 3a сохраняется); у `excel` запятая и точка — десятичный разделитель («4,200» = 4,20, «1.500» = 1,5, как `to_money` сейчас); «лучшая догадка» из цифр ограничена 16 цифрами, иначе `null`; многострочное значение в строке с однострочным кодом → `amount = null`, `string_value` = текст, `is_recognized = false`; Excel-ячейки «число текстом» идут тем же путём. Разбор ОСВ (`to_money` в `classify.py`) общим `parse_amount` не заменяется — он остаётся за пределами этой работы
- scan-gateway: `parse_amount` переезжает в общий код и получает квантование, предел длины, `}` / `]`, «−», тонкие пробелы
- tabular-parser: `to_money` заменяется общим `parse_amount` с `source = excel`
- tabular-parser: `to_money` для форм заменяется общим `parse_amount` с `source = excel`
- **2. Смысл `is_recognized`** — один: текст разобран как число без выброшенных и исправленных символов (замена двойника — исправление; `}` / `]` вместо скобки — тоже); `confidence` — отдельно, от источника; оба признака сервис сводит в `needs_review` (шаг 8, «Подтверждение проверкой»), экран подсвечивает по нему
- scan-gateway: отвязывает от `confidence`
- tabular-parser: как сейчас
- **3. «42831 1»** — в `parse_amount` пробел допустим только как разделитель тысяч (группы ровно по три цифры); иначе `amount` — лучшая догадка (цифры подряд), `is_recognized = false`, сырой текст — в `text` (AURORA-1511, 1519)
- scan-gateway: меняется через общий код
- tabular-parser: то же правило, у Excel почти не срабатывает
- **4. Единицы** — одно правило: единицы «тыс. руб.» и дробных ≥ 10 % → предупреждение документа `info` (как у парсера сейчас, документ остаётся `done`)
- **4. Единицы** — одно правило: единицы «тыс. руб.» и дробных ≥ 10 % → предупреждение документа `info` (как у парсера сейчас, документ остаётся `done`); то же сочетание у книги xlsb без числовых форматов — `error`: вероятен масштаб ×1000 (рубли с копейками, которые формат переводил в тысячи), а §7 сходится при любом масштабе
- scan-gateway: добавляет
- tabular-parser: как сейчас
- **5. Пустая ячейка** — **решение владельца (17.10.2026, Р-2):** одно правило: позиции нет, текст пуст или прочерк → `amount = null`, `string_value = null`, `is_recognized = true`, `confidence` ячейки (нет ячейки — 100), собственной подсветки нет. По `fixtures/ocr` ИС отдаёт ячейку на каждую позицию каждой строки: пустые — пустой текст с уверенностью 100, непрочитанные — с уверенностью 0, и такие идут в `needs_review` по общему правилу шага 8 (снимается подтверждением ячейки аналитиком, G-12); позиция отсутствует только при несовпадении колонок сегмента, что покрыто предупреждением `error` шага 2 п. 2
- tabular-parser: как сейчас; xlsb-сочетание — новое
- **5. Пустая ячейка** — Р-2: позиции нет, текст пуст или прочерк → `amount = null`, `string_value = null`, `is_recognized = true`, `confidence` ячейки (нет ячейки — 100), собственной подсветки нет. По `fixtures/ocr` ИС отдаёт ячейку на каждую позицию каждой строки: пустые — пустой текст с уверенностью 100; непрочитанные — пустой текст с уверенностью 0, и такие нуждаются в покрытии по общему правилу шага 8 (снимается покрытием или подтверждением ячейки аналитиком); позиция отсутствует только при несовпадении колонок сегмента, что покрыто предупреждением `error` шага 2 п. 2
- scan-gateway: «ячейки нет → 0, не распознано» уходит
- tabular-parser: как сейчас
@ -499,13 +511,14 @@ sheet:<A>,<B>`. `DISCLOSURE` и форма без колонки кодов —
**Единый выход:** `StatementLine {row_index, ref, line_status, line_code, line_code_text, line_code_confidence,
line_name, line_name_confidence, parent_ref, values}`: `line_code` — разобранный код (4–5 цифр) или `null` при
`unreadable` / `missing` / `detail`; `line_code_text` — сырой текст ячейки кода; `line_status` = `code_status` шага 3;
`parent_ref` — у `detail`. Несколько строк с одним кодом — обычные строки `ok`, порядок как в файле.
`unreadable` / `missing` / `detail`; `line_code_text` — сырой текст ячейки кода; `line_code_confidence` — от источника,
а у кода с заменой двойника — ниже 100 (неуверенный); `line_status` = `code_status` шага 3; `parent_ref` — у `detail`.
Несколько строк с одним кодом — обычные строки `ok`, порядок как в файле.
- **1. Источник наименования** — **решение владельца (17.10.2026):** сервисы отдают распознанный текст наименования как есть, с уверенностью источника; соответствие «код → наименование» — только в ядре и только когда нужно (показ справочного имени, достройка шаблонных строк). Экран показывает справочное имя, файловое — рядом (Р-3; AURORA-1406, 1522)
- **1. Источник наименования** — Р-3: сервисы отдают распознанный текст наименования как есть, с уверенностью источника; соответствие «код → наименование» — только в ядре и только когда нужно (показ справочного имени, достройка шаблонных строк, сверка для неуверенных кодов — Р-16). Экран показывает справочное имя, файловое — рядом (AURORA-1406, 1522)
- scan-gateway: имя из `statement_lines.yaml` → имя из OCR
- tabular-parser: как сейчас
- **2. Редакция (66н / ФСБУ 4)** — в сервисах редакция нужна только для упорядоченного перечня кодов (подтверждение кода структурой, шаг 8; F-9) и выбирается по эффективной дате документа (шаг 2 п. 3), без даты — проверяются обе редакции. Наименования и достройка шаблонных строк — ядро по его словарю имён; проверка «код не из редакции формы» — ядро по своей копии перечня общего кода (V-11)
- **2. Редакция (66н / ФСБУ 4)** — в сервисах редакция нужна только для упорядоченного перечня кодов (подтверждение кода структурой, шаг 8) и выбирается по эффективной дате документа (шаг 2 п. 3), без даты — проверяются обе редакции. Наименования и достройка шаблонных строк — ядро по его словарю имён; проверка «код не из редакции формы» — ядро по своей копии перечня общего кода
- scan-gateway: `_choose_edition` → выбор редакции перечня кодов в общем коде
- tabular-parser: добавляет выбор редакции перечня кодов
- **3. Справочник строк** — только в ядре; в общий код сервисов не входит; копия в шлюзе удаляется
@ -519,19 +532,20 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо
**Вход:** `FormCandidate` (`type_hint`, `evidence`, `header`) + `TableLayout` + `StatementLine[]`.
**Единый выход:** `StatementDocument {document_type, locator, header, columns, lines}` без `checks`.
**Единый выход:** `StatementDocument {document_type, form_variant, locator, header, columns, lines}` без `checks`.
- **1. Тип формы** — одно правило: большинство кодов 1xxx → `BALANCE`, 2xxx → `INCOME`, 3xxx–6xxx → `DISCLOSURE` (F-22, G-19); совпало с `type_hint` или `type_hint` `null` — тип по кодам; противоречит — тип по кодам + предупреждение `error`; кодов нет — `type_hint`; без колонки кодов: `type_hint` есть → документ без строк (`DISCLOSURE` — `done`, иначе `needs_check` с предупреждением `error`), `type_hint` нет → не документ, предупреждение `info`. `DISCLOSURE` по `type_hint` — шаги 2–7 не выполняются; `DISCLOSURE` по кодам — шаги 2–5 выполнены, строки и колонки отбрасываются (ядро строки пояснений не хранит), проверки не считаются (G-19). Упрощённая форма (ОКУД 0710096 в `evidence`, либо есть 1600 и нет ни 1100, ни 1200) помечается `form_variant = simplified` — шаг 7 и перечень кодов берут упрощённый набор (G-17)
- scan-gateway: как сейчас, через общее правило; добавляет упрощённый вариант
- **1. Тип формы** — одно правило: большинство кодов в диапазоне ББ → `BALANCE`, ОФР → `INCOME`, прочих форм и пояснений → `DISCLOSURE`; совпало с `type_hint` или `type_hint` `null` — тип по кодам; противоречит — тип по кодам + предупреждение `error`; кодов нет — `type_hint`. Без колонки кодов: `type_hint` есть → документ без строк (`DISCLOSURE` — `done`, иначе `needs_check` с предупреждением `error`); `type_hint` нет: у `ocr` каждая таблица ИС с содержимым — потерянный документ, предупреждение `error` файла (сегодня шлюз: «нет шапки и столбца кода» → `needs_check`), у `excel` лист «прочее» — `info`. `DISCLOSURE` по `type_hint` — шаги 2–7 не выполняются; `DISCLOSURE` по кодам — шаги 2–5 выполнены, строки и колонки отбрасываются (ядро строки пояснений не хранит), проверки не считаются, документ `done`. **Вариант формы** `form_variant` — `full` | `simplified`, только по структуре кодов (ОКУД для этого не годится: 0710096 — код КНД комплекта упрощённой отчётности, на бланках стоят 0710001 / 0710002): ББ — есть 1600 и нет ни 1100, ни 1200; ОФР — есть 2400 или 2110 и нет ни 2100, ни 2200, ни 2300; шаг 7 и перечень кодов берут упрощённый набор. Образец упрощённой формы — в fixtures при первой возможности
- scan-gateway: как сейчас, через общее правило; добавляет упрощённый вариант и `error` за потерянную таблицу
- tabular-parser: добавляет сверку с кодами и упрощённый вариант
- **2. Склейка страниц** — решена в шаге 1, различия нет
- scan-gateway: без изменений
- tabular-parser: без изменений
- **3. `locator`** — одна грамматика «пары `ключ:значение` через `;`», набор ключей по источнику: `file:<имя json>;table:<i>` / `sheet:<A>[,<B>]` — ключи как сейчас, golden-эталоны и §11 не меняются (F-23)
- **3. `locator`** — одна грамматика «пары `ключ:значение` через `;`», набор ключей по источнику: `file:<имя json>;table:<i>` / `sheet:<A>[,<B>]` — ключи как сейчас, golden-эталоны и §11 не меняются
- scan-gateway: без изменений
- tabular-parser: без изменений
Шапка документа — поля `header` кандидата как есть (`text` → `*_text`, `value` → поле, `confidence` → `*_confidence`).
Шапка документа — поля `header` кандидата как есть (`text` → `*_text`, `value` → поле, `confidence` → `*_confidence`
у всех полей).
### Шаг 7 — Самопроверки
@ -543,18 +557,22 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо
- значение кода в колонке — сумма по всем строкам `ok` с этим кодом (повтор кода — законный случай, шаг 3 п. 3); строки
`detail`, `unreadable`, `missing` в соотношения не входят; `duplicate_line_codes` уходит; нарушение подсвечивает все
строки кода;
- **цель отсутствует** (нет строки `ok` с кодом цели) или `null`, а хотя бы один член непуст — **жёсткое нарушение**,
не пропуск (G-2: иначе «БАЛАНС» без кода, ушедший в `detail`, делает 1600 невидимым, и документ проходит в `done`);
сегодня оба сервиса и ядро такое соотношение пропускают — поведение меняется;
- «сошлось» / «не сошлось» (F-13): жёсткое соотношение не сошлось, если расхождение больше допуска ±1 **и** больше
- соотношение считается **посчитанным**, если непуста цель или хотя бы один член; пропускается, только когда пусто
всё (ни строки `ok` с кодом цели, ни одного непустого члена). Пустая цель при непустых членах — жёсткое нарушение
(«БАЛАНС» без кода, ушедший в `detail`, делает 1600 невидимым, и это должно подсветить родителя); непустая цель при
пустых членах — тоже нарушение (лист только с активом: «1600 = 1700» при отсутствующем 1700 — `needs_check`, как
сегодня у трёх файлов манифеста парсера). Сегодня оба сервиса и ядро соотношение с отсутствующей целью пропускают
— поведение меняется; посчитанное соотношение из всех `null` ничего не подтверждает (то же правило, что для
«родитель = Σ подстрок» по AURORA-1527);
- «сошлось» / «не сошлось»: жёсткое соотношение не сошлось, если расхождение больше допуска ±1 **и** больше
округления 0,001 % — только тогда оно в `relations_failed`; расхождение в пределах округления и мягкие соотношения —
в `soft_relations_failed`, это `info`, на статус и подсветку не влияют (OQ-138 сохраняется);
- соотношение с членами по модулю (`abs_terms`: 2120, 2210, 2220, 2330, 2350, 2410) сходится при любом знаке такого
члена и потому **подтверждает только величину, не знак** (F-1) — см. «Подтверждение проверкой»;
- соотношение считается **посчитанным** только если хотя бы один его член в колонке непуст; все члены `null` — не
проверяется и ничего не подтверждает (F-2; то же правило, что для «родитель = Σ подстрок» по AURORA-1527);
- набор соотношений выбирается по `form_variant`: полный или упрощённый (ОКУД 0710096: нет подытогов 1100 / 1200 /
1300, свои равенства; G-17), оба — в одном словаре `statement_checks.yaml`.
члена и потому **подтверждает только величину, не знак** — см. «Подтверждение проверкой»;
- набор соотношений выбирается по `form_variant`: полный или упрощённый (нет подытогов 1100 / 1200 / 1300 у ББ,
2100 / 2200 / 2300 у ОФР, свои равенства), оба — в одном словаре `statement_checks.yaml`;
- родитель пятизначной подстроки — ближайшая четырёхзначная строка выше с теми же первыми тремя цифрами, иначе код
из первых четырёх цифр (`subline_parents`, §12 п. 7: 12310 под 1230).
Проверки по словарю имён (код не в словаре) и по перечню кодов (код не из редакции формы по эффективной дате) — в
ядре, как предупреждения `info` к строке; сервисы словаря имён не знают (Р-3, Р-12).
@ -568,283 +586,337 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо
**Вход:** `StatementDocument` + `StatementChecks` + предупреждения шагов 1–7.
**Единый выход:** у каждого документа `status` (`done` | `needs_check`), `warnings[]`, `needs_review` у каждого
значения и у каждой строки; у файла `metrics.warnings[]`; статус конверта — агрегат. Предупреждение — структура
`{code, severity: error | info, message, line_refs?: [], column_index?}`, а не строка; `line_refs` — список, потому
что нарушение подсвечивает все строки кода (G-25).
значения, каждой строки и каждого поля шапки; у файла `metrics.warnings[]`; статус конверта — агрегат.
Предупреждение — структура `{code, severity: error | info, message, line_refs?: [], column_index?, field?}`, а не
строка; `line_refs` — список, потому что нарушение подсвечивает все строки кода.
- **1. Критерий `done`** — одна функция общего кода с входами «раскладка, шапка, статусы строк, `needs_review`, нарушения, предупреждения» (F-8): раскладка найдена ∧ (организация или ИНН) ∧ период определён (дата отчёта **или** самая поздняя абсолютная дата колонок — `statement_period` ядра, F-6) ∧ все строки `ok` или `detail` ∧ нет значений и строк `needs_review` (см. «Подтверждение проверкой» ниже) ∧ нет жёстких нарушений §7 вне допуска и округления (F-13) ∧ нет предупреждений уровня `error`. `DISCLOSURE` — `done` без условий о раскладке, строках и проверках (как сейчас). Excel выполняет условия уверенности автоматически; структурная проверка кодов к Excel не применяется (Р-12). Квартал периода и статус конверта — знание ядра, в `done` не входят (F-18, G-7)
- **1. Критерий `done`** — одна функция общего кода с входами «раскладка, шапка, статусы строк, `needs_review`, нарушения, предупреждения, подтверждения и акцепт аналитика»: раскладка найдена ∧ (организация или ИНН) ∧ период определён (дата отчёта **или** самая поздняя абсолютная дата колонок — `statement_period` ядра; у ИС поле `date` часто мусор: у aep_6 и «БО 1 кв 2025» там название организации) ∧ все строки `ok` или `detail` ∧ нет значений, строк и полей шапки с `needs_review` (см. «Подтверждение проверкой» ниже) ∧ нет жёстких нарушений §7 вне допуска и округления ∧ нет предупреждений уровня `error`. `DISCLOSURE` — `done` без условий о раскладке, строках и проверках (как сейчас). Excel выполняет условия уверенности автоматически; структурная проверка кодов к Excel не применяется (Р-12). Квартал периода, статус конверта, сверка ИНН и наименований — знание ядра, в `done` не входят
- scan-gateway: «ничего не выброшено и уверенность 100» → общий критерий
- tabular-parser: `_is_ok` → общий критерий; добавляются строки и значения
- **2. Влияние предупреждений** — по уровню: `error` → `needs_check`, `info` — не влияет; у обоих одинаково. Таблица уровней (F-12, V-6, G-6, G-20), полная версия — в спеке. `error`: ошибочный json набора при успешном соседе (уровень файла; решение № 28 среза 3a), `BACKEND_PARSING_ERROR` таблицы, таблица или сегмент с содержимым без колонки кодов (в том числе потерянный лист-продолжение), колонки сегмента не совпали, колонка без даты, строка заголовка не найдена, тип по кодам противоречит заголовку, тип маркера продолжения противоречит форме, ни организации, ни ИНН в шапке, период не определён (нет ни даты отчёта, ни дат колонок). `info`: ИНН не найден или неуверен при найденной организации (сверку с ЮЛ делает ядро — `MISMATCH`; НПЗ ОФР без ИНН остаётся `done`), организация не найдена при найденном ИНН, дата отчёта не разобрана при наличии дат колонок, единицы не найдены (аналитик правит в шапке), формат файла не совпал с `file_type` (файл прочитан верно), подпись из словаря исключений пропущена, дробные при «тыс. руб.», xlsb без числовых форматов (tp#42, документ остаётся `done`), не документ без `type_hint` и кодов, код не в словаре имён или не из редакции (ядро). Принцип: `error` — только то, что меняет данные или их полноту (решение № 1)
- **2. Влияние предупреждений** — по уровню: `error` → `needs_check`, `info` — не влияет; у обоих одинаково. Таблица уровней, полная версия — в спеке. `error`: ошибочный json набора при успешном соседе (уровень файла; решение № 28 среза 3a), `BACKEND_PARSING_ERROR` таблицы, таблица ИС с содержимым без колонки кодов и без типа (уровень файла), сегмент с содержимым без колонки кодов (в том числе потерянный лист-продолжение), лист с тремя и более кодами и другим числом колонок, не присоединившийся к форме, колонки сегмента не совпали, колонка без даты, дата колонки неуверенна, заголовка нет ни у одного сегмента, тип по кодам противоречит заголовку, тип маркера продолжения противоречит форме, ни организации, ни ИНН в шапке, период не определён (нет ни даты отчёта, ни дат колонок), дата отчёта неуверенна и не совпала с датами колонок, xlsb без форматов при «тыс. руб.» и дробных. `info`: ИНН не найден или неуверен при найденной организации (сверку с ЮЛ делает ядро — `MISMATCH`; НПЗ ОФР без ИНН остаётся `done`), организация не найдена или неуверенна при найденном ИНН, дата отчёта не разобрана при наличии дат колонок, единицы не найдены (аналитик правит в шапке), формат файла не совпал с `file_type` (файл прочитан верно), подпись из словаря исключений пропущена, дробные при «тыс. руб.» (кроме xlsb без форматов), xlsb без числовых форматов само по себе (tp#42, документ остаётся `done`), лист Excel «прочее» без `type_hint` и кодов, разлеплена склеенная строка, код не в словаре имён или не из редакции (ядро). Принцип: `error` — только то, что меняет данные или их полноту
- scan-gateway: «любое → `needs_check`» уходит
- tabular-parser: предупреждения начинают влиять
- **3. Статус документа** — в контракте: `document.status`; конверт `done` ⇔ все документы `done` ∧ нет предупреждений файла уровня `error`; иначе `needs_check`; документов нет — `error` с кодом по источнику: `RECOGNITION_FAILED` у шлюза, `INVALID_DOCUMENT` у парсера — это различие остаётся, коды описывают разные причины (F-23). Статус конверта остаётся условием `RECOGNIZED` в ядре (G-7): потерянный json набора не даёт соседнему документу автоподтвердиться
- **3. Статус документа** — в контракте: `document.status` у каждого документа, включая ОСВ (у ОСВ `done`, когда ведомость разобрана, как сейчас; её предупреждения — `info`); конверт `done` ⇔ все документы `done` ∧ нет предупреждений файла уровня `error`; иначе `needs_check`; документов нет — `error` с кодом по источнику: `RECOGNITION_FAILED` у шлюза, `INVALID_DOCUMENT` у парсера — это различие остаётся, коды описывают разные причины. Статус конверта в условие `RECOGNIZED` ядра не входит, туда входят предупреждения `error` уровня файла (Р-19)
- scan-gateway: добавляет
- tabular-parser: внутренний `ok` → в контракт
- **4. Формат предупреждений** — структура (см. выше) вместо строк `<json>: table:<i>: …` / `sheet=<имя> …`; место — в `locator` документа, а не в тексте
- **4. Формат предупреждений** — структура (см. выше) вместо строк `<json>: table:<i>: …` / `sheet=<имя> …` у всех документов результата, включая ОСВ; место — в `locator` документа, а не в тексте
- scan-gateway: меняет
- tabular-parser: меняет
- **5. Привязка** — к документу, строкам (`line_refs`) или ячейкам (`line_refs` + `column_index`); `ref` строки уникален в документе и хранится в ядре вместе с `row_index` (F-4, G-9), поэтому экран показывает предупреждение у места и после сохранения
- tabular-parser: меняет, включая путь ОСВ
- **5. Привязка** — к документу, строкам (`line_refs`), ячейкам (`line_refs` + `column_index`) или полю шапки (`field`); `ref` строки уникален в документе и хранится в ядре вместе с `row_index`, поэтому экран показывает предупреждение у места и после сохранения
- scan-gateway: меняет
- tabular-parser: меняет
**Подтверждение проверкой (вопрос владельца 17.10.2026; F-1, F-2, F-13, V-13).** Проверка §7 — арбитр, уверенность —
подсказка там, где проверки нет. По каждому значению сервис считает `needs_review`:
**Подтверждение проверкой.** Проверка §7 — арбитр, уверенность — подсказка там, где проверки нет. По каждому
значению сервис считает `needs_review`:
- значение **нуждается в покрытии**, если `confidence` ниже порога (Р-5: 100) или `is_recognized = false` (число
собрано из грязного текста или с заменой двойников, Р-7, Р-11); чистые значения (уверенность 100 и
`is_recognized = true`) в покрытии не нуждаются и ни одним правилом ниже не подсвечиваются (V-13);
- значение **покрыто**, если в его колонке есть жёсткое соотношение с участием его кода, которое посчитано (хотя бы
один член непуст, F-2) и сошлось (в пределах допуска или округления, F-13), и `amount` самого значения не `null`
(мусор, давший `null`, покрытым не бывает, F-2);
- **знак** (F-1): для члена соотношения с `abs_terms` покрытие подтверждает только величину. Знак подтверждён, если
форма записи знака у ячейки (скобки, минус, без знака — по `text`) совпадает с конвенцией колонки — формой записи
у большинства членов `abs_terms` этой колонки с непустым `amount` и `is_recognized = true`; не совпадает или
сравнивать не с чем — `needs_review = true` независимо от соотношения. Так «(7744536}» с `}` как закрывающей скобкой
даёт −7 744 536 и подтверждается, если 2210 и 2220 в той же колонке тоже в скобках; «{38378)» при остальных в
скобках — подсвечивается;
собрано из грязного текста, с заменой двойников или из разлепленной строки — Р-7, Р-11, Р-17); чистые значения
(уверенность 100 и `is_recognized = true`) в покрытии не нуждаются: правило покрытия и правило знака к ним не
применяются;
- значение **покрыто**, если в его колонке есть жёсткое соотношение с участием его кода, которое посчитано и сошлось
(в пределах допуска или округления), и `amount` самого значения не `null` (мусор, давший `null`, покрытым не
бывает);
- **знак**: для члена соотношения с `abs_terms` покрытие подтверждает только величину. Знак подтверждён, если форма
записи знака у ячейки (скобки, минус, без знака — по `text`) совпадает с конвенцией колонки — формой записи у
большинства членов `abs_terms` этой колонки с непустым `amount`, `is_recognized = true` и без правки аналитика
(правленые ячейки в голосование не входят); не совпадает или сравнивать не с чем — `needs_review = true` независимо
от соотношения. Так «(7744536}» с `}` как закрывающей скобкой даёт −7 744 536 и подтверждается, если 2210 и 2220 в
той же колонке тоже в скобках; «{38378)» при остальных в скобках — подсвечивается;
- `needs_review = true`, если значение нуждается в покрытии и не покрыто (или не подтверждён знак);
- `true` у всех ячеек жёсткого соотношения в колонке, где оно не сошлось вне допуска и округления, независимо от
уверенности; мягкие соотношения и расхождения в пределах округления подсветки не дают;
уверенности и чистоты; мягкие соотношения и расхождения в пределах округления подсветки не дают;
- иначе `false`: значение подтверждено проверкой, не подсвечивается и статус не трогает; сырой текст (`text`) и
уверенность остаются в подсказке ячейки.
Экран подсвечивает ячейки только по `needs_review`. Ядро хранит `needs_review` и `text` как пришли и пересчитывает
`needs_review` вместе с `checks` общим кодом после PATCH (конвенция знака считается от сохранённого `text`, V-3),
добавляя своё: правленое значение (`is_modified`) или подтверждённое аналитиком без изменения (`confirmed`, G-12) —
`needs_review` вместе с `checks` общим кодом после PATCH (конвенция знака считается от сохранённого `text`), добавляя
своё: правленое значение (`is_modified`) или подтверждённое аналитиком без изменения (`is_confirmed`, хранится) —
`needs_review = false`; акцепт нарушений — жёсткие нарушения не считаются; предупреждения `error` документа
сохраняются и после правки (F-8). Не покрыто проверками и остаётся на уверенности: поля шапки, справочные строки ОФР
25xx / 29xx, детализация без проверки родителя, `DISCLOSURE`, колонки, где есть только мягкие соотношения. Риск — две
взаимно компенсирующие ошибки в одном соотношении; принимается (перекрёстные соотношения баланса его снижают).
Закрывает AURORA-1524 без ущерба для 1519. Сошедшееся посчитанное равенство подтверждает и догадку из грязного текста
(Р-7, решение владельца 17.10.2026): случайное совпадение требует второй, компенсирующей ошибки в том же равенстве.
сохраняются и после правки, кроме снимаемых соответствующей правкой (таблица — в «Что уходит из ядра»). Не покрыто
проверками и остаётся на уверенности: справочные строки ОФР 25xx / 29xx, детализация без проверки родителя,
`DISCLOSURE`, колонки, где есть только мягкие соотношения. Риск — две взаимно компенсирующие ошибки в одном
соотношении; принимается (перекрёстные соотношения баланса его снижают). Закрывает AURORA-1524 без ущерба для 1519.
Сошедшееся посчитанное равенство подтверждает и догадку из грязного текста (Р-7): случайное совпадение требует второй,
компенсирующей ошибки в том же равенстве.
**Код и наименование строки (Р-5, Р-12, решения владельца 17.10.2026; F-9, V-2, G-1).** Уверенность кода
подтверждается не §7 (перепутанные соседи 1220 и 1230 дают ту же сумму раздела), а структурой; правило применяется
**только к кодам с уверенностью ниже 100** (Р-12): у Excel и чистых OCR-кодов расхождение с перечнем даёт
предупреждение `info` ядра «код не из редакции», а не `needs_check` — иначе пятизначные подстроки 1С (11101, 23201)
и нестандартные строки клиента блокировали бы автоподтверждение. В общем коде лежит **упорядоченный перечень кодов**
каждой формы по редакциям (66н, ФСБУ 4) и вариантам (полная, упрощённая) без наименований — порядок бланка, где
итоги стоят после слагаемых (1110 … 1190, 1100; 2110, 2120, 2100), а не по возрастанию. Источник перечня —
объединение кодов шаблонов банка (словарь ядра) и кодов печатных форм 1С по редакции: 1105, 1530, 2420, 2460, 2930 и
другие, которых в словаре ядра нет (G-1). Алгоритм: четырёхзначные коды строки документа образуют
последовательность; подтверждены коды из наибольшей подпоследовательности, согласованной с порядком бланка хотя бы в
одной из проверяемых редакций (редакция по эффективной дате, без даты — обе); остальные — `needs_review` у строки
(`StatementLine.needs_review`), документ — `needs_check`. Пятизначный код подтверждён, если подтверждён его
четырёхзначный родитель (ближайшая строка выше с теми же четырьмя первыми цифрами, иначе код из первых четырёх
цифр — правило `subline_parents`) и подстрока стоит после него (V-2). Равные соседние коды допустимы (Р-4). По
`fixtures/ocr` коды часто приходят с уверенностью 50 и 25, поэтому требовать 100, как шлюз сейчас, нельзя.
Перечень кодов — не словарь строк: наименований в нём нет, Р-3 не нарушается; ядро пользуется своей копией перечня
для проверки «код не из редакции», словарь ядра хранит только имена (V-11). Уверенность наименования на статус и
подсветку в сервисах не влияет: сервисы отдают текст как есть; сопоставление файлового наименования со справочным по
коду — вопрос ядра, открыт (см. «Что уходит из ядра»).
**Поля шапки.** У каждого поля шапки свой `needs_review`: дата отчёта с уверенностью ниже 100 подтверждена, если
совпала с самой поздней абсолютной датой колонок, иначе `needs_review = true` и предупреждение `error` «дата отчёта
неуверенна» — иначе неверно прочитанный год давал бы неверный период и автоподтверждение; ИНН и организация с
уверенностью ниже 100 — `info`, их сверяет ядро (ИНН — `MISMATCH` с ЮЛ, организация — по алиасам); единицы и прочие
поля на статус не влияют. Сегодня шлюз требует уверенность 100 у ИНН и организации и распознанную дату — правило
становится точнее, не слабее.
**Код строки (Р-5, Р-12, Р-16).** Уверенность кода подтверждается не §7 (перепутанные соседи 1220 и 1230 дают ту же
сумму раздела), а структурой и наименованием; правило применяется **только к неуверенным кодам** — уверенность ниже
100 или замена двойника (Р-12): у Excel и чистых OCR-кодов расхождение с перечнем даёт предупреждение `info` ядра «код
не из редакции», а не `needs_check` — иначе пятизначные подстроки 1С (11101, 23201) и нестандартные строки клиента
(1231) блокировали бы автоподтверждение. В общем коде лежит **упорядоченный перечень кодов** каждой формы по
редакциям (66н, ФСБУ 4) и вариантам (полная, упрощённая) без наименований — порядок бланка, где итоги стоят после
слагаемых (1110 … 1190, 1100; 2110, 2120, 2100), а не по возрастанию. Источник перечня — объединение кодов шаблонов
банка (словарь ядра) и кодов печатных форм 1С по редакции: 1105, 1530, 2420, 2460, 2930 и другие, которых в словаре
ядра нет. Алгоритм: четырёхзначные коды строк документа образуют последовательность; для каждой проверяемой
редакции берётся наибольшая подпоследовательность, согласованная с порядком бланка; редакция — та, где она длиннее
(при равенстве — по эффективной дате); если таких подпоследовательностей несколько, подтверждены только коды, общие
для всех; коды с уверенностью 100 участвуют в последовательности как подтверждённые якоря. Пятизначный код
подтверждён, если подтверждён его родитель (правило шага 7) и подстрока стоит после него. Равные соседние коды
допустимы, но повтор кода, у которого хотя бы одна копия неуверенна, — `needs_review` у неуверенной строки (Р-16).
Неподтверждённый неуверенный код — `needs_review` у строки (`StatementLine.needs_review`), документ — `needs_check`.
Структура не ловит подмену неуверенного кода на незапечатанного соседа того же раздела (1110 → 1120, когда 1120 на
бланке пропущен): это закрывает ядро — для строк с неуверенным кодом условие `RECOGNIZED` включает совпадение
распознанного наименования со справочным по коду (Р-16; порог сходства — в спеке), расхождение подсвечивает строку
через `document_issue`. По `fixtures/ocr` коды часто приходят с уверенностью 50 и 25, поэтому требовать 100, как
шлюз сейчас, нельзя. Перечень кодов — не словарь строк: наименований в нём нет, Р-3 не нарушается; ядро пользуется
своей копией перечня для проверки «код не из редакции», словарь ядра хранит только имена. Уверенность наименования
на статус и подсветку в сервисах не влияет: сервисы отдают текст как есть.
### Шаг 9 — Результат
Одинаково, как сейчас; контракт `recognition-result.schema.json` → **v2.0** (**решение владельца 17.10.2026, Р-9**: по
правилам контракта смена типа поля — MAJOR, F-7). Что меняется: `line_code` nullable, `line_status`, `line_code_text`,
`row_index`, `ref`, `parent_ref`, `needs_review` у значения и у строки, `values[].text`; `document.status`,
`document.form_variant`; `document.warnings[]` и `metrics.warnings[]` — структуры с уровнем и привязкой `line_refs[]`;
`columns[].period_kind`; `checks` обязателен для `BALANCE` / `INCOME` со строками у обоих конвейеров (у документа без
строк отсутствует), нарушения с кодами слагаемых, без `duplicate_line_codes`; `service.statements_version` — коммит
копии общего кода, которым посчитан результат (F-17). Выкладка: ядро поднимает `SUPPORTED_MAJOR` и на время перехода
принимает 1.x и 2.0 (два маппера; при приёме 1.x новые поля заполняются по умолчанию: `line_status = ok`, `row_index`
по порядку, `text = string_value`, `needs_review` по старому правилу `values_ok`), сервисы переходят на 2.0 после
ядра — порядок «сначала ядро» обязателен, иначе первый же результат с `null` в `line_code` даёт файлу `ERROR` (F-7).
Запрос `recognition-request` остаётся 1.x.
Одинаково, как сейчас; контракт `recognition-result.schema.json` → **v2.0** (Р-9: по правилам контракта смена типа
поля — MAJOR). Что меняется: `line_code` nullable, `line_status`, `line_code_text`, `row_index`, `ref`,
`parent_ref`, `needs_review` у значения, строки и полей шапки, `values[].text`, `header.*_confidence` и `*_text` у
всех полей; `document.status` у всех документов, включая ОСВ, `document.form_variant`; `document.warnings[]` и
`metrics.warnings[]` — структуры с уровнем и привязкой у всех документов, включая ОСВ; `columns[].period_kind` и
`label_confidence`; `checks` обязателен для `BALANCE` / `INCOME` со строками у обоих конвейеров (у документа без строк
отсутствует), нарушения с кодами слагаемых, без `duplicate_line_codes`; `service.statements_version` — коммит копии
общего кода, которым посчитан результат. Выкладка: ядро поднимает `SUPPORTED_MAJOR` и на время перехода принимает
1.x и 2.0 (два маппера, ack тоже 1.x и 2.0; при приёме 1.x статус документа считается по прежнему правилу D-6 —
`job_done ∧ проверки ∧ values_ok ∧ …`, новые поля заполняются по умолчанию: `line_status = ok`, `row_index` по
порядку, `text = string_value`), сервисы переходят на 2.0 после ядра — порядок «сначала ядро» обязателен, иначе
первый же результат с `null` в `line_code` даёт файлу `ERROR`. Запрос `recognition-request` остаётся 1.x. Путь ОСВ
в парсере меняется только в части структуры предупреждений и `document.status`.
### Что уходит из ядра и что в нём меняется
- `build_statement`: повторная дедупликация колонок, кодов и значений (контракт гарантирует уникальность колонок;
повторы кодов — законные строки, хранятся все как пришли).
- **DBML v5.19 и одна миграция** (F-4, F-8, F-10, V-3, V-5, V-10, Р-4, Р-10): `financial_statement_line` — снять
уникальность `(meta_id, line_code)`, добавить `row_index`, `source_ref` (уникален в документе), `line_status`,
`line_code_text`, `parent_row_index` (родитель `detail`), `needs_review`; `financial_statement_value` — `source_text`
(сырой текст, из `values[].text`) и `needs_review` (как пришло от сервиса, пересчитывается ядром после PATCH;
пересматривает решение v5.13 «считается, не хранится»); `financial_statement_column.period_kind`;
`financial_statement_meta.form_variant`; таблица `document_issue` (вид `warning` | `relation` | `soft`, код, уровень,
сообщение, список `line_id`, `column_id?`, источник `service` | `core`) — хранит предупреждения и нарушения как
пришли, очищается при перезагрузке файла, записи источника `core` заменяются при каждом пересчёте (G-25);
- **DBML v5.19 и одна миграция:** `financial_statement_line` — снять уникальность `(meta_id, line_code)`, добавить
`row_index` (у строк из источника; у шаблонных и созданных правкой — `null`), `source_ref` (частичный уникальный
индекс по `(meta_id, source_ref)` там, где не `null`), `line_status`, `line_code_text`, `parent_row_index` (родитель
`detail`), `needs_review`, `is_confirmed` (подтверждение кода аналитиком); `financial_statement_value` —
`source_text` (из `values[].text`), `needs_review` (как пришло от сервиса, пересчитывается ядром после PATCH;
пересматривает решение v5.13 «считается, не хранится»), `is_confirmed` (подтверждено без изменения, хранится вместе
с `confirmed_by / at`); `financial_statement_column.period_kind`, `label_confidence`; `financial_statement_meta.form_variant`,
`needs_review` полей шапки; таблица `document_issue` (вид `warning` | `relation` | `soft`, код, уровень, сообщение,
список `line_id`, `column_id?`, `field?`, источник `service` | `core`) — хранит предупреждения и нарушения как
пришли; при перезагрузке файла очищается; при пересчёте после PATCH записи `relation` / `soft` заменяются целиком
независимо от источника, записи `warning` источника `service` сохраняются, кроме снимаемых правкой;
`recognition_job.statements_version`; `document` — снять уникальность `(stored_file_id, document_type,
osv_account_code)` и добавить порядковый номер формы в файле (**решение владельца 17.10.2026, Р-10**; F-11); в ту же
миграцию — `crm_id` ГСК / ГСЗ (AURORA-1619, arm-core#112, отложенный «до следующих изменений БД»).
- Повторы кодов (решение владельца 17.10.2026): для проверок §7, подтверждения и всего, что идёт после (КФО, выгрузки,
сверки), документ читается через **одну функцию «агрегат формы по коду»** — сумма значений строк `ok` по колонке,
список `line_id`, справочное имя — чистой функцией, без хранения (DECISION-5). На неё переводятся все точки, где
сегодня «одна строка на код» (F-15): `_line_amount` (межформенная проверка 1370 / 2400), `statement_amounts`,
`subline_parents` / `parent_code`, `formLayout` (дети под родителем), `_resolve_cells`, `metrics.fields_*`.
- `statement_status` (D-6): `RECOGNIZED` ⇔ конверт `done` (G-7, решение № 28: потерянный json не даёт соседу
автоподтвердиться) ∧ `document.status = done` ∧ ИНН ≠ `MISMATCH` ∧ период — конец квартала (`period_known`, F-18:
иначе документ `RECOGNIZED`, но не подтверждаемый, 409 `PERIOD_REQUIRED`); остальные признаки D-6 не
пересчитываются. Вторая форма того же типа и периода у того же ЮЛ из одного файла не автоподтверждается (Р-15).
osv_account_code)` только для `BALANCE` / `INCOME` (частичный индекс: ОСВ по счёту и `DISCLOSURE` / `OTHER` остаются
«один на тип на файл», §9.5 п. 2) и добавить порядковый номер формы в файле (Р-10); в ту же миграцию — `crm_id`
ГСК / ГСЗ (AURORA-1619, arm-core#112). Заполнение при миграции: `row_index` существующих строк — по порядку кода,
`line_status = ok`, `needs_review` значений — по прежнему `values_ok`, `source_text = string_value`.
- Повторы кодов (Р-4): для проверок §7, подтверждения и всего, что идёт после (КФО, выгрузки, сверки), документ
читается через **одну функцию «агрегат формы по коду»** — сумма значений строк `ok` по колонке, список `line_id`,
справочное имя — чистой функцией, без хранения (DECISION-5). На неё переводятся все точки, где сегодня «одна
строка на код»: `_line_amount` (межформенная проверка 1370 / 2400), `statement_amounts`, `subline_parents` /
`parent_code`, `formLayout` (дети под родителем), `_resolve_cells`, `metrics.fields_*`.
- `statement_status` (D-6) при приёме 2.0: `RECOGNIZED` ⇔ `document.status = done` ∧ нет предупреждений `error`
уровня файла (потерянный json или таблица ИС, решение № 28 среза 3a; `needs_check` соседнего документа само по себе
не блокирует — Р-19) ∧ ИНН ≠ `MISMATCH` ∧ период — конец квартала (`period_known`: иначе документ `RECOGNIZED`, но
не подтверждаемый, 409 `PERIOD_REQUIRED`) ∧ у строк с неуверенным кодом распознанное наименование совпало со
справочным (Р-16) ∧ в файле нет второй формы того же типа и периода (Р-18: при двух и более — ни одна не
автоподтверждается). Остальные признаки D-6 не пересчитываются.
- §7 при загрузке: `checks`, `needs_review` и предупреждения берутся из результата и хранятся (`document_issue`);
ядро добавляет только `info` по словарю имён и перечню кодов (G-26). Пересчёт общим кодом — только после PATCH, с
правилами `is_modified` или `confirmed` ⇒ `needs_review = false` (G-12), `line_code_modified` ⇒ `needs_review` строки
= false (V-5), акцепт ⇒ жёсткие нарушения сняты, предупреждения `error` сервиса сохраняются (F-8). Версия общего кода
из результата (`statements_version`) пишется в `recognition_job`; при пересчёте ядро сравнивает её со своей копией,
расхождение — предупреждение `error` к документу, не падение (F-17).
- **PATCH `…/statement`** (F-14, G-12, G-13, Р-13): помимо `header` и `values` принимает `values[].confirmed`
(подтвердить ячейку без изменения значения: `needs_review = false`, `is_modified` не ставится, аудит), `columns:
[{column_id, period_date}]` (дата колонке без подписи; снимает предупреждение `error` «колонка без даты»,
пересчитывает `is_current`; `report_date: null` обнуляет `period_end` относительных колонок, G-23) и `lines:
[{line_id, line_code}]` («назначить код») / `[{line_id, line_status: detail}]` («отметить расшифровкой»). Переходы:
ядро добавляет только `info` по словарю имён и перечню кодов и свою сверку наименований для неуверенных кодов.
Пересчёт общим кодом — только после PATCH, с правилами `is_modified` или `is_confirmed` ⇒ `needs_review = false` у
значения; `line_code_modified` или `is_confirmed` ⇒ `needs_review` строки = false; акцепт ⇒ жёсткие нарушения
сняты. Версия общего кода из результата (`statements_version`) пишется в `recognition_job`; при пересчёте ядро
сравнивает её со своей копией, расхождение — предупреждение `info` к документу (на окне выкладки «сначала ядро»
расхождение штатно).
- **Какая правка какое `error` снимает:** PATCH `header.report_date` — «период не определён», «дата отчёта не
разобрана», «дата отчёта неуверенна»; PATCH `header.inn` или `organization_name` — «ни организации, ни ИНН»; PATCH
`columns[].period_date` — «колонка без даты», «дата колонки неуверенна»; назначение кода или отметка `detail` —
нарушения, пересчитываемые §7. Остальные `error` сервиса (колонки сегмента не совпали, заголовка нет, тип
противоречит, потерянный json, лист или таблица, xlsb с масштабом) снимаются только перезагрузкой файла.
- **PATCH `…/statement`** (Р-13): помимо `header` и `values` принимает `values[].confirmed` (подтвердить ячейку без
изменения значения: `is_confirmed`, `needs_review = false`, `is_modified` не ставится, аудит), `columns:
[{column_id, period_date}]` (дата колонке без подписи или с неуверенной подписью; пересчитывает `is_current`;
`report_date: null` обнуляет `period_end` относительных колонок) и `lines`: «назначить код» (`line_id`, `line_code`),
«отметить расшифровкой» (`line_id`, `line_status = detail`), «подтвердить код» (`line_id`, `confirmed`). Переходы:
`unreadable` / `missing` / `detail` → `ok` назначением кода; `unreadable` / `missing` / `ok` → `detail` отметкой;
`ok` → `ok` с другим кодом (смена кода, прежний в `line_code_original_value`). Правила: код 4–5 цифр, может
совпадать с существующим (Р-4); незнакомый словарю — `info`; назначенный код снимает `needs_review` строки;
`line_code_modified`, `line_code_original_value`; у `detail` родитель пересчитывается как ближайшая строка `ok`
выше по `row_index`; шаблонная строка с таким кодом заменяется настоящей; пара «`line_status = ok` без кода» — 422;
удаления строк нет (первичка не мутируется, `detail` — способ вывести строку из сумм). Аудит
`RECOGNIZED_DATA_EDIT` с `before / after` по коду, статусу и родителю. Один PATCH — один пересчёт и одна запись.
- **Подтверждение документа** (Р-13, G-10): `form_confirmable` / `_check_statement` → 409 `LINES_UNRESOLVED` со
списком, пока есть строки `unreadable` / `missing` с непустым `amount`; строки без сумм не мешают. Автоподтверждения
это не касается (`done` таких строк не допускает). `detail` в агрегат, КФО и снимок ЕХ не попадают — цена Р-8.
Подтверждение второго документа того же типа и периода у ЮЛ при подтверждённом первом — 409 со ссылкой на первый
(Р-15).
`ok` → `ok` с другим кодом (смена кода, прежний в `line_code_original_value`); `ok` неуверенный → подтверждён.
Правила: код 4–5 цифр, может совпадать с существующим (Р-4); незнакомый словарю — `info`; назначенный или
подтверждённый код снимает `needs_review` строки, `line_code_confidence` становится 100, `line_code_modified`,
`line_code_original_value`; у `detail` родитель пересчитывается как ближайшая строка `ok` выше по `row_index`;
шаблонная строка с таким кодом, если она уже создана правкой аналитика с суммами, — 409 `LINE_EXISTS` с подсказкой
(аналитик сначала очищает её суммы или выбирает другой код), без сумм — удаляется, её место занимает настоящая;
пара «`line_status = ok` без кода» — 422; удаления строк нет (первичка не мутируется, `detail` — способ вывести
строку из сумм). Строки, созданные правкой: `row_index = null`, `source_ref = null`, `line_status = ok`. Аудит
`RECOGNIZED_DATA_EDIT` с `before / after` по коду, статусу, родителю и подтверждению. Один PATCH — один пересчёт и
одна запись.
- **Подтверждение документа** (Р-13, Р-18): порядок проверок 409 по §9.5 п. 4: `PERIOD_REQUIRED` → `LINES_UNRESOLVED`
(есть строки `unreadable` / `missing` с непустым `amount`; строки без сумм не мешают) → `DUPLICATE_FORM` (в файле уже
подтверждена другая форма того же типа и периода; 409 со ссылкой на неё; между файлами работает автозамена §12 п. 3)
→ жёсткие нарушения без акцепта. Автоподтверждения `LINES_UNRESOLVED` не касается (`done` таких строк не
допускает). `detail` в агрегат, КФО и снимок ЕХ не попадают — цена Р-8.
- Правка даты отчёта (PATCH шапки): для колонок с `period_kind = relative:<вид>` ядро досчитывает `period_end` общим
кодом от новой даты (F-10).
кодом от новой даты.
- Словарь §7: единственный экземпляр в общем коде (Р-1), копия ядра удаляется. Словарь имён строк (код → наименование
по редакциям) остаётся только в ядре, копия шлюза удаляется; порядок и состав редакций ядро берёт из своей копии
перечня общего кода (V-11).
перечня общего кода.
- Достройка шаблонных строк: по словарю ядра; код, у которого есть хотя бы одна строка `ok` из файла, шаблоном не
достраивается.
- **Экран** (решение владельца 17.10.2026, Р-14; G-15): раскладка остаётся «как на бумаге» — разделы, внутри строки
`ok` по возрастанию кода, несколько строк одного кода в порядке файла, шаблонные строки на своих местах,
пятизначные подстроки под родителем. Строка `unreadable` / `missing` ставится под ближайшей предыдущей по
`row_index` строкой, которая разместилась по коду, подсвечена целиком, в ячейке кода серым сырой текст с подсказкой.
Строки `detail` скрыты под переключателем «Показать расшифровки (N)», как пустые строки детализации, показываются
под родителем с отступом. Подсветка ячеек только по `needs_review` (уверенность, `is_recognized` и нарушения §7 уже
сведены сервисом), в подсказке — `text` и уверенность; предупреждения и нарушения — из `document_issue`, с привязкой
к месту. В режиме «Редактировать»: ячейка кода у `unreadable` / `missing` — поле ввода с подсказкой из справочника
(код → справочное имя), меню строки «Это расшифровка»; у `detail` — «Назначить код»; у обычной строки — «Сделать
расшифровкой» и смена кода; подтверждение ячейки без правки; дата у колонки без подписи. Кнопка «Подтвердить
данные» при неразобранных строках с суммами отключена с подсказкой «Разберите строки без кода: N» и переходом к
первой; сервер отвечает тем же 409.
- Открытый вопрос ядра (не сервисов): сопоставлять ли файловое наименование строки со справочным по коду и что
делать при расхождении (владелец 17.10.2026: «наверное, будем пытаться сопоставить»). В сервисах уверенность и
текст наименования на статус не влияют. Второй открытый вопрос (Р-15, V-7): склейка двух половинок формы из одного
файла не предоставляется, файл перезагружают — в `07-open-questions.md`.
- **Экран** (Р-14): раскладка остаётся «как на бумаге» — разделы, внутри строки `ok` по возрастанию кода, несколько
строк одного кода в порядке файла, шаблонные строки на своих местах, пятизначные подстроки под родителем. Строка
`unreadable` / `missing` ставится под ближайшей предыдущей по `row_index` строкой, которая разместилась по коду, а
без такой — в начало первого раздела; подсвечена целиком, в ячейке кода серым сырой текст с подсказкой. Строки
`detail` скрыты под переключателем «Показать расшифровки (N)», как пустые строки детализации, показываются под
родителем с отступом. Подсветка ячеек только по `needs_review` (уверенность, `is_recognized` и нарушения §7 уже
сведены сервисом), в подсказке — `text` и уверенность; поля шапки с `needs_review` подсвечены; предупреждения и
нарушения — из `document_issue`, с привязкой к месту. В режиме «Редактировать»: ячейка кода у `unreadable` /
`missing` — поле ввода с подсказкой из справочника (код → справочное имя), меню строки «Это расшифровка»; у `detail`
— «Назначить код»; у обычной строки — «Сделать расшифровкой», смена кода и «Подтвердить код» у неуверенного;
подтверждение ячейки без правки; дата у колонки без подписи. Кнопка «Подтвердить данные» при неразобранных
строках с суммами отключена с подсказкой «Разберите строки без кода: N» и переходом к первой; сервер отвечает тем
же 409.
- Открытый вопрос: склейка двух половинок формы из одного файла не предоставляется, файл перезагружают — в
`07-open-questions.md` (Р-15). Вопрос о сопоставлении файлового наименования со справочным закрыт Р-16: для
неуверенных кодов это условие `RECOGNIZED`, для остальных — только показ справочного имени.
### Решения владельца (Р-1…Р-15, 17.10.2026)
### Решения владельца (Р-1…Р-19, 17.10.2026)
- **Р-1** (шаг 1, п. 2) — **решено 17.10.2026: копия по скрипту.** Платформа не собирает Python-пакеты и не кладёт их
в свой Nexus, поэтому общий код — dataclass-ы шага 1 (сериализуемые в json), шаги 2–8, словари §7, периодов,
заголовков форм, перечни кодов (без словаря имён) со своим загрузчиком — живёт в отдельном репозитории
`arm-statements` со своими тестами и оформлен как пакет `arm_statements/` с `pyproject`. Границы пакета (G-14):
пакет владеет своими типами входа и выхода, хост конвертирует выход в свою модель контракта (у ядра на переходе их
две); golden-тесты пакета — внутри пакета на синтетических и обезличенных данных, тесты на реальных фикстурах
остаются в scan-gateway и tabular-parser и соседний репозиторий не читают. Уточнения по F-17: в tabular-parser,
scan-gateway и arm-core пакет лежит копией вместе со своими тестами и конфигом линтеров, импортируется по имени
пакета `arm_statements` (путь добавляется в `pyproject`), поэтому замена копии на зависимость не трогает импорты;
маркер коммита источника попадает в результат как `service.statements_version`; раскладывает и проверяет свежесть
скрипт `sync-statements` в docs по образцу `sync-web.sh` (`--check`, код 20 при прямых правках), вызывается из
`make check-full` перед PR локально — **не** из `make check` и не из пайплайна: тест, читающий соседний репозиторий,
уже ломал main (arm-core #33, пайплайн платформы вне репозиториев). Прямые правки копии запрещены.
- **Р-2** (шаг 4, п. 5) — **решено владельцем 17.10.2026:** отсутствующая позиция под колонкой — чистый `null` без
собственной подсветки. Основание: по fixtures ИС отдаёт ячейку на каждую позицию (пустая — уверенность 100,
непрочитанная — 0, и её ловит общее правило `needs_review`); отсутствие позиции бывает только при несовпадении
колонок сегмента и покрыто предупреждением шага 2 п. 2.
- **Р-3** (шаг 5, п. 1) — **решено владельцем 17.10.2026:** сервисы отдают распознанный текст наименования;
соответствие «код → наименование» — только в ядре, при необходимости (= решение № 2 по спринту 2, AURORA-1406 /
1522; договорённость ДБД «из справочника» выполняется на экране).
- **Р-4** (шаг 3, п. 3) — **решено владельцем 17.10.2026:** повторы кодов законны; все строки сохраняются и
показываются, для проверок и всех операций после подтверждения или автоподтверждения — суммой по коду. Следствие:
снять уникальность `(meta_id, line_code)` в DBML; все чтения «одна строка на код» в ядре переводятся на агрегат
(F-15).
- **Р-5** (шаг 8, «Подтверждение проверкой») — **решено владельцем 17.10.2026:** порог для значений — 100 (у ИС
уверенность дискретная: 100, ~80, 71, 67, 60, 50, 25, 0, промежутка 90–99 нет; в группе 70–89 есть настоящие ошибки,
например «11 30» с уверенностью 80); ложных подсветок становится меньше за счёт подтверждения проверкой §7, а не
порога. Код строки подтверждается структурой — упорядоченным перечнем кодов формы по редакции (F-9), не
уверенностью; область применения — Р-12. Уверенность наименования в сервисах не используется; сопоставление со
справочным — открытый вопрос ядра (= решение № 7 по спринту 2, AURORA-1519 / 1524).
- **Р-6** (шаг 2, п. 6) — **решено владельцем 17.10.2026:** числовая колонка с нераспознанной подписью остаётся в
документе без даты с предупреждением `error` (`needs_check`, подтверждение руками); пропуск отвергнут как тихая
потеря периода. Относительные подписи бланка 66н («На отчётную дату», «На 31 декабря предыдущего года», …)
добавляются в словарь периодов и считаются от эффективной даты документа (F-6); после правки даты отчёта ядро
досчитывает их (F-10); дату колонке можно поставить руками (G-13).
- **Р-7** (шаг 8, «Подтверждение проверкой») — **решено владельцем 17.10.2026: да.** Сошедшееся жёсткое соотношение §7
снимает подсветку и с `is_recognized = false`; такой документ может получить `done` и автоподтвердиться, сырой
текст остаётся в подсказке ячейки. Оговорки по ревью: соотношение должно быть посчитанным, а не «0 = 0» (F-2); для
членов по модулю знак подтверждается конвенцией колонки, а не соотношением (F-1); правило знака — только для
сомнительных ячеек (V-13). Варианты «всегда подсвечивать» и «снимать подсветку, но не автоподтверждать» отвергнуты:
первый возвращает ложные подсветки AURORA-1524, второй даёт статус «требует проверки» без подсветок.
- **Р-8** (шаг 3, F-3) — **решено владельцем 17.10.2026:** расшифровки без кода («в том числе …») отдаются сервисами
со статусом `detail` и хранятся ядром; в проверки, агрегаты и `done` не входят; экран по умолчанию их не показывает
(переключатель, как у пустых строк детализации). Отменяет OQ-36 / OQ-141.
- **Р-9** (шаг 9, F-7) — **решено владельцем 17.10.2026:** контракт **v2.0**, а не аддитивный 1.3; ядро на время
перехода принимает 1.x и 2.0; порядок выкладки — сначала ядро.
- **Р-10** (ядро, F-11) — **решено владельцем 17.10.2026:** снять уникальность `document` по `(stored_file_id,
document_type, osv_account_code)`, вторая форма того же типа в файле хранится; в ту же миграцию DBML v5.19 —
`crm_id` ГСК / ГСЗ из AURORA-1619 (arm-core#112).
- **Р-11** (принцип 5, F-19) — **решено владельцем 17.10.2026:** буквы-двойники заменяются и в суммах, и в кодах;
результат — `is_recognized = false`, дальше его проверяют контрольные соотношения §7, и если всё сошлось,
подсвечивать не нужно (Р-7 с оговорками F-1, F-2). Отменяет §11.1 п. 1 «только даты» и решение № 22 среза 3a.
Уточнение по G-18: ИНН двойники не трогают. Данные: в четырёх json ИС двойников в суммах и кодах нет вовсе —
правило на вырост, не под текущие сканы.
- **Р-12** (шаг 8, «Код и наименование строки»; G-1, V-2) — **решено владельцем 17.10.2026:** структурное
подтверждение кода применяется только к кодам с уверенностью ниже 100; у Excel и чистых OCR-кодов расхождение с
перечнем даёт `info` от ядра, не `needs_check`.
- **Р-13** (подтверждение документа; G-10, F-14) — **решено владельцем 17.10.2026:** подтверждение документа
блокируется (409 `LINES_UNRESOLVED`), пока есть строки `unreadable` / `missing` с суммами; аналитик назначает код
или отмечает строку расшифровкой в режиме правки, через `PATCH …/statement lines` (детали — в «Что уходит из
ядра»).
- **Р-14** (экран; G-15) — **решено владельцем 17.10.2026:** раскладка по коду «как на бумаге» остаётся (просьба
владельца 09.10.2026); строки со статусами ставятся под ближайшей предыдущей размещённой строкой по `row_index`,
расшифровки под родителем и скрыты по умолчанию; переключатель «как в файле» — не в этой работе.
- **Р-15** (ядро; V-7, G-24) — **решено владельцем 17.10.2026:** вторая форма того же типа и периода у одного ЮЛ из
того же файла не автоподтверждается; подтверждение второй при подтверждённой первой — 409 со ссылкой на первую;
склейка половинок формы из одного файла не предоставляется (файл перезагружают), вопрос — в `07-open-questions.md`.
От ИС Сканирование такой случай не ожидается (владелец), основной источник — Excel с листом-продолжением другого
типа или двойной печатью.
- **Р-1** (шаг 1, п. 2) — **копия по скрипту.** Платформа не собирает Python-пакеты и не кладёт их в свой Nexus, поэтому
общий код — dataclass-ы шага 1 (сериализуемые в json), шаги 2–8, словари §7, периодов, заголовков форм, перечни и
диапазоны кодов (без словаря имён) со своим загрузчиком — живёт в отдельном репозитории `arm-statements` со своими
тестами и оформлен как пакет `arm_statements/` с `pyproject`. Границы пакета: пакет владеет своими типами входа и
выхода, хост конвертирует выход в свою модель контракта (у ядра на переходе их две); golden-тесты пакета — внутри
пакета на синтетических и обезличенных данных, тесты на реальных фикстурах остаются в scan-gateway и tabular-parser
и соседний репозиторий не читают. В tabular-parser, scan-gateway и arm-core пакет лежит копией вместе со своими
тестами и конфигом линтеров (конфиг пакета подключается к `pytest` и `ruff` хоста как include, чтобы правила не
конфликтовали), импортируется по имени пакета `arm_statements` (путь добавляется в `pyproject`), поэтому замена
копии на зависимость не трогает импорты; маркер коммита источника попадает в результат как
`service.statements_version`; раскладывает и проверяет свежесть скрипт `sync-statements` в docs по образцу
`sync-web.sh` (`--check`, код 20 при прямых правках), вызывается из `make check-full` перед PR локально — **не** из
`make check` и не из пайплайна: тест, читающий соседний репозиторий, уже ломал main (arm-core #33, пайплайн
платформы вне репозиториев). Прямые правки копии запрещены. Срочное исправление общего кода: PR в `arm-statements`
→ `sync-statements` → PR в каждом потребителе, порядок выкладки «сначала ядро».
- **Р-2** (шаг 4, п. 5) — отсутствующая позиция под колонкой — чистый `null` без собственной подсветки. Основание: по
fixtures ИС отдаёт ячейку на каждую позицию (пустая — уверенность 100, непрочитанная — 0, и её ловит общее правило
`needs_review`); отсутствие позиции бывает только при несовпадении колонок сегмента и покрыто предупреждением
шага 2 п. 2.
- **Р-3** (шаг 5, п. 1) — сервисы отдают распознанный текст наименования; соответствие «код → наименование» — только в
ядре, при необходимости (= решение № 2 по спринту 2, AURORA-1406 / 1522; договорённость ДБД «из справочника»
выполняется на экране).
- **Р-4** (шаг 3, п. 3) — повторы кодов законны; все строки сохраняются и показываются, для проверок и всех операций
после подтверждения или автоподтверждения — суммой по коду. Следствие: снять уникальность `(meta_id, line_code)` в
DBML; все чтения «одна строка на код» в ядре переводятся на агрегат.
- **Р-5** (шаг 8, «Подтверждение проверкой») — порог для значений — 100 (у ИС уверенность дискретная: 100, ~80, 71,
67, 60, 50, 25, 0, промежутка 90–99 нет; в группе 70–89 есть настоящие ошибки, например «11 30» с уверенностью 80);
ложных подсветок становится меньше за счёт подтверждения проверкой §7, а не порога. Код строки подтверждается
структурой — упорядоченным перечнем кодов формы по редакции, не уверенностью; область применения — Р-12,
дополнение — Р-16 (= решение № 7 по спринту 2, AURORA-1519 / 1524).
- **Р-6** (шаг 2, п. 6) — числовая колонка с нераспознанной подписью остаётся в документе без даты с предупреждением
`error` (`needs_check`, подтверждение руками); пропуск отвергнут как тихая потеря периода. Относительные подписи
бланка 66н («На отчётную дату», «На 31 декабря предыдущего года», …) добавляются в словарь периодов и считаются от
эффективной даты документа; после правки даты отчёта ядро досчитывает их; дату колонке можно поставить руками.
- **Р-7** (шаг 8, «Подтверждение проверкой») — **да:** сошедшееся жёсткое соотношение §7 снимает подсветку и с
`is_recognized = false`; такой документ может получить `done` и автоподтвердиться, сырой текст остаётся в подсказке
ячейки. Оговорки по ревью: соотношение должно быть посчитанным, а не «0 = 0»; для членов по модулю знак
подтверждается конвенцией колонки, а не соотношением; правило знака — только для сомнительных ячеек. Варианты
«всегда подсвечивать» и «снимать подсветку, но не автоподтверждать» отвергнуты: первый возвращает ложные подсветки
AURORA-1524, второй даёт статус «требует проверки» без подсветок.
- **Р-8** (шаг 3) — расшифровки без кода («в том числе …») отдаются сервисами со статусом `detail` и хранятся ядром; в
проверки, агрегаты и `done` не входят; экран по умолчанию их не показывает (переключатель, как у пустых строк
детализации). Отменяет OQ-36 / OQ-141.
- **Р-9** (шаг 9) — контракт **v2.0**, а не аддитивный 1.3; ядро на время перехода принимает 1.x и 2.0; порядок
выкладки — сначала ядро.
- **Р-10** (ядро) — снять уникальность `document` по `(stored_file_id, document_type, osv_account_code)` для форм
ББ / ОФР, вторая форма того же типа в файле хранится; в ту же миграцию DBML v5.19 — `crm_id` ГСК / ГСЗ из AURORA-1619
(arm-core#112).
- **Р-11** (принцип 5) — буквы-двойники заменяются и в суммах, и в кодах; результат — `is_recognized = false` у суммы и
неуверенный код, дальше их проверяют контрольные соотношения §7 и структура, и если всё сошлось, подсвечивать не
нужно (Р-7 с оговорками). Отменяет §11.1 п. 1 «только даты» и решение № 22 среза 3a. ИНН двойники не трогают.
Данные: в четырёх json ИС двойников в суммах и кодах нет вовсе — правило на вырост, не под текущие сканы.
- **Р-12** (шаг 8, «Код строки») — структурное подтверждение кода применяется только к неуверенным кодам
(уверенность ниже 100 или замена двойника); у Excel и чистых OCR-кодов расхождение с перечнем даёт `info` от ядра,
не `needs_check`.
- **Р-13** (подтверждение документа) — подтверждение документа блокируется (409 `LINES_UNRESOLVED`), пока есть строки
`unreadable` / `missing` с суммами; аналитик назначает код или отмечает строку расшифровкой в режиме правки, через
`PATCH …/statement lines` (детали — в «Что уходит из ядра»).
- **Р-14** (экран) — раскладка по коду «как на бумаге» остаётся (просьба владельца 09.10.2026); строки со статусами
ставятся под ближайшей предыдущей размещённой строкой по `row_index`, расшифровки под родителем и скрыты по
умолчанию; переключатель «как в файле» — не в этой работе.
- **Р-15** (ядро) — вторая форма того же типа и периода у одного ЮЛ из того же файла хранится; склейка половинок формы
из одного файла не предоставляется (файл перезагружают), вопрос — в `07-open-questions.md`. От ИС Сканирование
такой случай не ожидается (владелец), основной источник — Excel с листом-продолжением другого типа или двойной
печатью. Уточнено Р-18.
- **Р-16** (шаг 8, «Код строки»; ядро) — защита от подмены неуверенного кода на соседний: повтор кода, у которого хотя
бы одна копия неуверенна, — `needs_review` у этой строки (сервис); для строк с неуверенным кодом условие
`RECOGNIZED` в ядре включает совпадение распознанного наименования со справочным по коду, расхождение подсвечивает
строку. Закрывает открытый вопрос о сопоставлении наименований: для неуверенных кодов — условие автоподтверждения,
для остальных — только показ справочного имени.
- **Р-17** (шаг 3, п. 2) — разлепленные строки OQ-156 не держат документ в `needs_check` безусловно (решение 08.10.2026
заменяется): их значения нуждаются в покрытии, как грязные, и при сошедшихся соотношениях §7 не подсвечиваются;
само разлепление — предупреждение `info`.
- **Р-18** (ядро) — при двух и более формах одного типа и периода в одном файле ни одна не автоподтверждается;
подтверждение второй при подтверждённой первой — 409 `DUPLICATE_FORM` только внутри файла (между файлами работает
автозамена §12 п. 3).
- **Р-19** (ядро) — `RECOGNIZED` не зависит от статуса конверта: достаточно `document.status = done` и отсутствия
предупреждений `error` уровня файла (потерянный json или таблица ИС блокируют, `needs_check` соседнего документа
из-за одной ячейки — нет).
## Decisions made without discussion
Решения агента 17.10.2026, принятые при оформлении целевой картины без отдельного обсуждения с владельцем; все
обратимы правкой спеки или словаря. Пункты 9–13 добавлены при внесении F-1…F-25, 14–19 — при внесении V-1…V-13 и
G-1…G-30.
обратимы правкой спеки или словаря.
1. **Уровни предупреждений** (шаг 8, п. 2): `error` — только то, что меняет данные или их полноту; полный список —
в шаге 8 п. 2. Цена ошибки: лишний или пропущенный `needs_check`, правится таблицей уровней в спеке.
1. **Уровни предупреждений** (шаг 8, п. 2): `error` — только то, что меняет данные или их полноту; полный список — в
шаге 8 п. 2, таблица «какая правка какое `error` снимает» — в «Что уходит из ядра». Цена ошибки: лишний или
пропущенный `needs_check`, правится таблицей уровней в спеке.
2. **`DISCLOSURE` всегда `done`** без условий о раскладке, строках и проверках; по `type_hint` шаги 2–7 не
выполняются, по кодам (3xxx–6xxx) строки отбрасываются (F-22, G-19) — сохранено текущее поведение парсера. Цена:
пояснения не проверяются; так и сейчас.
выполняются, по кодам прочих форм строки отбрасываются — сохранено текущее поведение парсера. Цена: пояснения и
формы 3, 4, 6 не проверяются; так и сейчас.
3. **Столбец кодов** — подпись «Код» принимается при доле кодов под ней ≥ 0,6 или кодах ≥ 3, при нескольких
кандидатах — больше кодов (V-1, G-5); запасной подсчёт «кодов ≥ 3 и доля ≥ 0,6» взят у парсера; сегмент не первый с
кодами < 3 наследует столбец предыдущего при кодах в диапазоне формы (V-4, G-8). Цена: таблица без подписи и с
одним-двумя кодами в первом сегменте → документ без строк с `needs_check`.
4. **Начальный словарь исключений подписей** («Изменение», «Отклонение», «%», «Темп», «Доля», «Примечание») — по тестам
кандидатах — больше кодов; запасной подсчёт «кодов ≥ 3 и доля ≥ 0,6» взят у парсера; сегмент не первый с кодами
< 3 — ослабленный поиск по диапазону столбца первого сегмента (как `relaxed` парсера). Цена: таблица без подписи и
с одним-двумя кодами в первом сегменте → документ без строк с `needs_check`.
4. **Диапазоны кодов** — явные отрезки по формам (шаг 2), объединение для поиска столбца, «диапазон столбца» для
разлепления и продолжений; границы форм 3, 4, 6 уточняются в спеке. Цена: код вне отрезка не распознаётся как код;
правится словарём.
5. **Начальный словарь исключений подписей** («Изменение», «Отклонение», «%», «Темп», «Доля», «Примечание») — по тестам
парсера; пополняется словарём, не кодом.
5. **Грамматика `locator`** — пары `ключ:значение` через `;`, ключи по источнику, как сейчас (`file:` у OCR, F-23).
Цена: нулевая.
6. **`value`, который не число** (дата, логическое) в колонке сумм — `amount = null`, `string_value` = текст,
`is_recognized = false` (F-20). Цена: минимальная.
7. **`checks` отсутствует у документа без строк**, обязателен у `BALANCE` / `INCOME` со строками.
8. **Рабочие имена** `FormCandidate`, `TableLayout`, `RawLine`, `needs_review`, `line_status`, `line_code_text`,
`row_index`, `source_ref`, `parent_ref`, `period_kind`, `form_variant`, `document_issue`, `statements_version`,
`LINES_UNRESOLVED`, `arm-statements`, `sync-statements` — по образцу существующих (`sync-web.sh`); меняются при
написании спеки без последствий.
9. **Правило `detail`** (шаг 3): строка без кода с числами под строкой с кодом в том же сегменте; родитель — ближайшая
строка с кодом выше. Цена: строка формы с потерянным кодом под другой строкой скрывается до нарушения §7 у
родителя (цель отсутствует — нарушение, G-2); лечится назначением кода (Р-13).
10. **Конвенция знака** для членов `abs_terms` (F-1): форма записи у большинства членов `abs_terms` колонки с непустым
`amount` и `is_recognized = true`; проверяется только у ячеек, нуждающихся в покрытии (V-13). Цена: ОФР, где
6. **Грамматика `locator`** — пары `ключ:значение` через `;`, ключи по источнику, как сейчас. Цена: нулевая.
7. **Грамматика `ref`** — OCR `row:<row_index>[#<k>]`, Excel `<лист>!<A1>`; `ref` строки уникален в документе.
8. **`value`, который не число** (дата, логическое) в колонке сумм — `amount = null`, `string_value` = текст,
`is_recognized = false`. Цена: минимальная.
9. **Предел и квантование суммы** — 16 целых цифр, 2 знака, half-up — только для числового `value` Excel; текст OCR с
тремя и более знаками после разделителя остаётся `null` + `string_value` (решение № 30 среза 3a); «догадка из цифр»
длиннее 16 — `null`.
10. **`parse_amount` знает источник**: запятая и точка у Excel — десятичные, у OCR — тысячи при группах из трёх цифр.
Единственное место, где шаги 2–8 смотрят на `source`. Разбор ОСВ не затрагивается.
11. **`checks` отсутствует у документа без строк**, обязателен у `BALANCE` / `INCOME` со строками.
12. **Правило `detail`**: строка без кода с содержимым под строкой с кодом (родитель продолжается через границу
сегмента); ячейка кода без единой цифры — пустая. Цена: строка формы с потерянным кодом под другой строкой
скрывается до нарушения §7 у родителя (цель отсутствует — нарушение); лечится назначением кода (Р-13).
13. **Посчитанное соотношение** — непуста цель или хотя бы один член; пустая цель при непустых членах и непустая
цель при пустых членах — нарушение; сегодня соотношение с отсутствующей целью пропускается у обоих сервисов и
ядра. Цена: формы без подытогов — только с упрощённым набором соотношений, иначе `needs_check`, как и сегодня.
14. **Упрощённая форма** определяется только по структуре кодов (ББ: есть 1600, нет 1100 и 1200; ОФР: есть 2400 или
2110, нет 2100, 2200, 2300) и получает свой набор соотношений и перечень кодов; ОКУД 0710096 признаком не служит.
Цена: ошибка определения → не те соотношения → `needs_check`; образца упрощённой формы в fixtures пока нет.
15. **Конвенция знака** для членов `abs_terms`: форма записи у большинства членов `abs_terms` колонки с непустым
`amount`, `is_recognized = true` и без правки; проверяется только у ячеек, нуждающихся в покрытии. Цена: ОФР, где
сомнительные расходы записаны не так, как остальные, подсвечивает их; это и нужно.
11. **Хранить `needs_review`, `text`, нарушения и предупреждения** в ядре (`financial_statement_value.needs_review`,
`source_text`, `financial_statement_line.needs_review`, `document_issue`) вместо пересчёта на каждом GET (F-8, V-3,
V-5) — пересматривает решение v5.13 «считается, не хранится». Цена: ещё одна таблица и миграция; взамен ядро при
чтении ничего не считает.
12. **Эффективная дата документа** = дата отчёта, иначе самая поздняя абсолютная дата колонок (F-6) — та же формула,
что `statement_period` ядра; от неё считаются относительные подписи и выбирается редакция перечня кодов.
13. **Предел и квантование суммы** в общем `parse_amount` — 16 целых цифр, 2 знака, half-up, как `quantize_money`
парсера (F-20); «догадка из цифр» длиннее 16 — `null`.
14. **Цель соотношения отсутствует при непустых членах — жёсткое нарушение** (G-2); сегодня пропуск у обоих сервисов и
ядра. Цена: упрощённые формы без подытогов — только с упрощённым набором соотношений (G-17), иначе всегда
`needs_check`, как и сегодня.
15. **Упрощённая форма** определяется по ОКУД 0710096 или по структуре (есть 1600, нет 1100 и 1200) и получает свой
набор соотношений и перечень кодов (G-17). Цена: ошибка определения → не те соотношения → `needs_check`.
16. **Грамматика `ref`** (G-9): OCR `row:<row_index>[#<k>]`, Excel `<лист>!<A1>`; уникален в документе.
17. **`parse_amount` знает источник** (G-16): запятая у Excel — десятичный разделитель, у OCR — тысячи при группе из
трёх цифр. Единственное место, где шаги 2–8 смотрят на `source`.
18. **`document_issue`**: очищается при перезагрузке файла, записи источника `core` заменяются при каждом пересчёте,
`line_id` — список (G-25).
19. **Границы пакета `arm_statements`** (G-14): свои типы входа и выхода, конвертация в модель контракта на стороне
хоста, golden внутри пакета на синтетике, тесты на реальных фикстурах — только в сервисах.
16. **Подтверждение даты отчёта структурой**: неуверенная дата отчёта подтверждается совпадением с самой поздней
датой колонок, неуверенная подпись колонки — попаданием в ожидаемые даты; иначе `error`. Цена: скан с верной, но
неуверенной датой без колонок с датами — `needs_check`, снимается правкой даты.
17. **Хранить `needs_review`, `text`, подтверждения, нарушения и предупреждения** в ядре (`financial_statement_value`,
`financial_statement_line`, `financial_statement_meta`, `document_issue`) вместо пересчёта на каждом GET —
пересматривает решение v5.13 «считается, не хранится». Цена: ещё одна таблица и миграция; взамен ядро при чтении
ничего не считает, а подтверждения аналитика переживают пересчёт.
18. **Эффективная дата документа** = дата отчёта, иначе самая поздняя абсолютная дата колонок — та же формула, что
`statement_period` ядра; от неё считаются относительные подписи и выбирается редакция перечня кодов.
19. **`document_issue`**: очищается при перезагрузке файла, записи `relation` / `soft` заменяются целиком при
пересчёте, `line_id` — список; `error` сервиса снимаются только правкой из таблицы соответствия.
20. **Границы пакета `arm_statements`**: свои типы входа и выхода, конвертация в модель контракта на стороне хоста,
golden внутри пакета на синтетике, тесты на реальных фикстурах — только в сервисах; расхождение версий копий —
`info`.
21. **Алгоритм структурного подтверждения кода**: наибольшая подпоследовательность по порядку бланка, редакция с
длиннейшей, пересечение при неоднозначности, уверенные коды — якоря, пятизначные — через родителя. Цена: при
двух переставленных неуверенных соседях оба `needs_review`; это и нужно.
22. **Слияние шаблонной строки с назначенным кодом**: шаблонная строка с суммами аналитика — 409 `LINE_EXISTS`, без
сумм — заменяется настоящей. Цена: лишний шаг у аналитика в редком случае; двойного счёта нет.
23. **Рабочие имена** `FormCandidate`, `TableLayout`, `RawLine`, `needs_review`, `line_status`, `line_code_text`,
`row_index`, `source_ref`, `parent_ref`, `period_kind`, `form_variant`, `document_issue`, `statements_version`,
`is_confirmed`, `LINES_UNRESOLVED`, `DUPLICATE_FORM`, `arm-statements`, `sync-statements` — по образцу существующих
(`sync-web.sh`); меняются при написании спеки без последствий.