2026-11-07 03:56:47 +04:00

182 lines
14 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.

# Дизайн: модель данных для метрик качества первички (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 покраснеют |