# Дизайн: модель данных для метрик качества первички (DBML v5.25) Зачем, для кого и все решения владельца (27.10.2026, подтверждено 05.11.2026) — в `../../arm-core/openspec/changes/quality-metrics/design.md` (разделы 1–3, 10). Здесь — только то, что входит в DBML v5.25 и производные документы этого репозитория: точные имена, типы, перечисления. ## 1. Новые и изменённые объекты модели ### 1.1 Enum `s3_bucket_role` — добавить значение `raw` — сырой ответ ИС Сканирование, `raw/{job_id}.json`. Не получает `expires_at` при удалении файла/`uploads/`, на которые ссылаются факты (вход для эталонного прогона). ### 1.2 Enum `permission_object` — добавить значение `QUALITY_METRICS` — объект права экрана «Качество первички» (действие `VIEW`; seed права `QUALITY_METRICS_VIEW` — в arm-core, не в DBML). ### 1.3 Новые enum для фактов ``` Enum quality_pipeline_is_scan — не нужен: используется существующий recognition_pipeline (tabular=Excel, image=PDF) и новый boolean stored_file.is_scan (NULL для не-PDF). Enum quality_outcome { AUTO_CONFIRMED CONFIRMED DELETED SUPERSEDED DELETED_AFTER_CONFIRM SUPERSEDED_AFTER_CONFIRM } Enum quality_cause { SERVICE_ERROR [note: 'есть правка, не найденная в сыром тексте сервиса'] OUR_ERROR [note: 'есть правка, найденная в сыром тексте сервиса (наш разбор ошибся)'] CLIENT_DOCUMENT [note: 'подтверждён с акцептом нарушенных жёстких проверок, без несписанных правок на это нарушение'] WITHOUT_PARTICIPATION [note: 'AUTO_CONFIRMED'] AS_IS [note: 'CONFIRMED без правок и без акцепта'] } // NULL у исходов DELETED* / SUPERSEDED* (не входят в матрицу) Enum quality_failure_kind { SERVICE_ERROR [note: 'recognition_job.status = error'] TIMEOUT NO_ACK [note: 'dlq — redelivery после max_deliver'] MAPPING [note: 'MappingFatal, файл → ERROR'] } ``` Приоритет значений `quality_cause` при вычислении (несколько правил могут совпасть) — пункт §14.3 `05-domain-rules.md`: SERVICE_ERROR > OUR_ERROR > CLIENT_DOCUMENT > WITHOUT_PARTICIPATION > AS_IS. ### 1.4 Таблица `quality_intake_fact` — документ «как распознано» Отдельная схема `dkb_metrics` (имя — настройка). Без FK на рабочие таблицы. | Столбец | Тип | Прим. | |---|---|---| | id | uuid [pk] | | | document_id | uuid [not null] | без FK; один факт на документ | | file_id | uuid [not null] | | | job_id | uuid [not null] | | | attempt | smallint [not null] | | | legal_entity_id | uuid [not null] | | | legal_entity_name | varchar [not null] | копия имени на момент факта | | group_id | uuid | NULL — ЮЛ вне ГК | | group_name | varchar | | | document_type | document_type [not null] | BALANCE / INCOME / OSV (DISCLOSURE/OTHER не пишут факт) | | pipeline | recognition_pipeline [not null] | tabular = Excel, image = PDF | | is_scan | boolean | только pipeline = image | | service_name | varchar | | | service_version | varchar | | | statements_version | varchar | | | result_schema_version | varchar | | | report_date | date | как распознано | | intake_at | timestamptz [not null] | | | intake_status | document_status [not null] | RECOGNIZED / NEEDS_CHECK | | line_count | int | | | value_count | int | | | value_needs_review_count | int | | | line_unreadable_count | int | | | line_missing_count | int | | | issue_relation_count | int | | | issue_soft_count | int | | | warning_count | int | | | raw_s3_key | varchar | PDF; NULL для Excel | | result_s3_key | varchar | | | upload_s3_key | varchar | | | payload | jsonb [not null] | снимок — §2 | | payload_schema_version | varchar | схема как в ex_snapshot_item.payload_schema_version; формат «1», «1.1» | Индексы: `(document_id)` unique; `(legal_entity_id, intake_at)`; `(group_id, intake_at)`. ### 1.5 Таблица `quality_outcome_fact` — исход документа | Столбец | Тип | Прим. | |---|---|---| | id | uuid [pk] | | | document_id | uuid [not null] | связь с 1.4 по значению, без FK | | outcome | quality_outcome [not null] | | | outcome_at | timestamptz [not null] | | | primary_cause | quality_cause | NULL для DELETED*/SUPERSEDED* | | fixes_value_service | int [not null, default: 0] | | | fixes_value_ours | int [not null, default: 0] | | | fixes_code_service | int [not null, default: 0] | | | fixes_code_ours | int [not null, default: 0] | | | fixes_added_row_service | int [not null, default: 0] | | | fixes_added_row_ours | int [not null, default: 0] | | | fixes_header_service | int [not null, default: 0] | | | fixes_header_ours | int [not null, default: 0] | | | fixes_osv_period_account | int [not null, default: 0] | всегда «наш разбор» (Excel) | | checks_accepted | boolean [not null, default: false] | подтверждён с акцептом жёстких нарушений | | issue_count_at_outcome | int [not null, default: 0] | | | report_date | date | подтверждённая | | confirmed_by | uuid | для агрегатов не используется | | payload | jsonb [not null] | подтверждённый результат + список правок — §2 | | payload_schema_version | varchar | схема как в ex_snapshot_item.payload_schema_version; формат «1», «1.1» | Индексы: `(document_id, outcome_at)`; `(outcome, outcome_at)`. **Не unique** на `document_id` — повторное подтверждение ОСВ и исходы after-confirm пишут новый факт; метрики берут последний по `outcome_at`. ### 1.6 Таблица `quality_failure_fact` — технический отказ | Столбец | Тип | Прим. | |---|---|---| | id | uuid [pk] | | | job_id | uuid [not null] | | | file_id | uuid [not null] | | | attempt | smallint [not null] | | | legal_entity_id | uuid [not null] | | | legal_entity_name | varchar [not null] | | | group_id | uuid | | | group_name | varchar | | | pipeline | recognition_pipeline [not null] | | | is_scan | boolean | | | service_name | varchar | | | failure_kind | quality_failure_kind [not null] | | | error_code | varchar | | | failed_at | timestamptz [not null] | | Нет `payload`/`payload_schema_version` — все поля плоские, в отличие от intake/outcome (§2). Индексы: `(job_id)` unique; `(file_id, attempt)`. ### 1.7 Изменения существующих таблиц - `stored_file.is_scan boolean` — NULL для не-PDF; PDF — определяется при загрузке по текстовому слою (ASSUMPTION §14: скрытый OCR-слой считается цифровым). - `recognition_job.raw_s3_object_id uuid [ref: > s3_object.id]` — заменяет прежнюю заметку «сырой ответ в БД не регистрируется» (v5.13); заполняется при приёме результата image pipeline. ## 2. Контракт `docs/contracts/metrics/quality-facts.schema.json` JSON Schema (Draft 2020-12), три подсхемы с `$id` `intake-payload`, `outcome-payload`, `failure-fact` (у failure payload нет — все поля плоские). Описывает только `payload` интейка и исхода: - `intake-payload`: `header` (`header_source`, распознанные поля шапки), `lines[]` (`ref`, `row_index`, `line_code`, `line_code_text`, `line_status`, `needs_review`), `values[]` (`line_ref`, `column`, `amount`, `string_value`, `source_text`, `confidence`, `is_recognized`, `needs_review`), `issues[]` (код, уровень), `raw_line_codes[]` (только PDF — четырёхзначные токены сырого ответа, см. §5 `arm-core/.../design.md`); для OSV — `osv_warnings[]` и счётчики строк вместо `lines`/`values`. - `outcome-payload`: тот же `header`/`lines`/`values`, что и intake (подтверждённое состояние), плюс `fixes[]` (`field`, `before_raw`, `before_recognized`, `after`, `source`, `rule`). **Валидация.** `scripts/validate_contracts.py` не расширяется этим change: он жёстко завязан на три сообщения NATS-контракта (`request-`/`result-message-`/`result-` префиксы примеров). Payload фактов не ходит по NATS и не исполняет формат идемпотентности — расширение скрипта под него добавило бы ветвление без необходимости. Схема проверяется отдельным тестом в `arm-core` (где и формируется payload), а не `make check` здесь. Решение записано, так как расходится с общим правилом «каждый контракт в `contracts/` валидируется этим скриптом». ## 3. §14 `05-domain-rules.md` — состав раздела 1. Какие документы получают факты (BALANCE/INCOME/OSV, не DISCLOSURE/OTHER). 2. Правило атрибуции правки «сервис / наш разбор» (таблица из `arm-core/.../design.md` §5) — переносится как есть, источник правды один (здесь — табличная форма для ссылок из кода). 3. Правило «документ клиента» (ручное подтверждение с акцептом жёстких нарушений, мягкие и 1370↔2400 не считаются). 4. Приоритет `quality_cause` (§1.3) и знаменатель долей матрицы. 5. Признак скана — определение по текстовому слою PDF, допущение о скрытом OCR-слое. 6. Технические отказы считаются по файлам, не по документам; `FILE_REPROCESS` — первая попытка остаётся отказом. ## Decisions made without discussion | Дата | Решение | Обоснование | Цена ошибки | |---|---|---|---| | 05.11.2026 | Имена таблиц/столбцов/enum — как в разделе 1 (DBML v5.25) | исполнитель решает в рамках design arm-core §11 («оставлено исполнителю») | переименование потребует новой мажорной версии DBML | | 05.11.2026 | `quality_cause` — отдельный enum-столбец (денормализация §6 вместо вычисления на лету при каждом запросе матрицы) | факт append-only, пересчёт не нужен; упрощает API метрик (`GROUP BY primary_cause`) | расхождение между сохранённым `primary_cause` и правилом §14, если правило изменится — старые факты не пересчитываются (ожидаемо для append-only) | | 05.11.2026 | Атрибуция правок — 2 источника (service/ours), не 3: «документ клиента» — свойство исхода, не правки (см. §5 исходного design: таблица даёт только «сервис» или «наш разбор») | в design arm-core таблица §5 бинарна; «документ клиента» в §6 — отдельное правило по checks_accepted | лишний enum-источник правки, если окажется нужен позже | | 05.11.2026 | `quality-facts.schema.json` не подключается к `scripts/validate_contracts.py` | скрипт — для NATS wire-контракта с дедупом/версией сообщений; payload фактов — внутренний формат arm-core, не обмен между сервисами | изменение формата payload не поймает `make check` в этом репозитории; ловит тест arm-core | | 05.11.2026 | `legal_entity_name`/`group_name` добавлены и в `quality_failure_fact`, хотя design arm-core §4.3 их не перечислял | общее правило §4 design arm-core («имена ЮЛ и ГК копируются на момент факта») относится ко всем фактам; итемized список там был неполным | лишние 2 столбца, если экран всё же джойнит live-справочник для отказов | | 07.11.2026 | Схема перенесена в `contracts/metrics/quality-facts.schema.json` (R-16 arm-core) | в корне `contracts/` каждая `*.schema.json` обязана иметь копию в сервисах (`contract_identity.py`); подкаталог глубины 2 вне `examples/` копию не требует — схема не относится к NATS-контракту распознавания; содержание схемы не менялось | при возврате в корень тесты идентичности tabular-parser и scan-gateway покраснеют |