diff --git a/openspec/changes/quality-metrics/.openspec.yaml b/openspec/changes/quality-metrics/.openspec.yaml new file mode 100644 index 0000000..97e3642 --- /dev/null +++ b/openspec/changes/quality-metrics/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-27 diff --git a/openspec/changes/quality-metrics/design.md b/openspec/changes/quality-metrics/design.md new file mode 100644 index 0000000..5aa154f --- /dev/null +++ b/openspec/changes/quality-metrics/design.md @@ -0,0 +1,224 @@ +# Дизайн: метрики качества первички (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-111), фиксируется в + `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 | ОСВ — без построчного снимка, только статусы, предупреждения, отказы, правки периода/счёта | правок сумм ОСВ в ядре нет | при появлении правок ОСВ — доработка снимка | diff --git a/openspec/changes/quality-metrics/proposal.md b/openspec/changes/quality-metrics/proposal.md new file mode 100644 index 0000000..e296000 --- /dev/null +++ b/openspec/changes/quality-metrics/proposal.md @@ -0,0 +1,50 @@ +# Метрики качества первички (AURORA-1759, arm-core #190) + +## Зачем + +Заказчику нужно подтверждать эффект инициативы АРМ КА цифрами, а не прикидками: какая доля ББ/ОФР/ОСВ доходит до +результата без ручного ввода, сколько правят, что ломается. Вторая цель — иметь доказательную базу о качестве сервиса +разбора PDF (ИС Сканирование): если цифры неудовлетворительны, это аргумент за смену сервиса, и он должен отделять +ошибки сервиса от ошибок нашей нормализации и от дефектов самого документа клиента. + +## Что меняется + +- Ядро записывает **неизменяемые факты качества** по каждому документу: на приёме результата («как распознано») и на + исходе (подтверждён / удалён / заменён), а также факты технических отказов заданий распознавания. Факты + самодостаточны (без FK на рабочие таблицы), переживают удаление и перераспознавание файла и лежат плоскими таблицами + в отдельной схеме БД — их читает Superset платформы и переносит отдельное решение без доработки ядра. +- Каждая правка аналитика относится к источнику ошибки сравнением подтверждённого значения с сырым текстом сервиса: + сервис разбора / наш разбор / документ клиента. +- Сырой ответ ИС Сканирование (`raw/{job_id}.json`) регистрируется в реестре S3 и хранится, пока на него ссылаются + факты, — для будущего эталонного прогона новой нормализации. +- Признак «скан / цифровой PDF» определяется при загрузке. +- Экран «Качество первички» в веб-интерфейсе: матрица «источник × причина» за период и динамика по месяцам, переход из + ячейки в список документов и в документ; выгрузка фактов. Отдельное право на просмотр. + +## Затронутые репозитории + +- **arm-ka-docs** (отдельный change): DBML v5.24 (таблицы фактов, роль бакета `raw`, признак скана, право), правила + подсчёта — новый раздел `05-domain-rules.md` §14, контракт фактов `docs/contracts/quality-facts.schema.json`. +- **arm-core** (этот change): запись фактов, атрибуция правок, расчёт метрик, API, экран, право в seed. +- scan-gateway и tabular-parser не меняются. + +Вне срезов `09-delivery-plan.md`: спринт 3, эпик AURORA-1353. + +## Затрагиваемые таблицы DBML v5.23 + +`recognition_job` (ключ сырого ответа), `s3_object` / `s3_bucket_role` (роль `raw`), `stored_file` (признак скана), +`document`, `financial_statement_meta|column|line|value`, `osv_entry`, `document_issue`, `audit_event` (читаются при +записи фактов), `permission_object` (новый объект). Новые таблицы фактов — в DBML v5.24 до кода. + +## Non-goals + +- Метрики по документам, загруженным до выкладки: считаем только новые. +- Время работы аналитика (визиты документа, активное / полное время) и пользовательские метрики интерфейса — в будущий + раунд; факты ключуются по `document_id`, чтобы визиты потом присоединились. +- Отдельный счётчик «документ есть, но сервис его не разбирает» (ОСВ в PDF, расшифровки) — не отделить от технических + отказов ББ/ОФР (решение владельца). +- Ограничение видимости метрик по филиалу / ГК — пока все видят всё. +- Сам эталонный прогон (перенормализация сохранённых сырых ответов) — этот change только сохраняет для него данные. +- Свой BI: Superset платформы читает факты напрямую. + +Подробности и решения — `design.md`.