Andrey Epifancev 1171e173af docs(#190): исправить номер открытого вопроса OQ-111 → OQ-179
OQ-111 уже занят (ретеншн S3 общего назначения) — реальный свободный номер для
срока хранения raw/{job_id}.json нашёлся при работе над arm-ka-docs#202.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-11-05 14:13:23 +04:00

225 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Дизайн: метрики качества первички (AURORA-1759)
## 1. Зачем и для кого
**Заказчик** (пользователи АРМ в банке) сам открывает экран в веб-интерфейсе и видит, какую работу снял АРМ: доля
документов, прошедших без участия аналитика или подтверждённых без правок, доля правленых, технические отказы — за
период, по ГК, по типу документа и по источнику. Цифры подтверждают эффект инициативы.
**Владелец проекта** использует те же цифры как аргумент о качестве сервиса разбора PDF (ИС Сканирование): если они
плохие — довод за переход на другой сервис. Поэтому каждая ошибка относится к источнику: **сервис разбора**, **наш
разбор** (нормализация `arm_statements`, маппинг ядра, tabular-parser) или **документ клиента** (контрольные цифры не
сходятся в оригинале). Без такого разделения цифра «30 % правят» против сервиса не работает.
Позже метрики переедут в отдельное решение на платформе банка (там точно есть Superset, хранилище неизвестно) — перенос
не должен требовать доработки ядра.
## 2. Цели, не-цели, анти-цели
**Цели**
- Факты качества по каждому документу ББ / ОФР / ОСВ, загруженному после выкладки, и по каждому техническому отказу.
- Отнесение каждой правки аналитика к источнику ошибки по сырому тексту сервиса.
- Экран: матрица «источник × причина» за период + динамика по месяцам; переход из ячейки в список документов и далее
в документ (правки, комментарии, сам файл).
- Факты пригодны для Superset как есть и для переноса в другое хранилище.
- Сохранить вход для будущего эталонного прогона: сырые ответы ИС и исходные xlsx, подтверждённый результат.
**Не-цели** — см. `proposal.md`, раздел Non-goals (в т. ч. ретро-метрики, время аналитика, видимость по филиалам, сам
эталонный прогон).
**Анти-цели** (провал, даже если работает)
- Цифры, которые нельзя проверить: каждая ячейка матрицы раскрывается до конкретных документов.
- Вина сервиса там, где ошиблись мы (или наоборот): правило атрибуции — механическое и записанное (§5), спорные случаи
уходят в «наш разбор», а не в «сервис».
- Лишняя работа аналитика: никаких отметок, анкет и кнопок ради метрик.
- Метрики, которые меняются задним числом: факт записан — не переписывается (append-only), поздние события — новые
факты.
- Связка с рабочими таблицами, из-за которой перенос на платформу требует переписать ядро.
## 3. Ограничения
- Инварианты `../CLAUDE.md`: первичка не мутируется; единственный писатель в БД — arm-core; производные не хранятся.
Факты — не производные консолидации, а журнал событий (как `audit_event` / `ex_snapshot`); метрики — чистая функция
от фактов, не хранятся.
- Поля — только из DBML: таблицы фактов, роль бакета, признак скана, объект права появляются в DBML v5.24 (change в
`../docs`) до кода.
- Нормализацию ББ/ОФР делают сервисы; в ядро приходит нормализованный `result.json`. «Сырой текст сервиса» в ядре — это
`financial_statement_value.source_text`, `financial_statement_line.line_code_text`,
`financial_statement_meta.header_source`, `osv_entry.row_value` (§11.5: `text` — всегда сырой текст источника).
Сырой JSON ИС лежит в S3 `raw/{job_id}.json`, в БД не зарегистрирован.
- Пересчёт после правки перезаписывает `needs_review`, `line_status`, статус документа и `document_issue`
(`relation`/`soft`) — поэтому состояние «как распознано» фиксируется фактом в момент приёма, до любых правок.
- Удаление файла физически удаляет строки документа (`primary_files.delete`) — факты не ссылаются на них FK.
- Сейчас у заказчика все работают под одной учётной записью (до РАИСА/SSO, AURORA-1404).
## 4. Факты
Отдельная схема PostgreSQL (имя — настройка, по умолчанию `dkb_metrics`), миграции — общей цепочкой Alembic. Таблицы
плоские: измерения — обычные столбцы, подробности — `jsonb` с версией схемы (`schema_version`). Без FK на рабочие
таблицы; идентификаторы документов, файлов, заданий, ЮЛ, ГК — просто `uuid`, имена ЮЛ и ГК копируются на момент факта.
Контракт столбцов и `payload` — `../docs/contracts/quality-facts.schema.json`. Записи только добавляются.
### 4.1 `quality_intake_fact` — документ «как распознано»
Пишется в транзакции маппинга (`result_mapping._map_candidate`, после `map_file`, **до** `auto_confirm`), по одному на
созданный документ ББ / ОФР / ОСВ (`DISCLOSURE` / `OTHER` — не пишутся).
Измерения: `document_id`, `file_id`, `job_id`, `attempt`, `legal_entity_id` + имя, `group_id` + имя (ГК ЮЛ на момент
факта; ЮЛ вне ГК — пусто), `document_type`, `source_kind` (`pdf` / `excel`), `is_scan` (только PDF), `service_name`,
`service_version`, `statements_version` (ББ/ОФР), `result_schema_version`, `report_date` (как распознано),
`intake_at`, `intake_status` (статус документа после маппинга: `RECOGNIZED` / `NEEDS_CHECK`), счётчики: строк,
значений, значений с `needs_review`, строк `unreadable` / `missing`, нарушений `relation` / `soft`, предупреждений
сервиса; `raw_s3_key` (PDF), `result_s3_key`, `upload_s3_key`.
`payload`: снимок «как распознано» — шапка (`header_source`, распознанные поля), строки (`ref`, `row_index`,
`line_code`, `line_code_text`, `line_status`, `needs_review`), значения (`line ref`, колонка, `amount`, `string_value`,
`source_text`, `confidence`, `is_recognized`, `needs_review`), нарушения и коды предупреждений. Для ОСВ — счётчики
строк и коды предупреждений `OSV_*`, без построчного снимка (правок ОСВ по суммам в ядре нет).
### 4.2 `quality_outcome_fact` — исход документа
Один на документ, пишется при первом из событий:
- **подтверждение** — `documents.confirm` (вручную) и `auto_confirm` (автоматически), в той же транзакции;
- **удаление до подтверждения** — `primary_files.delete`;
- **замена** — автозамена `FILE_REPLACE` (документ вытеснен новым файлом).
Измерения: `document_id` (связь с 4.1), `outcome` (`AUTO_CONFIRMED` / `CONFIRMED` / `DELETED` / `SUPERSEDED`),
`outcome_at`, `primary_cause` (§6), счётчики правок по источнику (§5): значения, коды, добавленные строки, шапка —
`service` / `ours` / `client`; `checks_accepted` (подтверждено с акцептом нарушенных проверок), число нарушений на
момент исхода, `report_date` подтверждённый, `confirmed_by` (пользователь; для агрегатов не используется).
`payload`: подтверждённый результат (то же строение, что снимок приёма) и список правок с отнесением — каждая правка:
что, было (сырой текст / распознано), стало, источник, правило §5, которое сработало.
Подтверждённую ОСВ можно править (правка сбрасывает подтверждение) — повторное подтверждение пишет **новый** факт
исхода; метрики берут последний.
Удаление файла с уже подтверждённым документом (автоподтверждённые при версии «в работе», OQ-157) и автозамена
подтверждённого документа пишут **новый** факт `DELETED_AFTER_CONFIRM` / `SUPERSEDED_AFTER_CONFIRM` с исходом, который
был до него. Метрики берут последний факт: такой документ уходит из своей ячейки в отдельный столбец «удалён / заменён
после подтверждения» (решение владельца 27.10.2026) — в доли не входит, но виден, и из него раскрывается список с
прежним исходом.
### 4.3 `quality_failure_fact` — технический отказ
Пишется при переходе задания распознавания в `error` / `timeout` / `dlq` (`recognition_exchange`) и при неповторяемой
ошибке маппинга (`MappingFatal`, файл → `ERROR`). Измерения: `job_id`, `file_id`, `attempt`, `legal_entity_id`,
`group_id`, `source_kind`, `is_scan`, `service_name`, `failure_kind` (`SERVICE_ERROR` / `TIMEOUT` / `NO_ACK` /
`MAPPING`), `error_code`, `failed_at`. Повторная обработка (`FILE_REPROCESS`) создаёт новое задание: отказ первой
попытки остаётся фактом, документы удачной попытки получают свои 4.1 / 4.2.
## 5. Отнесение правки к источнику
Правка — отличие подтверждённого результата от снимка приёма (4.1). Правило одно: **верный ответ есть в сыром тексте
сервиса — ошибка наша; нет — ошибка сервиса.** Для Excel сервис разбора — наш tabular-parser, поэтому у Excel любая
ошибка распознавания — «наш разбор».
| Что правили | «Наш разбор», если | Иначе |
|---|---|---|
| Сумма | цифры подтверждённой суммы (без знака) совпадают с цифрами сырого текста после замены букв-двойников §11.3 — сервис прочитал верно, а мы неверно разобрали (знак, скобки, разряды, мусор по краям) | сервис |
| Код строки | подтверждённый код есть среди кодов, извлекаемых из `line_code_text` (в т. ч. одна из частей склейки `1250\n1260`) | сервис |
| Добавленная строка (`row_index IS NULL`) | её код есть в наборе кодов сырого ответа (для PDF — четырёхзначные токены, стоящие отдельной строкой в тексте ячеек `raw/{job_id}.json`; набор извлекается при приёме, вне транзакции, рядом с чтением `result.json`, и кладётся в снимок 4.1; для Excel — всегда «наш разбор») | сервис |
| Шапка: период, ИНН, единицы, наименование | подтверждённое значение извлекается из `header_source[*].text` | сервис |
| Дата колонки | подтверждённая дата извлекается из подписи колонки (`label`) | сервис |
| Период / счёт ОСВ | всегда «наш разбор» (Excel) | — |
**Документ клиента** — не правка, а исход: подтверждён **вручную** с акцептом нарушенных **жёстких** проверок
(`checks_accepted`), и нарушенное соотношение не задето правками. Мягкие нарушения (в т. ч. «в пределах округления» и
межформенная 1370↔2400) не учитываются — это в основном шум округления, и подтверждение они не блокируют.
Автоподтверждение жёстких нарушений иметь не может. Если правки чинят часть нарушений, а остальные акцептованы — у
документа и правки, и «документ клиента».
Сравнение — чистая функция `domain/quality_attribution.py` (§14 `05-domain-rules.md`), без БД. Разбор сырого текста —
публичным API `arm_statements` (`fix_lookalikes`, `parse_amount`, `parse_code`, `parse_date`, `default_rules`), тем же
кодом, что разбирал сервис; сам пакет не меняется (и не зависит от того, слита ли AURORA-1677: имя модуля то же).
## 6. Причина документа (ячейка матрицы)
Каждый документ с исходом попадает ровно в одну ячейку — по первому сработавшему правилу:
1. исход `DELETED` / `SUPERSEDED` → «удалён / заменён до подтверждения»; `DELETED_AFTER_CONFIRM` /
`SUPERSEDED_AFTER_CONFIRM` → «удалён / заменён после подтверждения» (оба столбца в доли не входят);
2. есть правки «сервис» → **ошибка сервиса**;
3. есть правки «наш разбор» → **наш разбор**;
4. подтверждён с акцептом нарушений (§5) → **документ клиента**;
5. `AUTO_CONFIRMED` → **без участия**;
6. `CONFIRMED` без правок → **как есть** (распознано верно, но потребовалась проверка аналитика).
Документы без исхода — **в работе** (отдельный столбец, в доли не входит). Технические отказы — отдельный столбец из
4.3, считаются по заданиям. Все счётчики правок по источнику остаются в факте, поэтому глубже матрицы видно и
«сервис + наш разбор» в одном документе.
Знаменатель долей — документы с исходом `AUTO_CONFIRMED` / `CONFIRMED`. Технические отказы считаются **по файлам**
(документов у них нет, тип неизвестен): отдельный столбец со своей долей «отказавших файлов от загруженных файлов того
же источника»; фильтр по типу документа на него не действует (на экране — пометка). Рядом с матрицей — счётчик
«уникальных отчётов» (ЮЛ + подтверждённая отчётная дата + тип), чтобы было видно, сколько повторных загрузок; это
счётчик, а не режим матрицы.
## 7. Расчёт, API, экран
- **Расчёт** — чистая функция `domain/quality_metrics.py` от фактов: фильтры (период по дате приёма, ГК, ЮЛ, тип,
источник, скан/цифровой), матрица и ряд по месяцам. В БД не пишется.
- **API** (`/api/v1/quality-metrics`, право §8): матрица за период; ряд по месяцам по выбранной причине; список
документов ячейки (документ, ЮЛ, дата, причина, счётчики правок, ссылка на документ — если он ещё существует);
выгрузка фактов за период (CSV / JSON) для переноса.
- **Экран** «Качество первички» (`web/`, пункт меню по праву): фильтры, матрица «источник × причина», динамика по
месяцам, переход ячейка → список документов → документ (существующий экран проверки распознавания с правками и
комментариями). Точная раскладка — при реализации (владелец: «главное наличие данных»).
## 8. Доступ
Новый объект права `QUALITY_METRICS` в `permission_object`, право `QUALITY_METRICS_VIEW`; seed выдаёт его всем ролям.
Позже права сужаются данными (`role_permission`), без кода. Видимость по филиалу / ГК не применяется. Список
документов показывает `confirmed_by`, агрегаты — без фамилий.
Superset читает схему фактов отдельной ролью БД только на чтение (создаёт платформа; в репозитории — описание в
`03-architecture.md`).
## 9. Сырые ответы и признак скана
- **Сырой ответ ИС.** Ядро уже получает `raw_s3_key` в сообщении результата — регистрирует объект в `s3_object` с новой
ролью бакета `raw` и ссылкой из `recognition_job`. Удаление файла не ставит `expires_at` сырому ответу и `uploads/`,
на которые ссылаются факты: они нужны эталону. Срок хранения — вопрос платформы (OQ-179), фиксируется в
`07-open-questions.md`.
- **Скан / цифровой PDF** определяется при загрузке по наличию текстового слоя (шрифтов в PDF) и хранится в
`stored_file`. PDF со скрытым OCR-слоем считается цифровым — допущение, записывается в §14.
## 10. Что решено владельцем (27.10.2026)
- Метрики — для заказчика (эффект инициативы) и как аргумент о сервисе разбора PDF.
- Охват — PDF и Excel; разрезы — качество сервиса, нашего разбора, документа клиента, технические отказы.
- Атрибуция по сырому тексту сервиса + сохранить данные для эталонного прогона («оставляем»).
- Экран в веб-интерфейсе, заказчик открывает сам; матрица и динамика; раскладка — при реализации.
- Только данные после выкладки.
- Без счётчика «есть, но не разбирается».
- Время аналитика — собирать в будущем (визиты с привязкой к документу, без нагрузки на аналитика; фамилии — для
разбора, агрегаты — без них); в этом раунде — только технические метрики.
- Отдельное право; сейчас видят все.
- Документ, удалённый или заменённый после подтверждения, — в отдельный столбец «удалён / заменён после
подтверждения» (не остаётся в «без участия» и не считается ошибкой).
- Перенос на платформу без доработки ядра; там есть Superset.
## 11. Оставлено исполнителю
Имена таблиц и столбцов (в рамках DBML v5.24), формат `payload`, раскладка экрана, способ определения текстового слоя
PDF, точный вид выгрузки, разбиение на задачи.
## Decisions made without discussion
| Дата | Решение | Обоснование | Цена ошибки |
|---|---|---|---|
| 27.10.2026 | Сам эталонный прогон — не в этом change; здесь только сохранение входа (сырые ответы, xlsx, подтверждённый результат) | прогон требует адаптера шага 1 сервисов (он в scan-gateway / tabular-parser) и отдельного инструмента сравнения; без данных он невозможен, а данные копятся только с выкладки | прогон приедет позже; данные для него уже будут |
| 27.10.2026 | Правки шапки (период, ИНН, единицы, наименование) — ошибка распознавания, атрибуция по `header_source` | шапку тоже распознаёт сервис | неверная классификация части правок |
| 27.10.2026 | Документы без исхода — столбец «в работе», в доли не входят | неподтверждённый документ ещё не оценён | — |
| 27.10.2026 | Повторная обработка: отказ первой попытки — технический отказ; документ — по итоговой попытке | отказ был, но документ в итоге получен | двойной учёт файла в отказах и в исходах — виден по `file_id` |
| 27.10.2026 | Одна ячейка на документ по приоритету §6 (сервис > наш разбор > документ клиента > без участия > как есть) | матрица должна суммироваться в 100 %; приоритет «сервис» — потому что правку сервиса аналитик делает руками всегда | документ с ошибками обеих сторон виден только в «сервисе»; счётчики в факте сохраняют обе |
| 27.10.2026 | Спорное — в «наш разбор» (верный ответ есть в сыром тексте, напр. склейка `1250\n1260`) | анти-цель: не приписывать сервису наши недоработки | метрика сервиса выглядит лучше, чем есть |
| 27.10.2026 | Отдельная схема БД `dkb_metrics` под факты, плоские таблицы + `jsonb` | Superset подключается к схеме ролью только на чтение; перенос — одна схема с контрактом | лишняя сложность миграций со второй схемой |
| 27.10.2026 | Признак скана — по текстовому слою PDF в ядре при загрузке, без изменения scan-gateway | ИС такой признак не отдаёт; ядро видит файл при загрузке | PDF со скрытым OCR-слоем — «цифровой» |
| 27.10.2026 | Период метрик — по дате приёма результата | исход приходит позже, документ остаётся в своей когорте | недавние периоды долго «в работе» |
| 27.10.2026 | Право `QUALITY_METRICS_VIEW` всем ролям в seed | владелец: сейчас видят все, потом — отдельная роль | — |
| 27.10.2026 | «Документ клиента» — только ручное подтверждение с акцептом жёстких нарушений; мягкие и 1370↔2400 не учитываются (builder check) | мягкие — шум округления и не блокируют подтверждение; иначе автоподтверждённые уйдут из «без участия» | реальные дефекты отчётности в пределах мягких соотношений не видны |
| 27.10.2026 | Код добавленной строки ищется среди четырёхзначных токенов сырого ответа ИС, извлечённых при приёме, без переноса адаптера шага 1 в ядро (builder check) | адаптер живёт в scan-gateway; совпадение с суммой даёт «наш разбор» — по правилу «спорное — нам» | метрика сервиса чуть лучше реальной на добавленных строках |
| 27.10.2026 | Технические отказы — по файлам, своей долей; фильтр по типу документа на них не действует; «уникальные отчёты» — счётчик, не режим (builder check) | у отказавшего файла нет документов и типа; смешивать файлы и документы в одном знаменателе нельзя | доля отказов не складывается с долями матрицы в 100 % |
| 27.10.2026 | ОСВ — без построчного снимка, только статусы, предупреждения, отказы, правки периода/счёта | правок сумм ОСВ в ядре нет | при появлении правок ОСВ — доработка снимка |