# quality-metrics Specification ## Purpose Факты качества первички (приём «как распознано», исход, технический отказ), отнесение правок аналитика к источнику ошибки, матрица причин и API `/quality-metrics` с отдельным правом. Правила подсчёта — `05-domain-rules.md` §14, модель — DBML v5.25, дизайн — `design.md` этого change. ## Requirements ### Requirement: Факты качества — отдельная схема, только добавление Ядро SHALL хранить факты качества тремя плоскими таблицами `quality_intake_fact`, `quality_outcome_fact`, `quality_failure_fact` в отдельной схеме PostgreSQL (`QUALITY_METRICS_SCHEMA`, по умолчанию `dkb_metrics`; значение MUST отличаться от `DB_SCHEMA`). Таблицы MUST NOT иметь внешних ключей на рабочие таблицы: идентификаторы документов, файлов, заданий, ЮЛ и ГК — просто `uuid`, имена ЮЛ и ГК копируются на момент факта. Записи SHALL только добавляться (UPDATE и DELETE по фактам MUST NOT выполняться); `payload` — `jsonb` с `payload_schema_version`, форма по контракту `quality-facts.schema.json`. Факты пишутся только для документов, принятых после выкладки. #### Scenario: Факт переживает удаление файла - **WHEN** файл с документом, у которого есть факт приёма, удаляется - **THEN** строки документа и файла удалены, а `quality_intake_fact` остаётся нетронутым #### Scenario: Схема метрик совпала со схемой модели - **WHEN** `DB_SCHEMA` и `QUALITY_METRICS_SCHEMA` заданы одним значением - **THEN** приложение не стартует с ошибкой конфигурации ### Requirement: Факт приёма «как распознано» В транзакции маппинга результата, после `map_file` и до автоподтверждения, ядро SHALL записать по одному `quality_intake_fact` на каждый созданный документ `BALANCE`, `INCOME`, `OSV` (`DISCLOSURE` и `OTHER` факта не получают): измерения (ЮЛ и ГК с именами, тип документа, `source_kind` pdf/excel, `is_scan` для PDF, сервис и версии, `intake_at`, `intake_status`), счётчики строк, значений, нарушений и предупреждений, ключи S3, и `payload` со снимком «как распознано». Запись факта MUST NOT менять документы, статусы и результат маппинга. #### Scenario: Приём ББ - **WHEN** маппинг создал документ `BALANCE` из PDF - **THEN** есть ровно один `quality_intake_fact` с `document_id`, `source_kind = pdf`, `intake_status` как у документа после маппинга и снимком строк и значений в `payload` #### Scenario: Расшифровка не учитывается - **WHEN** маппинг создал документ `DISCLOSURE` - **THEN** факта приёма по нему нет ### Requirement: Факт исхода документа Ядро SHALL записать `quality_outcome_fact` в транзакции события, которое завершает документ: ручное подтверждение (`CONFIRMED`), автоподтверждение (`AUTO_CONFIRMED`), удаление файла (`DELETED`), автозамена файла (`SUPERSEDED`). Если документ на момент удаления или замены уже `CONFIRMED`, исход SHALL быть `DELETED_AFTER_CONFIRM` / `SUPERSEDED_AFTER_CONFIRM`. Факт содержит `primary_cause`, счётчики правок по источнику (`service` / `ours` / `client`), `checks_accepted`, число нарушений на момент исхода, подтверждённую отчётную дату. Повторное подтверждение ОСВ после правки SHALL писать новый факт; метрики берут последний факт документа. #### Scenario: Автоподтверждение - **WHEN** документ без замечаний автоподтверждён - **THEN** факт с `outcome = AUTO_CONFIRMED`, `primary_cause = WITHOUT_PARTICIPATION`, `confirmed_by` пуст #### Scenario: Удаление после подтверждения - **WHEN** удаляется файл с автоподтверждённым документом - **THEN** новый факт `DELETED_AFTER_CONFIRM`; прежний факт `AUTO_CONFIRMED` не изменён ### Requirement: Факт технического отказа Ядро SHALL записывать `quality_failure_fact` при переходе задания распознавания в `error`, `timeout`, `dlq` и при неповторяемой ошибке маппинга (файл → `ERROR`). `failure_kind` — `SERVICE_ERROR` / `TIMEOUT` / `NO_ACK` / `MAPPING`; `error_code` отказа маппинга — константа `MAPPING_FAILED`. Повторная обработка создаёт новое задание: отказ первой попытки остаётся фактом. #### Scenario: Таймаут задания - **WHEN** вахтёр переводит задание в `timeout` - **THEN** есть факт `failure_kind = TIMEOUT` с `job_id`, `file_id`, `failed_at` ### Requirement: Отнесение правки к источнику ошибки Правка аналитика (отличие подтверждённого результата от снимка приёма) SHALL относиться к источнику чистой функцией без БД по правилу «верный ответ есть в сыром тексте сервиса — ошибка наша, иначе сервиса»: сумма, код строки, добавленная строка, поля шапки (отчётная дата сравнивается как дата после разбора `parse_date`), период и счёт ОСВ. Для Excel любая ошибка — «наш разбор». Спорные случаи и случаи без сырого текста SHALL относиться к «нашему разбору». Правки колонок дат и набор кодов сырого ответа для добавленных строк PDF в этом change не учитываются (отложено). #### Scenario: Сервис прочитал верно - **WHEN** аналитик исправил сумму, цифры которой совпадают с цифрами сырого текста - **THEN** правка относится к «нашему разбору» #### Scenario: Сервис ошибся - **WHEN** подтверждённый код строки не извлекается из сырого текста кода - **THEN** правка относится к «ошибке сервиса» ### Requirement: Причина документа и матрица Каждый документ с исходом SHALL попадать ровно в одну ячейку по первому сработавшему правилу: удалён/заменён до подтверждения; удалён/заменён после подтверждения; есть правки «сервис» → ошибка сервиса; есть правки «наш разбор» → наш разбор; ручное подтверждение с акцептом жёстких нарушений → документ клиента; `AUTO_CONFIRMED` → без участия; иначе → как есть. Документы без исхода — «в работе». Доли считаются от документов с исходом `AUTO_CONFIRMED` / `CONFIRMED`; удалённые, заменённые и «в работе» в знаменатель не входят. Технические отказы считаются по файлам своей долей. Расчёт — чистая функция от фактов, в БД не хранится; последний факт документа выбирается по времени исхода, при равенстве — детерминированно. #### Scenario: Документ с правкой сервиса и клиента - **WHEN** у подтверждённого документа есть правка «сервис» и акцепт нарушений - **THEN** документ в ячейке «ошибка сервиса», счётчики обеих сторон сохранены в факте #### Scenario: Нулевой знаменатель - **WHEN** за период нет документов с исходом - **THEN** доли равны 0, `denominator = 0` ### Requirement: API метрик качества Система SHALL предоставлять под правом `QUALITY_METRICS_VIEW` маршруты `/api/v1/quality-metrics`: `GET /matrix` (матрица за период с фильтрами период по дате приёма, ГК, ЮЛ, тип, источник, скан/цифровой, отказы по файлам, счётчик уникальных отчётов), `GET /series` (ряд по месяцам даты приёма для причины, пропущенные месяцы — нули), `GET /cells/{cell}/documents` (постраничный список документов ячейки со счётчиками правок, ссылкой на документ, если он существует, и `confirmed_by`), `GET /export` (факты за период, CSV с BOM и JSON; значения, начинающиеся с символов формулы, экранируются). Выгрузка SHALL писать `audit_event(EXPORT)` с `object_type = quality_metrics` и фильтрами. Агрегаты MUST NOT содержать фамилий пользователей. #### Scenario: Матрица без права - **WHEN** пользователь без `QUALITY_METRICS_VIEW` запрашивает `/quality-metrics/matrix` - **THEN** 403 #### Scenario: Выгрузка оставляет след - **WHEN** пользователь выгружает факты за период - **THEN** записан `audit_event(EXPORT)` с форматом, фильтрами и числом строк ### Requirement: Право QUALITY_METRICS_VIEW Объект `QUALITY_METRICS` в `permission_object` и право `QUALITY_METRICS_VIEW` SHALL быть в seed и выдаваться всем ролям; позже права сужаются данными (`role_permission`) без изменения кода. Видимость по филиалу и ГК не применяется. #### Scenario: Право у всех ролей - **WHEN** выполнен `make seed` на чистой БД - **THEN** `GET /me` любой роли содержит `QUALITY_METRICS_VIEW`