docs(#116): целевая картина по шагам 1–9 — единый выход каждого шага и решения по строкам различий

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-17 15:05:31 +04:00
parent b86924bb93
commit e92840fe7d

View File

@ -1,6 +1,7 @@
# Единый алгоритм нормализации ББ/ОФР (scan-gateway и tabular-parser) — дизайн
Issue: arm-ka-docs#116. Статус: черновик, идёт разбор текущего процесса по шагам (brainstorming 17.10.2026).
Issue: arm-ka-docs#116. Статус: черновик; текущая цепочка описана, целевая картина по шагам 1–9 — предложение, ждёт
решений владельца Р-1…Р-6 (brainstorming 17.10.2026).
## Цель
@ -314,19 +315,191 @@ sheet:<A>,<B>`. `DISCLOSURE` и форма без колонки кодов —
## Целевая картина
### Шаг 1 — решения владельца по различиям (17.10.2026)
Каждый шаг ниже: **вход = выход предыдущего шага**, единый выход для обоих сервисов, таблица «различие → к чему
приводим → что меняется в scan-gateway → что меняется в tabular-parser». Решения владельца помечены датой; остальное —
предложение (17.10.2026), решения, которые нужны от владельца, собраны в конце (Р-1…Р-6). Имена полей — рабочие,
уточняются при написании спеки.
1. **Кто размечает** — различие остаётся: у шлюза разметку делает ИС, у парсера — сам парсер. Это природа источников.
2. **Единица разметки** — на выходе шага 1 у обоих сервисов **одинаковый контракт**. Форма контракта (json или
промежуточные объекты в коде сервиса) — решается отдельно.
3. **Шапка** — у **каждой таблицы своя**. Шлюз отдаёт шапку json каждой его таблице.
4. **Тип формы** — определяется **единым подходом** в обоих сервисах (правило — открытый вопрос).
5. **Уверенность** — есть и у парсера, всегда 100: формат один, значение у Excel-пути постоянное.
6. **Многостраничная форма** — парсер **склеивает листы-продолжения в шаге 1**; на выходе шага 1 одна форма — одна
таблица, как у ИС.
7. **Ячейка** — **один общий формат** для обоих сервисов (состав — открытый вопрос).
Принципы, из которых выведены решения по строкам:
Открытые вопросы шага 1: форма контракта (п. 2), правило типа формы (п. 4), формат ячейки (п. 7), как парсеру склеивать
продолжения до шага 2 (п. 6: сейчас склейка опирается на раскладку колонок).
1. **Один выход на шаг.** Код, зависящий от источника, живёт только в шаге 1 (адаптеры «json ИС → кандидат», «лист
Excel → кандидат»). Шаги 2–8 — общий код с общими словарями (форма общего кода — Р-1).
2. **Ничего не выбрасывать молча.** Строка, колонка или ячейка, которую не удалось истолковать, идёт дальше со
статусом, а не превращается в счётчик; пользователь её видит и исправляет.
3. **Качество — структурно и с привязкой к месту:** уверенность ячейки (от источника), `is_recognized` (разбор числа),
статус строки, нарушения §7 по ячейкам, предупреждения с уровнем и привязкой. Статус документа сервис выводит из
них сам; ядро не пересчитывает.
4. **Один словарь — одно место:** строки форм, проверки §7, периоды, заголовки форм, диапазоны кодов.
Шаги 2–8 целевой картины — по ходу brainstorming, по той же схеме: решение по каждой строке таблицы различий.
### Шаг 1 — Подготовка данных
**Решения владельца (17.10.2026):** 1) кто размечает — различие остаётся; 2) на выходе одинаковый контракт;
3) шапка своя у каждой таблицы; 4) тип формы — единый подход; 5) уверенность есть и у парсера, всегда 100;
6) парсер склеивает продолжения в шаге 1; 7) ячейка — один формат.
**Вход:** байты файла (шаг 0).
**Единый выход:** список кандидатов в форму на файл, `FormCandidate`:
- `source` — `ocr` | `excel`;
- `locator` — откуда взят (имя json и номер таблицы / имена листов);
- `type_hint` — `BALANCE` | `INCOME` | `DISCLOSURE` | `null`, по единому правилу (п. 4);
- `evidence` — ОКУД и заголовок как ячейки (для сверки с кодами в шаге 6);
- `header` — поля `header` контракта (`organization_name`, `inn`, `report_date`, `units`, `okud`, `okpo`, `okopf`,
`okfs`, `address`), каждое — ячейка или `null`;
- `rows` — плотная сетка строк × ячеек; все страницы и листы формы уже в одной сетке; пустая позиция — `null`;
- `warnings` — предупреждения шага (формат — шаг 8).
Ячейка `Cell` одна для шапки и таблицы: `text` (как показывает источник), `value` (число или дата, если источник их
знает; у OCR — `null`), `confidence` 0–100 (у Excel — 100), `ref` (позиция в источнике для сообщений: `row_index` /
`column_index` у OCR, адрес A1 у Excel).
| № | Различие | Приводим к | scan-gateway | tabular-parser |
|---|---|---|---|---|
| 1 | Кто размечает | остаётся, фиксируется полем `source` | — | — |
| 2 | Единица разметки | файл → список кандидатов; шаги 2–8 работают только с кандидатом (Р-1 — форма общего кода) | адаптер «таблица json → кандидат» | адаптер «блок формы на листе → кандидат» |
| 3 | Шапка | у каждого кандидата своя; поля = поля `header` контракта, каждое — `Cell`; разбор текста поля (ИНН → цифры, дата → `date`, единицы → словарь) — общий код по `text` | шапку json копирует в каждую свою таблицу | добавляет сырой текст и уверенность 100 |
| 4 | Тип формы | одно правило: ОКУД → заголовок по одному словарю → `null`; окончательно — по кодам строк в шаге 6, у обоих | `label` таблицы становится заголовком; `statement_labels.yaml` уходит в общий словарь | добавляет сверку по кодам (шаг 6) |
| 5 | Уверенность | поле `Cell.confidence`; Excel всегда 100 | — | добавляет поле |
| 6 | Многостраничная форма | одна форма = одна сетка на выходе шага 1; строки продолжения дописываются в конец сетки по маркеру «Форма … с.2» или по позиции листа; проверка «число колонок сумм совпало» из склейки уходит (повторная шапка внутри сетки — правило шага 2, общее для обоих: у OCR она и так внутри таблицы); тип из маркера сверяется с `type_hint`, расхождение — отдельный кандидат + предупреждение | — | склейка переезжает до раскладки; `join_blocker` остаётся только по типу |
| 7 | Ячейка | один формат `Cell`; Excel: `text` — как показывает числовой формат, `value` — число по формату (tp#42; формат не разобран — сырое значение + предупреждение); OCR: `text` сырой, `value` `null`, `confidence` от ИС | обёртка над `value` + `confidence` ИС | отображаемый текст и значение считаются здесь, а не в шаге 4 |
### Шаг 2 — Раскладка таблицы
**Вход:** `FormCandidate` (сетка, `type_hint`).
**Единый выход:** `TableLayout` — столбец кода, столбец наименования, колонки значений `[{col, label, period_end}]`,
роль каждой строки сетки (`header` | `numbering` | `data`), сегменты (участки сетки между повторными шапками, у
каждого своя раскладка колонок).
| № | Различие | Приводим к | scan-gateway | tabular-parser |
|---|---|---|---|---|
| 1 | Что считается кодом при поиске столбца | одно правило: 4–5 цифр (допускается `.0`), первые четыре — в диапазонах словаря кодов всех форм (`type_hint` неизвестен — объединение диапазонов); столбец с наибольшим числом кодов, кодов ≥ 3, доля среди непустых ≥ порога; нет — «колонка кодов не найдена» | `int(text)` → правило словаря (закрывает AURORA-1517 вместе с п. 5) | диапазон формы → объединение; `relaxed` для одиноких продолжений не нужен (они склеены в шаге 1) |
| 2 | Повторная шапка внутри таблицы | одно правило: строка внутри сетки с признаком шапки («Код» / та же сигнатура) открывает новый сегмент; у сегмента своя раскладка (позиции колонок могут сдвинуться); колонки значений сегментов сопоставляются по порядку; число не совпало — предупреждение, значения по позиции | как сейчас, через общее правило | добавляет: бывший «лист-продолжение со своей раскладкой» становится сегментом |
| 3 | Дата колонки | один словарь периодов (`statement_periods.yaml`) → `period_end`; перед разбором — очистка двойников OCR (общий шаг, у Excel не срабатывает) | `parse_date` «размером с дату» → словарь | добавляет очистку двойников (no-op) |
| 4 | Без шапки | одно правило: строка заголовка не найдена → колонки значений = столбцы правее кода, где в строках с кодами стоят числа; `label` пустая, `period_end` `null`; одно предупреждение «строка заголовка не найдена» | синтез шапки уходит (он эквивалентен) | добавляет предупреждение |
| 5 | Строка нумерации «1 2 3 4 5» | одно правило `is_numbering_row` → роль `numbering`, в данные не попадает | добавляет (AURORA-1517) | как сейчас |
| 6 | Подпись без периода | одно правило: подпись — период по словарю → колонка с датой; подпись из словаря исключений («Изменение», «Отклонение», «%», «Темп», «Доля») → пропуск с предупреждением info; иначе, если в столбце есть числа в строках с кодами — колонка без даты + предупреждение (Р-6); иначе пропуск | добавляет словарь исключений | перестаёт молча пропускать неизвестные числовые колонки |
### Шаг 3 — Отбор строк данных
**Вход:** `FormCandidate` + `TableLayout`.
**Единый выход:** `RawLine[]` — по строке на каждую строку сетки с ролью `data`, у которой есть код или хотя бы одно
число: `ref`, ячейка кода, `code_status` (`ok` | `unreadable` — код есть, но не разобран | `missing` — кода нет, суммы
есть | `duplicate` — код уже встречался), ячейка наименования, ячейки значений по колонкам раскладки. Строка без кода
и без чисел (заголовок раздела «АКТИВ», «в том числе:») — не строка. Счётчиков отброшенных нет — есть статусы.
| № | Различие | Приводим к | scan-gateway | tabular-parser |
|---|---|---|---|---|
| 1 | Код-мусор | статус `unreadable`, строка сохраняется с сырым текстом кода (AURORA-1462: «ИЧ,250» видна, аналитик назначает 1250) | счётчик → статус | отличает «нет кода» от «не разобран» |
| 2 | Склейка двух строк в одну | одно правило: ячейка кода содержит N кодов через перевод строки и ячейки значений — N частей → N строк (OQ-156); у Excel не срабатывает | как сейчас, через общее правило | правило общее, no-op |
| 3 | Повтор кода | статус `duplicate` у второй и последующих строк; строки сохраняются; текстовое предупреждение и `checks.duplicate_line_codes` уходят; что показывать и как считать — ядро (Р-4) | «оставлена первая» → обе строки | `duplicates` → статус |
| 4 | Отброшенная строка | строки не отбрасываются: `unreadable` / `missing` / `duplicate` идут дальше со статусом (AURORA-1634, 1462, 1406); ядро не достраивает вместо них пустую шаблонную строку молча | счётчики → статусы | счётчики → статусы |
### Шаг 4 — Значения ячеек
**Вход:** ячейки значений `RawLine` по колонкам раскладки (`Cell` или `null`).
**Единый выход:** `StatementValue {column_index, amount, string_value, confidence, is_recognized}` — как в контракте,
с одним смыслом полей у обоих.
| № | Различие | Приводим к | scan-gateway | tabular-parser |
|---|---|---|---|---|
| 1 | Предобработка текста | у ячейки после шага 1 либо есть `value`, либо только `text`. `value` есть → `amount = value` (2 знака), `is_recognized = true`. `value` нет → один `parse_amount(text)` для обоих: двойники, разделители тысяч, скобки → минус, прочерк → `null`; Excel-ячейки «число текстом» идут тем же путём | `parse_amount` переезжает в общий код | `to_money` заменяется общим `parse_amount` |
| 2 | Смысл `is_recognized` | один: текст разобран как число без выброшенных и исправленных символов; `confidence` — отдельно, от источника; экран подсвечивает по любому из двух (как сейчас) | отвязывает от `confidence` | как сейчас |
| 3 | «42831 1» | в `parse_amount` пробел допустим только как разделитель тысяч (группы ровно по три цифры); иначе `amount` — лучшая догадка (цифры подряд), `is_recognized = false`, `string_value` — сырой текст (AURORA-1511, 1519) | меняется через общий код | то же правило, у Excel почти не срабатывает |
| 4 | Единицы | одно правило: единицы «тыс. руб.» и дробных ≥ 10 % → предупреждение документа | добавляет | как сейчас |
| 5 | Пустая ячейка | одно правило: позиции нет, текст пуст или прочерк → `amount = null`, `string_value = null`, `is_recognized = true`, `confidence` ячейки (нет ячейки — 100). Потерянную OCR-ячейку ловят проверки §7 (Р-2) | «ячейки нет → 0, не распознано» уходит | как сейчас |
### Шаг 5 — Код, наименование, редакция
**Вход:** `RawLine` (статус, ячейки кода и наименования) + `StatementValue[]`.
**Единый выход:** `StatementLine {line_status, line_code, line_code_text, line_code_confidence, line_name,
line_name_confidence, values}`: `line_code` — разобранный код (4–5 цифр) или `null` при `unreadable` / `missing`;
`line_code_text` — сырой текст ячейки кода; `line_status` = `code_status` шага 3.
| № | Различие | Приводим к | scan-gateway | tabular-parser |
|---|---|---|---|---|
| 1 | Источник наименования | одно правило: наименование — из файла, с уверенностью источника; справочник наименование не подменяет. Справочное имя по коду и редакции добавляет ядро из своего (единственного) словаря, экран показывает справочное, файловое — рядом (Р-3; AURORA-1406, 1522) | имя из `statement_lines.yaml` → имя из OCR | как сейчас |
| 2 | Редакция (66н / ФСБУ 4) | в сервисах не определяется; редакция — свойство словаря строк (по дате отчёта), используется в шаге 7 (проверка «код не из редакции формы») и в ядре при достройке шаблонных строк | `_choose_edition` уходит | — |
| 3 | Справочник строк | один словарь строк (в общем коде, Р-1); копия в шлюзе удаляется | удаляет `api/rules/statement_lines.yaml` | — |
| 4 | Уверенность кода | от источника (ИС / 100), без срезания до 99 по справочнику; «не в справочнике» — предупреждение к строке в шаге 7 | убирает `min(conf, 99)` | как сейчас |
### Шаг 6 — Сборка документа
**Вход:** `FormCandidate` (`type_hint`, `evidence`, `header`) + `TableLayout` + `StatementLine[]`.
**Единый выход:** `StatementDocument {document_type, locator, header, columns, lines}` без `checks`.
| № | Различие | Приводим к | scan-gateway | tabular-parser |
|---|---|---|---|---|
| 1 | Тип формы | одно правило: большинство кодов 1xxx → `BALANCE`, 2xxx → `INCOME`; совпало с `type_hint` или `type_hint` `null` — тип по кодам; противоречит — тип по кодам + предупреждение; кодов нет — `type_hint`; без колонки кодов: `type_hint` есть → документ без строк (`DISCLOSURE` — `done`, иначе `needs_check`), `type_hint` нет → не документ, предупреждение info | как сейчас, через общее правило | добавляет сверку с кодами |
| 2 | Склейка страниц | решена в шаге 1, различия нет | — | — |
| 3 | `locator` | одна грамматика «пары `ключ:значение` через `;`», набор ключей по источнику: `json:<имя>;table:<i>` / `sheet:<A>[,<B>]` | — | — |
Шапка документа — поля `header` кандидата как есть (`text` → `*_text`, `value` → поле, `confidence` → `*_confidence`).
### Шаг 7 — Самопроверки
**Вход:** `StatementDocument`.
**Единый выход:** `StatementChecks` у обоих: `relations_failed[]`, `soft_relations_failed[]` (каждое нарушение — с
кодами слагаемых и `column_index`, чтобы экран подсветил ячейки), `line_warnings[]` (код не из редакции формы по дате
отчёта, код не в словаре). `duplicate_line_codes` уходит в `line_status`.
| № | Различие | Приводим к | scan-gateway | tabular-parser |
|---|---|---|---|---|
| 1 | Проверки только у парсера; словарь §7 в двух копиях; ядро результат не читает | проверки — общий код с одним словарём `statement_checks.yaml` у обоих сервисов; для OCR нарушение §7 — главный признак неверно прочитанной цифры; ядро читает `checks` из результата и пересчитывает тем же кодом только после правок аналитика (PATCH) | добавляет шаг | `run_statement_checks` → общий код |
### Шаг 8 — Статус и предупреждения
**Вход:** `StatementDocument` + `StatementChecks` + предупреждения шагов 1–7.
**Единый выход:** у каждого документа `status` (`done` | `needs_check`) и `warnings[]`; у файла `metrics.warnings[]`;
статус конверта — агрегат. Предупреждение — структура `{code, severity: error | info, message, line_ref?,
column_index?}`, а не строка.
| № | Различие | Приводим к | scan-gateway | tabular-parser |
|---|---|---|---|---|
| 1 | Критерий `done` | один критерий из самого результата: раскладка найдена ∧ (организация или ИНН) ∧ дата отчёта ∧ все строки `ok` ∧ все значения `is_recognized` ∧ `confidence` всех ячеек ≥ порога (Р-5) ∧ жёсткие проверки §7 сошлись ∧ нет предупреждений уровня `error`. Excel выполняет условия уверенности автоматически | «ничего не выброшено и уверенность 100» → общий критерий | `_is_ok` → общий критерий; добавляются строки и значения |
| 2 | Влияние предупреждений | по уровню: `error` → `needs_check`, `info` — не влияет; у обоих одинаково | «любое → `needs_check`» уходит | предупреждения начинают влиять |
| 3 | Статус документа | в контракте: `document.status`; конверт `done` ⇔ все документы `done` ∧ нет предупреждений файла уровня `error`; иначе `needs_check`; документов нет — `error` (как сейчас) | добавляет | внутренний `ok` → в контракт |
| 4 | Формат предупреждений | структура (см. выше) вместо строк `<json>: table:<i>: …` / `sheet=<имя> …`; место — в `locator` документа, а не в тексте | меняет | меняет |
| 5 | Привязка | к документу, строке (`line_ref`) или ячейке (`line_ref` + `column_index`); экран показывает у места | меняет | меняет |
### Шаг 9 — Результат
Одинаково, как сейчас; контракт `recognition-result.schema.json` → **v1.3**: `line_status`, `line_code` nullable,
`line_code_text`; `document.status`; `document.warnings[]` и `metrics.warnings[]` структурные; `checks` обязателен для
`BALANCE` / `INCOME` у обоих конвейеров, нарушения с кодами слагаемых; `line_warnings`.
### Что уходит из ядра
- `build_statement`: повторная дедупликация колонок, кодов и значений (контракт гарантирует уникальность колонок;
повторы строк приходят со статусом `duplicate`).
- `statement_status` (D-6): `RECOGNIZED` ⇔ `document.status = done` ∧ ИНН ≠ `MISMATCH` (единственное знание ядра);
остальные признаки D-6 не пересчитываются.
- §7 при загрузке: `checks` берутся из результата; пересчёт общим кодом — только после PATCH.
- Словарь строк и словарь §7: единственный экземпляр в общем коде (Р-1); копии ядра и шлюза удаляются.
- Достройка шаблонных строк: по тому же словарю; строки `missing` / `unreadable` / `duplicate` показываются как
пришли, с подсветкой.
- Экран: подсветка ячеек по `confidence`, `is_recognized` и нарушениям §7 (по кодам слагаемых и колонке); строк — по
`line_status`; предупреждения документа — с привязкой к месту.
### Решения, которые нужны от владельца
- **Р-1** (шаг 1, п. 2) — форма общего кода: общий Python-пакет с dataclass-ами (сериализуемыми в json — golden-файлы
выхода шага 1 в fixtures), шагами 2–8 и словарями, который подключают оба сервиса и ядро (рекомендация); запасной
вариант, если платформа не даст ставить пакет из git или зеркала, — копия по скрипту, как `sync-web.sh`.
- **Р-2** (шаг 4, п. 5) — отсутствующая OCR-ячейка под колонкой: чистый `null` (предложение; потерю ловит §7) или
подсветка «не распознано», как сейчас (больше ложных подсветок, AURORA-1524).
- **Р-3** (шаг 5, п. 1) — наименование: из файла в сервисах, справочное — в ядре (= решение № 2 по спринту 2,
AURORA-1406 / 1522; договорённость ДБД «из справочника» выполняется на экране).
- **Р-4** (шаг 3, п. 3) — повторы кодов: сервисы отдают обе строки со статусом; показ обеих и сумма по договорённости
ДБД (AURORA-1431) — в ядре и на экране.
- **Р-5** (шаг 8, п. 1) — порог уверенности для `done` (= решение № 7 по спринту 2, AURORA-1519 / 1524); предложение —
100, как сейчас.
- **Р-6** (шаг 2, п. 6) — неизвестная числовая колонка без периода: оставлять без даты с предупреждением (`needs_check`)
или пропускать, как парсер сейчас.