584 lines
61 KiB
Markdown
584 lines
61 KiB
Markdown
# Единый алгоритм нормализации ББ/ОФР (scan-gateway и tabular-parser) — дизайн
|
||
|
||
Issue: arm-ka-docs#116. Статус: черновик; текущая цепочка описана, целевая картина по шагам 1–9 — предложение, ждёт
|
||
решений владельца Р-1…Р-6 (brainstorming 17.10.2026).
|
||
|
||
## Цель
|
||
|
||
Нормализация результата распознавания форм ББ/ОФР в scan-gateway (PDF → ИС Сканирование) и в tabular-parser
|
||
(Excel) должна идти по одной цепочке шагов. У каждого шага — определённый результат, который передаётся в следующий
|
||
шаг; результаты между шагами одинаковы для обоих сервисов (где это возможно). Все нестыковки собраны в таблицы
|
||
различий по каждому шагу.
|
||
|
||
Зачем: нормализовать каждый шаг; убрать бойлерплейт в arm-core (повторная дедупликация, пересчёт проверок, статус,
|
||
который ядро не может взять из результата); улучшить поддерживаемость (один словарь — одно место); улучшить
|
||
пользовательский опыт заказчика (старые ошибки распознавания AURORA-1406, 1462, 1511, 1517, 1519, 1522, 1523, 1524, 1634
|
||
— свойство конвейера, а не отдельные баги, см. issue #116).
|
||
|
||
## Как работает сейчас: цепочка шагов
|
||
|
||
Каждый шаг описан так: **вход → что делает scan-gateway → что делает tabular-parser → выход → различия**.
|
||
Код: scan-gateway `api/src/core/worker.py`, `gateway/`, `normalizer/ocr/`; tabular-parser `api/src/core/worker.py`,
|
||
`parser/book.py`, `parser/readers/`, `parser/statements/`. Правила: `05-domain-rules.md` §7, §11, §11.1, §12.
|
||
|
||
### Шаг 0. Загрузка и доставка до сервиса
|
||
|
||
**Вход:** файл пользователя.
|
||
|
||
Общая часть (arm-core): файл принимается в карточку ЮЛ или в консолидацию (`primary_files.upload_one`): тип — по
|
||
расширению и сигнатуре, дедуп по `(ЮЛ, sha256)`, файл — в S3, запись `stored_file` и `recognition_job` (`queued`).
|
||
Конвейер: xls / xlsx / xlsb → `tabular`, PDF / PNG / JPG → `image`. Публикатор outbox собирает `recognition-request`
|
||
(`job_id`, `file_id`, `pipeline`, `file_type`, `original_name`, ссылка S3, `reply_deadline`, `attempt`) и публикует
|
||
в subject конвейера (`files.tabular_uploaded` / `files.img_uploaded`). Срок ответа по умолчанию: OCR — 20 мин,
|
||
парсер — 5 мин; нет ack 30 с — повтор, после трёх повторов — `dlq`, файл `ERROR`.
|
||
|
||
В сервисе (одинаково): запрос проверяется (JSON-объект, MAJOR версии 1, свой `pipeline`, схема; иначе
|
||
`SCHEMA_MISMATCH` без ack); повтор `job_id` в работе — только повторный ack; ack `processing` до чтения S3; файл
|
||
читается из S3 (не прочитан — `FILE_READ_ERROR`). scan-gateway дополнительно: бакет должен быть его единственным;
|
||
`reply_deadline` истёк до отправки — `TIMEOUT`.
|
||
|
||
**Выход:** байты исходного файла + запрос (`file_type`, `original_name`, `reply_deadline`).
|
||
|
||
**Различия:** нет, кроме сроков ответа и проверки бакета.
|
||
|
||
### Шаг 1. Подготовка данных
|
||
|
||
**Вход:** байты файла.
|
||
|
||
#### 1.1 Разбор и чтение сырых данных
|
||
|
||
**scan-gateway** — разбор делает ИС Сканирование:
|
||
1. Файл пишется во «Входящие» горячей папки как `<job_id>.<ext>` (нет расширения / папка недоступна —
|
||
`RECOGNITION_FAILED`).
|
||
2. ИС кладёт в «Исходящие» `<job_id>/` один или несколько json — по одному на распознанный комплект отчётности.
|
||
3. Опрос каждые `SCAN_POLL_SECONDS` (5 с); набор готов, когда есть `*.json` и он не менялся `SCAN_SETTLE_SECONDS`
|
||
(2 с); срок — `min(reply_deadline, запись + SCAN_TIMEOUT_SECONDS)` (1200 с), иначе `TIMEOUT`.
|
||
4. Склейка: каждый json разбирается независимо, по возрастанию имени → `{job_id, files: [{name, response,
|
||
parse_error}]}`; сохраняется в S3 `raw/{job_id}.json` **до** нормализации; затем (только при `SCAN_CLEANUP`)
|
||
папка очищается.
|
||
|
||
Содержимое одного json (по `fixtures/ocr/*.json`, §11): корень `{file_uid, error {code, message}, data_ocr}`,
|
||
успех — `error.code == ""`; `data_ocr`: поля шапки `label`, `organization`, `inn`, `units`, `date` — каждое
|
||
`{form_name, confidence}` (сырой OCR-текст), **одна шапка на json**; `table_structure[]` — таблицы форм `{label
|
||
{form_name, confidence}, rows[{row_index, cells[{column_index, value, confidence}]}]}`, `column_index` не обязательно
|
||
подряд. Наблюдается: json с таблицами содержит ББ и ОФР одного периода под общей шапкой. **Допущение владельца
|
||
(17.10.2026):** несколько периодов в одном файле ИС отдаёт разными json; образца нет — проверить на первом таком файле.
|
||
|
||
**tabular-parser** — чтение делает парсер:
|
||
1. Файл больше `PARSE_MAX_BYTES` (50 МБ) — `UNSUPPORTED_FORMAT`; `PARSE_TIMEOUT_SECONDS` (120 с) — от начала разбора.
|
||
2. Формат — по сигнатуре байтов (OLE2 → XLS, zip с `xl/workbook.bin` → XLSB, иной zip → XLSX, прочее —
|
||
`UNSUPPORTED_FORMAT`); несовпадение с `file_type` — предупреждение; лимиты распакованного zip и площади
|
||
объединённых ячеек.
|
||
3. Книга → листы `Sheet`: непустые ячейки (текст, значение, числовой формат), объединённые диапазоны, уровни
|
||
группировки, габариты. Библиотека упала — `UNREADABLE`; xlsb без стилей и группировки — предупреждение.
|
||
|
||
#### 1.2 Базовая разметка
|
||
|
||
**scan-gateway** — разметку сделала ИС, шлюз её принимает: ошибочный json (`parse_error`, непустой `error.code`, корень
|
||
не объект) — предупреждение `<имя>: <код>: <сообщение>` и пропуск; все json ошибочны — `error` с кодом первого. Шапка
|
||
json: ИНН — только цифры, организация, дата (двойники + разбор), единицы; уверенность ИС у каждого поля;
|
||
предупреждения уровня файла «ИНН не распознан» / «ИНН распознан неуверенно» / «дата отчёта не разобрана». Каждая
|
||
таблица `table_structure[i]` — кандидат в документ; тип — по `label` (`rules/statement_labels.yaml`).
|
||
|
||
**tabular-parser** — разметка по содержимому (§12 п. 1, 6): вид листа (признак ведомости в первых 30 строках —
|
||
ОСВ, отдельный путь; заголовок формы или ОКУД — форма; маркер «Форма … с.2», шапка «Код» с периодами, колонка кодов
|
||
или 1–2 кода сразу после листа формы — продолжение; иначе прочее с предупреждением). Формы на листе: строки с
|
||
заголовком («Бухгалтерский баланс», «Отчет о финансовых результатах», «…о прибылях и убытках») или ОКУД —
|
||
кандидаты; без кодов между ними сливаются; заголовок после кодов — новая форма. Тип по ОКУД (0710001 → `BALANCE`,
|
||
0710002 → `INCOME`, прочие 0710xxx и «Пояснения» → `DISCLOSURE`), заголовок спорит с ОКУД — ОКУД и предупреждение.
|
||
Шапка **своя у каждой формы** в границах блока: организация, ИНН (без ИНН аудитора), ОКПО, ОКОПФ, ОКФС, единицы,
|
||
дата, адрес; найдено — 100, нет — `null`; предупреждения «в шапке не найдена организация» / «…единица измерения».
|
||
Листы-продолжения присоединяются к последней открытой форме — но решение требует раскладки колонок (шаг 2).
|
||
|
||
**Выход:** список кандидатов в документы форм — признак типа, шапка, сырые строки таблицы (строки × ячейки).
|
||
|
||
**Различия (шаг 1):**
|
||
|
||
| № | | scan-gateway | tabular-parser |
|
||
|---|---|---|---|
|
||
| 1 | Кто размечает | ИС Сканирование | парсер, по содержимому |
|
||
| 2 | Единица разметки | json (комплект одного периода) → таблицы | книга → листы → блоки форм |
|
||
| 3 | Шапка | одна на json, общая для его таблиц | своя у каждой формы |
|
||
| 4 | Тип формы | `label` таблицы (уточнение по кодам — шаг 6) | ОКУД, иначе заголовок |
|
||
| 5 | Уверенность | у каждого поля шапки и каждой ячейки, 0–100 | нет (найдено — 100) |
|
||
| 6 | Многостраничная форма | одна таблица = одна форма, склейки нет | листы-продолжения присоединяются (шаг 6) |
|
||
| 7 | Ячейка | сырой OCR-текст + `confidence` | текст, значение и числовой формат |
|
||
|
||
### Шаг 2. Раскладка таблицы
|
||
|
||
**Вход:** сырые строки таблицы одной формы.
|
||
|
||
**scan-gateway** (`layout.py`, `rows.parse_table_rows`, §11.1 п. 3): шапка — строка с ячейкой, начинающейся на
|
||
«код»; наименование — ячейка «наименование», иначе ближайшая заполненная слева от кода; колонки значений — правее
|
||
кода, где в тексте четыре цифры года или текст начинается на «на » / «за ». **Повторная шапка внутри таблицы
|
||
перезаписывает раскладку** (число колонок изменилось — предупреждение; значения строки берутся по позиции, лишние
|
||
отбрасываются, недостающие — «ячейки нет»). Без шапки раскладка выводится по данным: код — столбец с наибольшим
|
||
числом значений из справочника кодов, наименование — левее с наибольшим числом текстов, значения — правее, где
|
||
суммы не реже мусора; шапка синтезируется, колонки без дат, предупреждение. Ни шапки, ни столбца кода — таблица
|
||
пропускается («колонка кодов не найдена»). Колонка: `label` — текст шапки, `period_date` — разбор даты после замены
|
||
букв-двойников, только если текст «размером с дату»; нет даты — «колонка N без даты».
|
||
|
||
**tabular-parser** (`columns.find_layout`, §12 п. 2–4): колонка кодов — столбец с наибольшим числом кодов (4–5
|
||
цифр, первые четыре в диапазоне формы), кодов ≥ 3 (у продолжения ≥ 1) и доля среди непустых ≥ порога; нет —
|
||
документ без строк, «колонка кодов не найдена». Строка заголовка — последняя над первым кодом с подписью «Код», иначе
|
||
последняя с двумя и более периодами правее кодов (или одним не голым годом); двухстрочный заголовок склеивается;
|
||
строка нумерации «1 2 3 4 5» — часть заголовка. Колонки-периоды — правее кодов с распознанным периодом по словарю
|
||
`statement_periods.yaml`; подпись без периода («Изменение», «%») пропускается, одно предупреждение со списком.
|
||
Наименование — левее кодов с наибольшим числом текстов, кроме «Пояснения». Без заголовка (продолжение): колонки
|
||
значений — где в строках с кодами есть числа, подпись пустая, дата `null`.
|
||
|
||
**Выход:** раскладка формы — столбец кода, столбец наименования, колонки значений `[{label, period_date}]`, первая
|
||
строка данных, строки, которые не данные (шапка, нумерация).
|
||
|
||
**Различия (шаг 2):**
|
||
|
||
| № | | scan-gateway | tabular-parser |
|
||
|---|---|---|---|
|
||
| 1 | Что считается кодом при поиске столбца | любое целое число (`int(text)`) | 4–5 цифр в диапазоне формы (словарь) |
|
||
| 2 | Повторная шапка внутри таблицы | перезаписывает раскладку | не ищется (одна раскладка на блок) |
|
||
| 3 | Дата колонки | «размером с дату» + двойники + `parse_date` | словарь периодов `period_end` |
|
||
| 4 | Без шапки | синтез шапки, колонки без дат, предупреждение | колонки по числам, подпись пустая |
|
||
| 5 | Строка нумерации «1 2 3 4 5» | не распознаётся (становится строкой данных, AURORA-1517) | часть заголовка, в данных пропускается |
|
||
| 6 | Подпись без периода | колонка значений, «без даты» | колонка пропускается, предупреждение |
|
||
|
||
### Шаг 3. Отбор строк данных
|
||
|
||
**Вход:** раскладка + сырые строки.
|
||
|
||
**scan-gateway** (`rows.py`, §11.1 п. 4): склеенная строка («1500\n1700» с N частями в суммах) разлепляется на N
|
||
(OQ-156, предупреждение-счётчик). Классификация по ячейке кода: пустой код — пропуск (есть суммы — счётчик «строк без
|
||
кода с суммами пропущено»); **нечисловой код («4 400\nI #4У», «ИЧ,250») — пропуск** (счётчик «строк с нечисловым
|
||
кодом пропущено»); целое число — строка данных (вид `ROW` / `TOTAL` / `BALANCE` по наименованию — дальше не
|
||
используется). Повтор кода — остаётся первая, предупреждение «код N повторяется: оставлена первая строка». Пропущенные
|
||
строки **не восстанавливаются**.
|
||
|
||
**tabular-parser** (`lines.collect_lines`, §12 п. 5): пустые строки пропускаются; код — 4–5 цифр в диапазоне, иначе
|
||
строка «без кода»: нумерация «1 2 3 4 5» — пропуск; без кода с ненулевыми суммами — счётчик «строк без кода с суммами
|
||
пропущено»; повтор кода — остаётся первая, код в `duplicates`.
|
||
|
||
**Выход:** строки данных формы — сырой код, сырое наименование, ячейки значений по колонкам; счётчики отброшенных.
|
||
|
||
**Различия (шаг 3):**
|
||
|
||
| № | | scan-gateway | tabular-parser |
|
||
|---|---|---|---|
|
||
| 1 | Код-мусор | отдельный счётчик «нечисловой код» | не отличается от «без кода» |
|
||
| 2 | Склейка двух строк в одну | разлепляется (OQ-156) | нет такого случая |
|
||
| 3 | Повтор кода | предупреждение текстом | `checks.duplicate_line_codes` |
|
||
| 4 | Отброшенная строка | только счётчик; строка исчезает (AURORA-1634, 1462) | только счётчик; строка исчезает |
|
||
|
||
### Шаг 4. Значения ячеек
|
||
|
||
**Вход:** ячейки значений строки.
|
||
|
||
**scan-gateway** (`amounts.parse_amount`, `document._value`, §11.1 п. 7): пробелы удаляются первыми; запятая /
|
||
точка как разделитель тысяч по шаблонам (`4,200`, `837.994`); запятая → точка; скобки → минус; из строки выбрасывается
|
||
всё, кроме цифр, `+`, `-`, `.`; больше одной точки — все удаляются; больше двух знаков после точки — `amount = null`,
|
||
текст в `string_value`. Прочерк / пусто — `null`. `is_recognized` = `confidence == 100` **и** при разборе не
|
||
выбрасывались символы. Ячейки под колонкой нет — `amount = null`, `confidence = 0`, `is_recognized = false`.
|
||
|
||
**tabular-parser** (`lines._value`, `statement_money`, `number_format.display_amount`, §12 п. 5): число с числовым
|
||
форматом — **как показывает формат ячейки** (масштабирующие запятые → тысячи, число знаков, секции и условия,
|
||
округление half-up; формат не разобран — сырое значение и одно предупреждение на документ); иначе `to_money`:
|
||
`Decimal`, скобки → минус, прочерк / пусто → `null`, нечисловой текст → `string_value`, `is_recognized = false`.
|
||
`confidence` всегда 100. Счётчики дробных значений: при «тыс.» и ≥ 10 % дробных — предупреждение.
|
||
|
||
**Выход:** `StatementValue {column_index, amount, string_value, confidence, is_recognized}`.
|
||
|
||
**Различия (шаг 4):**
|
||
|
||
| № | | scan-gateway | tabular-parser |
|
||
|---|---|---|---|
|
||
| 1 | Предобработка текста | OCR-мусор: пробелы, двойники, тысячи, скобки `}` / `]` | числовой формат ячейки Excel |
|
||
| 2 | Смысл `is_recognized` | уверенность 100 ∧ ничего не выброшено | текст разобрался как число |
|
||
| 3 | «42831 1» | пробел удалён → 428311, «чистое» (AURORA-1511) | не бывает |
|
||
| 4 | Единицы | не проверяются | предупреждение о дробных при «тыс.» |
|
||
| 5 | Пустая ячейка там, где была колонка | `null`, `confidence` ИС | `null`, 100 |
|
||
|
||
### Шаг 5. Код, наименование, редакция строки
|
||
|
||
**Вход:** строка данных с сырым кодом и наименованием.
|
||
|
||
**scan-gateway** (`document._line`, `_choose_edition`, §11.1 п. 6): редакция формы — по дате отчёта, иначе по самой
|
||
поздней дате колонок, иначе по кодам (предупреждение); код очищается до букв, цифр, пробела и точки; **наименование —
|
||
из справочника редакции по коду** (`api/rules/statement_lines.yaml` шлюза), уверенность кода и имени = уверенность
|
||
ячейки кода, если текст чистый и код в справочнике, иначе `min(conf, 99)`; кода нет в справочнике — имя из OCR со
|
||
своей уверенностью, код `min(conf, 99)`, предупреждение «код N из редакции …», если он есть в другой.
|
||
|
||
**tabular-parser** (`lines.collect_lines`, `_line_name`): код как найден (4–5 цифр); **наименование — из файла**:
|
||
ячейка колонки наименований, если пуста — ближайшая текстовая левее кодов, кроме «Пояснения»; уверенность кода 100,
|
||
имени 100 (пусто — `null`). Редакции и справочника нет.
|
||
|
||
**Выход:** `StatementLine {line_code, line_code_confidence, line_name, line_name_confidence, values}`.
|
||
|
||
**Различия (шаг 5):**
|
||
|
||
| № | | scan-gateway | tabular-parser |
|
||
|---|---|---|---|
|
||
| 1 | Источник наименования | справочник по коду (AURORA-1406, 1522) | файл |
|
||
| 2 | Редакция (66н / ФСБУ 4) | определяется, влияет на имена | не определяется |
|
||
| 3 | Справочник строк | свой `statement_lines.yaml` (не совпадает с ядром) | нет |
|
||
| 4 | Уверенность кода | от ИС, срезается до 99, если не в справочнике | всегда 100 |
|
||
|
||
### Шаг 6. Сборка документа
|
||
|
||
**Вход:** раскладка, строки, шапка кандидата.
|
||
|
||
**scan-gateway** (`document.build_document`, `_resolve_type`): тип — по `label`; все коды одной группы и противоречат
|
||
`label` — тип по кодам с предупреждением; `label` не сопоставлен — по большинству кодов. Колонки — из первой шапки.
|
||
`locator = file:<json>;table:<i>`. Шапка — поля json (общая).
|
||
|
||
**tabular-parser** (`statement._document`, `merge`): лист-продолжение присоединяется к последней открытой форме,
|
||
если число колонок сумм совпало и тип в маркере не противоречит (слабое продолжение с 1–2 кодами — ещё и коды из
|
||
диапазона формы); не присоединился — одинокое продолжение с пустой шапкой и предупреждение. `locator =
|
||
sheet:<A>,<B>`. `DISCLOSURE` и форма без колонки кодов — без `columns` / `lines`.
|
||
|
||
**Выход:** `StatementDocument {document_type, locator, header, columns, lines}` (без `checks`).
|
||
|
||
**Различия (шаг 6):**
|
||
|
||
| № | | scan-gateway | tabular-parser |
|
||
|---|---|---|---|
|
||
| 1 | Тип формы | `label`, сверка с кодами | ОКУД / заголовок, с кодами не сверяется |
|
||
| 2 | Склейка страниц | нет | листы-продолжения |
|
||
| 3 | `locator` | `file:…;table:i` | `sheet:A,B` |
|
||
|
||
### Шаг 7. Самопроверки
|
||
|
||
**Вход:** документ.
|
||
|
||
**scan-gateway:** не делает; `checks` отсутствует («не проверялось»).
|
||
|
||
**tabular-parser** (`statements/checks.run_statement_checks`, §12 п. 7): соотношения §7 по каждой колонке из
|
||
`statement_checks.yaml`: жёсткие → `relations_failed`, мягкие → `soft_relations_failed`; допуск ±1, округление
|
||
0,001 % уводит жёсткое в мягкие; `abs_terms`; «родитель = Σ подстрок» только при непустой детализации (AURORA-1527);
|
||
повторы кодов → `duplicate_line_codes`; предупреждения текстом на каждое нарушение.
|
||
|
||
**Выход:** `StatementChecks` или ничего.
|
||
|
||
**Различия (шаг 7):** есть только у парсера; словарь §7 у парсера и ядра — две копии; ядро результат парсера **не
|
||
читает**, считает заново.
|
||
|
||
### Шаг 8. Статус и предупреждения
|
||
|
||
**Вход:** документы и накопленные предупреждения.
|
||
|
||
**scan-gateway** (`_Build.fully_recognized`, `_normalize_file`, `normalize`): документ «полностью распознан», если ни
|
||
одного предупреждения, все ячейки `is_recognized`, уверенность кода и имени 100, ИНН и организация с уверенностью 100,
|
||
дата распознана. Файл — все документы распознаны ∧ нет предупреждений файла. Конверт `done` ⇔ все файлы распознаны ∧
|
||
ни один json не ошибочен; иначе `needs_check`; ни одной таблицы — `error RECOGNITION_FAILED`. Предупреждения —
|
||
плоские строки `<json>: table:<i>: <текст>` в `metrics.warnings`.
|
||
|
||
**tabular-parser** (`statement._is_ok`, `book.parse`): документ `ok` ⇔ колонка кодов найдена ∧ не одинокое
|
||
продолжение ∧ есть колонки ∧ в шапке организация или ИНН ∧ `checks` есть ∧ нет жёстких нарушений ∧ нет повторов
|
||
кодов; `DISCLOSURE` всегда `ok`. Книга `done` ⇔ все документы `ok` ∧ ни один лист не потерян; книга без листов ОСВ и
|
||
форм — `INVALID_DOCUMENT`. Предупреждения — строки `sheet=<имя> <текст>` в `metrics.warnings`; на статус не влияют.
|
||
|
||
**Выход:** `ResultStatus` конверта + `metrics.warnings[]`.
|
||
|
||
**Различия (шаг 8):**
|
||
|
||
| № | | scan-gateway | tabular-parser |
|
||
|---|---|---|---|
|
||
| 1 | Критерий `done` | всё с уверенностью 100 и без предупреждений | структура найдена, проверки сошлись |
|
||
| 2 | Влияние предупреждений | любое → `needs_check` | не влияют |
|
||
| 3 | Статус документа | нет (только конверт) | внутренний `ok`, в контракт не попадает |
|
||
| 4 | Формат предупреждений | `<json>: table:<i>: …` | `sheet=<имя> …` |
|
||
| 5 | Привязка предупреждения | к файлу | к документу (по префиксу) |
|
||
|
||
Общее для обоих: статус один на конверт; предупреждения — свободный текст без привязки к строке или ячейке; ядро и
|
||
экран не могут показать, что именно и где не так.
|
||
|
||
### Шаг 9. Результат
|
||
|
||
Одинаково: `RecognitionResult` (`schema_version`, `job_id`, `file_id`, `status`, `documents`, `metrics`) → S3
|
||
`results/{file_id}/{job_id}/result.json` → конверт в `files.tabular_parsed` с `Nats-Msg-Id = result:<job_id>:<attempt>`.
|
||
У шлюза в конверте ещё `raw_s3_key`.
|
||
|
||
### Что с результатом делает ядро (для полноты картины)
|
||
|
||
- `result_mapping.map_file` → `statement_mapping.build_statement`: **повторно** отбрасывает повторы колонок, кодов и
|
||
значений (защита от обоих производителей), пишет `financial_statement_*` как пришло.
|
||
- `statement_status` (D-6): `RECOGNIZED` ⇔ конверт `done` ∧ проверки §7 **пересчитаны ядром** и сошлись ∧ ИНН не
|
||
`MISMATCH` ∧ период известен ∧ все ячейки с уверенностью 100 и `is_recognized` ∧ есть строки ∧ нет предупреждений
|
||
маппинга; иначе `NEEDS_CHECK`. `checks` парсера не читаются; `metrics.warnings` сервиса на статус не влияют.
|
||
`RECOGNIZED` сразу автоподтверждается.
|
||
- `GET …/statement`: §7 считается заново по словарю ядра (копия словаря парсера); недостающие строки достраиваются
|
||
из `statement_lines.yaml` ядра (не совпадает со справочником шлюза); предупреждения в ответе отсутствуют.
|
||
- Экран: ячейка подсвечивается только по `confidence < 100` или `is_recognized = false`; нарушения §7 — списком без
|
||
привязки к ячейкам; предупреждения сервиса и маппинга нигде не показываются.
|
||
|
||
Бойлерплейт и потери, которые порождены различиями шагов 2–8: три копии словарей (§7 в парсере и ядре, строки в
|
||
шлюзе и ядре); двойная дедупликация; пересчёт проверок; статус, который ядро собирает заново из косвенных признаков;
|
||
три несвязанных канала качества (уверенность ячейки, проверки §7, предупреждения), из которых пользователь видит
|
||
полтора.
|
||
|
||
## Целевая картина
|
||
|
||
Каждый шаг ниже: **вход = выход предыдущего шага**, единый выход для обоих сервисов, таблица «различие → к чему
|
||
приводим → что меняется в scan-gateway → что меняется в tabular-parser». Решения владельца помечены датой; остальное —
|
||
предложение (17.10.2026), решения, которые нужны от владельца, собраны в конце (Р-1…Р-6). Имена полей — рабочие,
|
||
уточняются при написании спеки.
|
||
|
||
Принципы, из которых выведены решения по строкам:
|
||
|
||
1. **Один выход на шаг.** Код, зависящий от источника, живёт только в шаге 1 (адаптеры «json ИС → кандидат», «лист
|
||
Excel → кандидат»). Шаги 2–8 — общий код с общими словарями (форма общего кода — Р-1).
|
||
2. **Ничего не выбрасывать молча.** Строка, колонка или ячейка, которую не удалось истолковать, идёт дальше со
|
||
статусом, а не превращается в счётчик; пользователь её видит и исправляет.
|
||
3. **Качество — структурно и с привязкой к месту:** уверенность ячейки (от источника), `is_recognized` (разбор числа),
|
||
статус строки, нарушения §7 по ячейкам, предупреждения с уровнем и привязкой. Статус документа сервис выводит из
|
||
них сам; ядро не пересчитывает.
|
||
4. **Один словарь — одно место:** строки форм, проверки §7, периоды, заголовки форм, диапазоны кодов.
|
||
5. **Очистка OCR (§11.1) — не отдельный шаг, а первая стадия типовых разборов текста** в общем коде, там, где уже
|
||
известен смысл ячейки: ИНН и дата шапки (шаг 1), подписи колонок (шаг 2), код строки и разлепление склеенных
|
||
строк (шаг 3), сумма (шаг 4). Разбор вызывается для любой ячейки без `value`; у Excel он срабатывает только на
|
||
текстовых ячейках и почти всегда ничего не меняет. Факт исправления фиксируется там же: `is_recognized = false`
|
||
у значения, `unreadable` у кода, предупреждение у даты. Из `normalizer/ocr/` шлюза `text`, `dates`, `amounts`
|
||
становятся общими разборами, `layout`, `rows`, `header`, `document` — шагами 2–6 общего кода.
|
||
|
||
### Шаг 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).
|
||
|
||
- **1. Кто размечает** — остаётся, фиксируется полем `source`
|
||
- scan-gateway: без изменений
|
||
- tabular-parser: без изменений
|
||
- **2. Единица разметки** — файл → список кандидатов; шаги 2–8 работают только с кандидатом (Р-1 — форма общего кода)
|
||
- scan-gateway: адаптер «таблица json → кандидат»
|
||
- tabular-parser: адаптер «блок формы на листе → кандидат»
|
||
- **3. Шапка** — у каждого кандидата своя; поля = поля `header` контракта, каждое — `Cell`; разбор текста поля (ИНН → цифры, дата → `date`, единицы → словарь) — общий код по `text`
|
||
- scan-gateway: шапку json копирует в каждую свою таблицу
|
||
- tabular-parser: добавляет сырой текст и уверенность 100
|
||
- **4. Тип формы** — одно правило: ОКУД → заголовок по одному словарю → `null`; окончательно — по кодам строк в шаге 6, у обоих
|
||
- scan-gateway: `label` таблицы становится заголовком; `statement_labels.yaml` уходит в общий словарь
|
||
- tabular-parser: добавляет сверку по кодам (шаг 6)
|
||
- **5. Уверенность** — поле `Cell.confidence`; Excel всегда 100
|
||
- scan-gateway: без изменений
|
||
- tabular-parser: добавляет поле
|
||
- **6. Многостраничная форма** — одна форма = одна сетка на выходе шага 1; строки продолжения дописываются в конец сетки по маркеру «Форма … с.2» или по позиции листа; проверка «число колонок сумм совпало» из склейки уходит (повторная шапка внутри сетки — правило шага 2, общее для обоих: у OCR она и так внутри таблицы); тип из маркера сверяется с `type_hint`, расхождение — отдельный кандидат + предупреждение
|
||
- scan-gateway: без изменений
|
||
- tabular-parser: склейка переезжает до раскладки; `join_blocker` остаётся только по типу
|
||
- **7. Ячейка** — один формат `Cell`; Excel: `text` — как показывает числовой формат, `value` — число по формату (tp#42; формат не разобран — сырое значение + предупреждение); OCR: `text` сырой, `value` `null`, `confidence` от ИС
|
||
- scan-gateway: обёртка над `value` + `confidence` ИС
|
||
- tabular-parser: отображаемый текст и значение считаются здесь, а не в шаге 4
|
||
|
||
### Шаг 2 — Раскладка таблицы
|
||
|
||
**Вход:** `FormCandidate` (сетка, `type_hint`).
|
||
|
||
**Единый выход:** `TableLayout` — столбец кода, столбец наименования, колонки значений `[{col, label, period_end}]`,
|
||
роль каждой строки сетки (`header` | `numbering` | `data`), сегменты (участки сетки между повторными шапками, у
|
||
каждого своя раскладка колонок).
|
||
|
||
- **1. Что считается кодом при поиске столбца** — одно правило: 4–5 цифр (допускается `.0`), первые четыре — в диапазонах словаря кодов всех форм (`type_hint` неизвестен — объединение диапазонов); столбец с наибольшим числом кодов, кодов ≥ 3, доля среди непустых ≥ порога; нет — «колонка кодов не найдена»
|
||
- scan-gateway: `int(text)` → правило словаря (закрывает AURORA-1517 вместе с п. 5)
|
||
- tabular-parser: диапазон формы → объединение; `relaxed` для одиноких продолжений не нужен (они склеены в шаге 1)
|
||
- **2. Повторная шапка внутри таблицы** — одно правило: строка внутри сетки с признаком шапки («Код» / та же сигнатура) открывает новый сегмент; у сегмента своя раскладка (позиции колонок могут сдвинуться); колонки значений сегментов сопоставляются по порядку; число не совпало — предупреждение, значения по позиции
|
||
- scan-gateway: как сейчас, через общее правило
|
||
- tabular-parser: добавляет: бывший «лист-продолжение со своей раскладкой» становится сегментом
|
||
- **3. Дата колонки** — один словарь периодов (`statement_periods.yaml`) → `period_end`; перед разбором — очистка двойников OCR (общий шаг, у Excel не срабатывает)
|
||
- scan-gateway: `parse_date` «размером с дату» → словарь
|
||
- tabular-parser: добавляет очистку двойников (no-op)
|
||
- **4. Без шапки** — одно правило: строка заголовка не найдена → колонки значений = столбцы правее кода, где в строках с кодами стоят числа; `label` пустая, `period_end` `null`; одно предупреждение «строка заголовка не найдена»
|
||
- scan-gateway: синтез шапки уходит (он эквивалентен)
|
||
- tabular-parser: добавляет предупреждение
|
||
- **5. Строка нумерации «1 2 3 4 5»** — одно правило `is_numbering_row` → роль `numbering`, в данные не попадает
|
||
- scan-gateway: добавляет (AURORA-1517)
|
||
- tabular-parser: как сейчас
|
||
- **6. Подпись без периода** — одно правило: подпись — период по словарю → колонка с датой; подпись из словаря исключений («Изменение», «Отклонение», «%», «Темп», «Доля») → пропуск с предупреждением info; иначе, если в столбце есть числа в строках с кодами — колонка без даты + предупреждение (Р-6); иначе пропуск
|
||
- scan-gateway: добавляет словарь исключений
|
||
- tabular-parser: перестаёт молча пропускать неизвестные числовые колонки
|
||
|
||
### Шаг 3 — Отбор строк данных
|
||
|
||
**Вход:** `FormCandidate` + `TableLayout`.
|
||
|
||
**Единый выход:** `RawLine[]` — по строке на каждую строку сетки с ролью `data`, у которой есть код или хотя бы одно
|
||
число: `ref`, ячейка кода, `code_status` (`ok` | `unreadable` — код есть, но не разобран | `missing` — кода нет, суммы
|
||
есть | `duplicate` — код уже встречался), ячейка наименования, ячейки значений по колонкам раскладки. Строка без кода
|
||
и без чисел (заголовок раздела «АКТИВ», «в том числе:») — не строка. Счётчиков отброшенных нет — есть статусы.
|
||
|
||
- **1. Код-мусор** — статус `unreadable`, строка сохраняется с сырым текстом кода (AURORA-1462: «ИЧ,250» видна, аналитик назначает 1250)
|
||
- scan-gateway: счётчик → статус
|
||
- tabular-parser: отличает «нет кода» от «не разобран»
|
||
- **2. Склейка двух строк в одну** — одно правило: ячейка кода содержит N кодов через перевод строки и ячейки значений — N частей → N строк (OQ-156); у Excel не срабатывает
|
||
- scan-gateway: как сейчас, через общее правило
|
||
- tabular-parser: правило общее, no-op
|
||
- **3. Повтор кода** — статус `duplicate` у второй и последующих строк; строки сохраняются; текстовое предупреждение и `checks.duplicate_line_codes` уходят; что показывать и как считать — ядро (Р-4)
|
||
- scan-gateway: «оставлена первая» → обе строки
|
||
- tabular-parser: `duplicates` → статус
|
||
- **4. Отброшенная строка** — строки не отбрасываются: `unreadable` / `missing` / `duplicate` идут дальше со статусом (AURORA-1634, 1462, 1406); ядро не достраивает вместо них пустую шаблонную строку молча
|
||
- scan-gateway: счётчики → статусы
|
||
- tabular-parser: счётчики → статусы
|
||
|
||
### Шаг 4 — Значения ячеек
|
||
|
||
**Вход:** ячейки значений `RawLine` по колонкам раскладки (`Cell` или `null`).
|
||
|
||
**Единый выход:** `StatementValue {column_index, amount, string_value, confidence, is_recognized}` — как в контракте,
|
||
с одним смыслом полей у обоих.
|
||
|
||
- **1. Предобработка текста** — у ячейки после шага 1 либо есть `value`, либо только `text`. `value` есть → `amount = value` (2 знака), `is_recognized = true`. `value` нет → один `parse_amount(text)` для обоих: двойники, разделители тысяч, скобки → минус, прочерк → `null`; Excel-ячейки «число текстом» идут тем же путём
|
||
- scan-gateway: `parse_amount` переезжает в общий код
|
||
- tabular-parser: `to_money` заменяется общим `parse_amount`
|
||
- **2. Смысл `is_recognized`** — один: текст разобран как число без выброшенных и исправленных символов; `confidence` — отдельно, от источника; экран подсвечивает по любому из двух (как сейчас)
|
||
- scan-gateway: отвязывает от `confidence`
|
||
- tabular-parser: как сейчас
|
||
- **3. «42831 1»** — в `parse_amount` пробел допустим только как разделитель тысяч (группы ровно по три цифры); иначе `amount` — лучшая догадка (цифры подряд), `is_recognized = false`, `string_value` — сырой текст (AURORA-1511, 1519)
|
||
- scan-gateway: меняется через общий код
|
||
- tabular-parser: то же правило, у Excel почти не срабатывает
|
||
- **4. Единицы** — одно правило: единицы «тыс. руб.» и дробных ≥ 10 % → предупреждение документа
|
||
- scan-gateway: добавляет
|
||
- tabular-parser: как сейчас
|
||
- **5. Пустая ячейка** — одно правило: позиции нет, текст пуст или прочерк → `amount = null`, `string_value = null`, `is_recognized = true`, `confidence` ячейки (нет ячейки — 100). Потерянную OCR-ячейку ловят проверки §7 (Р-2)
|
||
- scan-gateway: «ячейки нет → 0, не распознано» уходит
|
||
- tabular-parser: как сейчас
|
||
|
||
### Шаг 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.
|
||
|
||
- **1. Источник наименования** — одно правило: наименование — из файла, с уверенностью источника; справочник наименование не подменяет. Справочное имя по коду и редакции добавляет ядро из своего (единственного) словаря, экран показывает справочное, файловое — рядом (Р-3; AURORA-1406, 1522)
|
||
- scan-gateway: имя из `statement_lines.yaml` → имя из OCR
|
||
- tabular-parser: как сейчас
|
||
- **2. Редакция (66н / ФСБУ 4)** — в сервисах не определяется; редакция — свойство словаря строк (по дате отчёта), используется в шаге 7 (проверка «код не из редакции формы») и в ядре при достройке шаблонных строк
|
||
- scan-gateway: `_choose_edition` уходит
|
||
- tabular-parser: без изменений
|
||
- **3. Справочник строк** — один словарь строк (в общем коде, Р-1); копия в шлюзе удаляется
|
||
- scan-gateway: удаляет `api/rules/statement_lines.yaml`
|
||
- tabular-parser: без изменений
|
||
- **4. Уверенность кода** — от источника (ИС / 100), без срезания до 99 по справочнику; «не в справочнике» — предупреждение к строке в шаге 7
|
||
- scan-gateway: убирает `min(conf, 99)`
|
||
- tabular-parser: как сейчас
|
||
|
||
### Шаг 6 — Сборка документа
|
||
|
||
**Вход:** `FormCandidate` (`type_hint`, `evidence`, `header`) + `TableLayout` + `StatementLine[]`.
|
||
|
||
**Единый выход:** `StatementDocument {document_type, locator, header, columns, lines}` без `checks`.
|
||
|
||
- **1. Тип формы** — одно правило: большинство кодов 1xxx → `BALANCE`, 2xxx → `INCOME`; совпало с `type_hint` или `type_hint` `null` — тип по кодам; противоречит — тип по кодам + предупреждение; кодов нет — `type_hint`; без колонки кодов: `type_hint` есть → документ без строк (`DISCLOSURE` — `done`, иначе `needs_check`), `type_hint` нет → не документ, предупреждение info
|
||
- scan-gateway: как сейчас, через общее правило
|
||
- tabular-parser: добавляет сверку с кодами
|
||
- **2. Склейка страниц** — решена в шаге 1, различия нет
|
||
- scan-gateway: без изменений
|
||
- tabular-parser: без изменений
|
||
- **3. `locator`** — одна грамматика «пары `ключ:значение` через `;`», набор ключей по источнику: `json:<имя>;table:<i>` / `sheet:<A>[,<B>]`
|
||
- scan-gateway: без изменений
|
||
- tabular-parser: без изменений
|
||
|
||
Шапка документа — поля `header` кандидата как есть (`text` → `*_text`, `value` → поле, `confidence` → `*_confidence`).
|
||
|
||
### Шаг 7 — Самопроверки
|
||
|
||
**Вход:** `StatementDocument`.
|
||
|
||
**Единый выход:** `StatementChecks` у обоих: `relations_failed[]`, `soft_relations_failed[]` (каждое нарушение — с
|
||
кодами слагаемых и `column_index`, чтобы экран подсветил ячейки), `line_warnings[]` (код не из редакции формы по дате
|
||
отчёта, код не в словаре). `duplicate_line_codes` уходит в `line_status`.
|
||
|
||
- **1. Проверки только у парсера; словарь §7 в двух копиях; ядро результат не читает** — проверки — общий код с одним словарём `statement_checks.yaml` у обоих сервисов; для OCR нарушение §7 — главный признак неверно прочитанной цифры; ядро читает `checks` из результата и пересчитывает тем же кодом только после правок аналитика (PATCH)
|
||
- scan-gateway: добавляет шаг
|
||
- tabular-parser: `run_statement_checks` → общий код
|
||
|
||
### Шаг 8 — Статус и предупреждения
|
||
|
||
**Вход:** `StatementDocument` + `StatementChecks` + предупреждения шагов 1–7.
|
||
|
||
**Единый выход:** у каждого документа `status` (`done` | `needs_check`) и `warnings[]`; у файла `metrics.warnings[]`;
|
||
статус конверта — агрегат. Предупреждение — структура `{code, severity: error | info, message, line_ref?,
|
||
column_index?}`, а не строка.
|
||
|
||
- **1. Критерий `done`** — один критерий из самого результата: раскладка найдена ∧ (организация или ИНН) ∧ дата отчёта ∧ все строки `ok` ∧ нет значений `needs_review` (см. «Подтверждение проверкой» ниже) ∧ жёсткие проверки §7 сошлись ∧ нет предупреждений уровня `error`. Excel выполняет условия уверенности автоматически
|
||
- scan-gateway: «ничего не выброшено и уверенность 100» → общий критерий
|
||
- tabular-parser: `_is_ok` → общий критерий; добавляются строки и значения
|
||
- **2. Влияние предупреждений** — по уровню: `error` → `needs_check`, `info` — не влияет; у обоих одинаково
|
||
- scan-gateway: «любое → `needs_check`» уходит
|
||
- tabular-parser: предупреждения начинают влиять
|
||
- **3. Статус документа** — в контракте: `document.status`; конверт `done` ⇔ все документы `done` ∧ нет предупреждений файла уровня `error`; иначе `needs_check`; документов нет — `error` (как сейчас)
|
||
- scan-gateway: добавляет
|
||
- tabular-parser: внутренний `ok` → в контракт
|
||
- **4. Формат предупреждений** — структура (см. выше) вместо строк `<json>: table:<i>: …` / `sheet=<имя> …`; место — в `locator` документа, а не в тексте
|
||
- scan-gateway: меняет
|
||
- tabular-parser: меняет
|
||
- **5. Привязка** — к документу, строке (`line_ref`) или ячейке (`line_ref` + `column_index`); экран показывает у места
|
||
- scan-gateway: меняет
|
||
- tabular-parser: меняет
|
||
|
||
**Подтверждение проверкой (вопрос владельца 17.10.2026).** Проверка §7 — арбитр, уверенность — подсказка там, где
|
||
проверки нет. По каждому значению сервис считает `needs_review`:
|
||
|
||
- `true`, если `is_recognized = false` или `confidence` ниже порога (Р-5) **и** значение не покрыто сошедшимся жёстким
|
||
соотношением §7 в своей колонке (не входит ни в одно, соотношение не сошлось или оно мягкое);
|
||
- `true` у всех ячеек соотношения в колонке, где оно не сошлось, независимо от уверенности;
|
||
- иначе `false`: значение подтверждено проверкой, не подсвечивается и статус не трогает; сырой текст и уверенность
|
||
остаются в подсказке ячейки.
|
||
|
||
Экран подсвечивает ячейки только по `needs_review`; после PATCH ядро пересчитывает `checks` и `needs_review` общим
|
||
кодом. Не покрыто проверками и остаётся на уверенности: поля шапки, справочные строки ОФР 25xx / 29xx, детализация
|
||
без проверки родителя, `DISCLOSURE`, колонки, где есть только мягкие соотношения. Риск — две взаимно компенсирующие
|
||
ошибки в одном соотношении; принимается (перекрёстные соотношения баланса его снижают). Закрывает AURORA-1524 без
|
||
ущерба для 1519. Распространение на `is_recognized = false` — Р-7.
|
||
|
||
### Шаг 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` показываются как
|
||
пришли, с подсветкой.
|
||
- Экран: подсветка ячеек только по `needs_review` (уверенность, `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`)
|
||
или пропускать, как парсер сейчас.
|
||
- **Р-7** (шаг 8, «Подтверждение проверкой») — снимать ли подсветку сошедшимся соотношением §7 и с `is_recognized =
|
||
false` (число собрано из грязного текста), а не только с низкой уверенности; предложение — да: сошедшаяся проверка
|
||
сильнее догадки о мусоре.
|