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