35 KiB
Дизайн: метрики качества первички (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 ИС лежит в S3raw/{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. Причина документа (ячейка матрицы)
Каждый документ с исходом попадает ровно в одну ячейку — по первому сработавшему правилу:
- исход
DELETED/SUPERSEDED→ «удалён / заменён до подтверждения»;DELETED_AFTER_CONFIRM/SUPERSEDED_AFTER_CONFIRM→ «удалён / заменён после подтверждения» (оба столбца в доли не входят); - есть правки «сервис» → ошибка сервиса;
- есть правки «наш разбор» → наш разбор;
- подтверждён с акцептом нарушений (§5) → документ клиента;
AUTO_CONFIRMED→ без участия;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 | ОСВ — без построчного снимка, только статусы, предупреждения, отказы, правки периода/счёта | правок сумм ОСВ в ядре нет | при появлении правок ОСВ — доработка снимка |
| 05.11.2026 | Task 1: api/scripts/dbml_parity.py (схема таблицы — через явный allowlist METRICS_TABLES, не единственная константа SCHEMA), api/scripts/check_dbml_vs_orm.py (фиксирует QUALITY_METRICS_SCHEMA=dkb_metrics в окружении проверки, как уже делает для DB_SCHEMA) и api/alembic/env.py (include_name признаёт обе схемы) — доработаны под вторую схему; план задачи 1 предполагал, что check_dbml_vs_orm.py «скорее всего» не потребует правок |
без этого make dbml-check безусловно падал бы на quality_intake_fact: схема: ожидалось dkb / фактически dkb_metrics» для всех трёх таблиц фактов; это ровно тот структурный сюрприз, о котором бриф просил не обходить тихо, а чинить или сообщать — чинить оказалось дешевле и находится в рамках задачи (make dbml-check` — приёмный критерий шага 7) |
без правки вторая схема логически невозможна — любой PR с dkb_metrics падает на CI/проверке |
| 05.11.2026 | Три новых ORM-класса (QualityIntakeFact/OutcomeFact/FailureFact) — на ОБЩЕМ Base/Base.metadata (как и всё остальное), схема переопределена точечно через __table_args__ = {"schema": METRICS_SCHEMA}; второй DeclarativeBase НЕ заводился |
ровно второй вариант, предложенный самим брифом («…или аналог существующего паттерна»); так Base.metadata остаётся единой точкой для check_dbml_vs_orm.py/alembic/env.py/create_all — отдельная metadata потребовала бы протащить её через все три места отдельно |
при введении третьей схемы в будущем придётся повторить тот же приём, а не скопировать общий механизм |
| 05.11.2026 | Три новых enum-типа (quality_outcome, quality_cause, quality_failure_kind) — значения живут в схеме SCHEMA (dkb), НЕ в METRICS_SCHEMA, хотя используют их только таблицы dkb_metrics |
pg_enum() (src/models/enums.py) без правок — единственный источник схемы enum'а; разводить enum'ы по двум схемам потребовало бы параметра схемы в фабрике без текущей пользы (Superset читает только таблицы, не типы) |
таблица dkb_metrics зависит от типов в dkb; перенос схемы метрик на другой сервер БД без dkb невозможен без правки (сейчас design §10 этого не требует) |
| 05.11.2026 | Откат миграции 0012 НЕ удаляет схему dkb_metrics (оставляет её пустой), симметрично тому, что SCHEMA (dkb) никогда не удаляется в env.py/0001 |
тест test_upgrade_into_preprovisioned_schema_without_database_create (api/tests/test_migrations.py) показал конкретный отказ: роль без CREATE на базу не могла бы создать dkb_metrics заново при повторном upgrade head, если бы платформа выдавала схему готовой, а откат её удалял; симметрия с dkb — тот же риск, то же решение |
если отладочный откат до base оставляет пустую схему dkb_metrics — незначительный мусор, не ошибка |
| 05.11.2026 | В план (docs/superpowers/plans/2026-11-05-quality-metrics-arm-core.md) добавлена строка **Ветка:** (feat/190-ocr-metrics, worktree, issue, Jira) — без неё хук require-plan.sh блокировал любую правку в api/src/**, так как не находил имя ветки ни в одном файле docs/superpowers/plans/*.md |
во всех остальных активных планах эта строка есть (сверено на 2026-10-28-corrections-table-ui.md); её отсутствие — пропуск на этапе написания плана, не содержательное решение |
минимальный — чисто метаданные плана |
| 05.11.2026 | Коммит задачи 1 включает больше файлов, чем перечислено в брифе (Makefile, README.md, api/.env.example, api/.env.test, api/alembic/env.py, api/scripts/check_dbml_vs_orm.py, api/scripts/dbml_parity.py, api/src/models/base.py, api/src/models/__init__.py, восемь файлов api/tests/test_*.py) |
все они требуют правки для согласованности версии v5.25 (имя DBML-файла в Makefile/README, HEAD-ревизия и допущение «всё в dkb» в тестах) или для поддержки второй схемы; список git add в брифе соответствовал более узкому ожидаемому дифу до того, как выяснилось, что check_dbml_vs_orm.py и тесты миграций не знают о второй схеме |
коммит по списку брифа оставил бы make test/make lint/make dbml-check красными — хуже, чем более широкий, но согласованный коммит |
| 05.11.2026 | api/contracts/quality-facts.schema.json НЕ скопирован из ../docs/contracts/ в Task 1 (test_contract.py::test_vendored_contracts_are_identical_to_docs остаётся красным на этом файле) — копирование отложено на задачу, которая реально строит payload по этой схеме |
файл уже есть в ../docs/contracts/ (сиблинг-репозиторий arm-ka-docs, работа по нему уже слита туда), а в api/contracts/ копии нет и несовпадение чисто механическое; копия сейчас была бы мёртвой — Task 1 (схема/ORM/миграция) не читает и не пишет payload, валидация полезной нагрузки — логика, не схема БД, и не входит в её рамки |
следующая задача может не заметить, что это отложено осознанно, и либо повторно принять то же решение не зная о нём, либо оставить контракт невендоренным навсегда |