feat(#190): метрики качества первички (AURORA-1759) #229

Merged
aep merged 45 commits from feat/190-ocr-metrics into main 2026-11-07 07:50:00 +03:00
Owner

Closes #190 (AURORA-1759). Сливать после docs PR #203 (RSHB/arm-ka-docs): ORM и тесты опираются на DBML v5.25 и схему contracts/metrics/quality-facts.schema.json из той ветки.

Что сделано

Метрики качества первички: факты приёма и исходов (quality_intake_fact, quality_outcome_fact) пишутся в транзакции событий (приём, подтверждение, автоподтверждение, удаление, замена, отказ распознавания); атрибуция правок «сервис / наш разбор» по §14.2; матрица «источник × причина», ряд по месяцам, список документов ячейки, выгрузка CSV/JSON с аудитом EXPORT; право QUALITY_METRICS_VIEW; экран «Качество первички» (web); схема dkb_metrics для Superset; регистрация сырого ответа ИС (raw_s3_object_id), признак скана is_scan; OpenSpec change quality-metrics с дельтами.

Отдельно: меню Sidebar снова фильтрует пункты по правам — «Пользователи и роли» скрыт без USERS_ADMIN.

Выкладка

Схема dkb_metrics и право CREATE у роли миграций; read-only роль Superset; make seed (новое право); подробности — README, раздел «Метрики качества первички».

Что НЕ проверено

Экран «Качество первички» вручную на стенде не проверен — обязательная проверка владельцем до merge. e2e с реальным tabular-parser не запускался (профиль services не поднят, тест пропущен с причиной).

Проверки

make check-full на состоянии после влива origin/main: exit 0 — ruff/mypy, 2082 unit passed, statements-check 0 (копия синхронна с arm-statements 25b2bb8), dbml-check ok, 1269 integration passed, e2e 1 skipped (профиль services не поднят), docker-build и web-check ok. web: vitest 589 passed, lint 0 ошибок, prettier ok. openspec validate quality-metrics --strict — valid в arm-core и docs.

Rulings I made (design.md, раздел ## Decisions made without discussion)

R-1 имена экспорта arm_statements сверяются с реальным dir(); R-2 регистрация raw не зависит от status; R-3 build_*_payload расширены header_source / osv_row_count; R-4 атрибуция читает живые колонки, а не payload приёма; R-5a «Дата колонки» не учитывается; R-5b raw_line_codes не пишутся; R-6 добавленная строка PDF без кодов — «наш разбор»; R-7 payload↔контракт вынесен в финальное ревью; R-8 ряд по месяцам по дате приёма; R-9 SQL-оптимизация (T15.2–T15.4) без БД не делалась; R-10 нет фильтра по ЮЛ в UI; R-11 матрица источник × причина тремя запросами; R-12 аудит EXPORT; R-13 is_scan/raw_s3_object_id — последние колонки (ORM+DBML); R-14 uploads с фактом приёма не экспирится при удалении; R-15 has_text_layer в thread, лимит 10 страниц; R-16 схема перенесена в contracts/metrics/; R-17 values[] по контракту; R-18 fixes[] по контракту; R-19 (устарел после R-24); R-20 тест валидации payload; R-21 дата шапки через parse_date; R-22 условие §14.4 про соотношения не проверяется; R-23 состав дельт OpenSpec (MODIFIED копируют требования целиком — возможен конфликт при archive); R-24 is_recognized в схеме nullable. Цена ошибки по каждому — в design.md.

Находки (маркер — статус)

Финальное ревью (opus): C-1 fixed 85c5ab8; I-1 fixed e14f1ab; I-2 fixed 8c7ce7c (+docs 55f06e9); I-3 fixed 7a6c364 (+docs 760c3dc); I-4 fixed 7a6c364; I-5 fixed 8c7ce7c; I-6 (=T16.13) fixed 85c5ab8; I-7 fixed 3f6bd96; I-8 fixed 8f90d0c; M-1 fixed 85c5ab8; M-2 fixed 85c5ab8; M-3 fixed 7a6c364; M-4, M-5, M-7 fixed 3f6bd96; M-6 fixed (ledger); M-8 deferred.
Повторное ревью: I-9 fixed 24fbec8 (+docs aefbf78); I-10 fixed (merge 5a5e5a2 / docs 75f5b52); M-10 fixed 4593cdc; M-12 fixed 627c3e2 (docs); M-13 fixed 24fbec8; M-14 fixed 4593cdc; M-16 fixed 4593cdc/2f48171; M-17 fixed d1f4f45; M-9 (тест лимита страниц), M-11 (поле level в issues[]), M-15 (purge_statements не пишет факты DELETED) — deferred.
Задачи 1–16: fixed — T9.1, T10.3, T14.1–T14.4, T15.1, T16.1(ГК)–T16.7, T16.9–T16.11, T16.13; rejected — T1.2, T3.3, T5.1, T5.2, T10.1, T11.2–T11.4, T13.1, T14.5, T15.5–T15.7.

Deferred (для follow-up в Jira при закрытии)

M-8 = R-5a (правка «Дата колонки» не учитывается — нужна схема) и R-5b (raw_line_codes не извлекаются); M-9; M-11; M-15; T1.1; T2.1; T2.2; T3.1; T3.2; T3.4; T4.1; T4.2; T5.3; T5.4; T8.1; T9.2; T10.2; T11.1; T12.1; T15.2; T15.3; T15.4; T16.1 (фильтр по ЮЛ, R-10); T16.8; T16.12.

🤖 Generated with Claude Code

Closes #190 (AURORA-1759). **Сливать после docs PR #203** (RSHB/arm-ka-docs): ORM и тесты опираются на DBML v5.25 и схему `contracts/metrics/quality-facts.schema.json` из той ветки. ## Что сделано Метрики качества первички: факты приёма и исходов (`quality_intake_fact`, `quality_outcome_fact`) пишутся в транзакции событий (приём, подтверждение, автоподтверждение, удаление, замена, отказ распознавания); атрибуция правок «сервис / наш разбор» по §14.2; матрица «источник × причина», ряд по месяцам, список документов ячейки, выгрузка CSV/JSON с аудитом EXPORT; право `QUALITY_METRICS_VIEW`; экран «Качество первички» (web); схема `dkb_metrics` для Superset; регистрация сырого ответа ИС (`raw_s3_object_id`), признак скана `is_scan`; OpenSpec change `quality-metrics` с дельтами. Отдельно: меню Sidebar снова фильтрует пункты по правам — «Пользователи и роли» скрыт без `USERS_ADMIN`. ## Выкладка Схема `dkb_metrics` и право CREATE у роли миграций; read-only роль Superset; `make seed` (новое право); подробности — README, раздел «Метрики качества первички». ## Что НЕ проверено Экран «Качество первички» вручную на стенде не проверен — обязательная проверка владельцем до merge. e2e с реальным tabular-parser не запускался (профиль services не поднят, тест пропущен с причиной). ## Проверки `make check-full` на состоянии после влива origin/main: exit 0 — ruff/mypy, 2082 unit passed, statements-check 0 (копия синхронна с arm-statements 25b2bb8), dbml-check ok, 1269 integration passed, e2e 1 skipped (профиль services не поднят), docker-build и web-check ok. `web`: vitest 589 passed, lint 0 ошибок, prettier ok. `openspec validate quality-metrics --strict` — valid в arm-core и docs. ## Rulings I made (design.md, раздел `## Decisions made without discussion`) R-1 имена экспорта arm_statements сверяются с реальным dir(); R-2 регистрация raw не зависит от status; R-3 build_*_payload расширены header_source / osv_row_count; R-4 атрибуция читает живые колонки, а не payload приёма; R-5a «Дата колонки» не учитывается; R-5b raw_line_codes не пишутся; R-6 добавленная строка PDF без кодов — «наш разбор»; R-7 payload↔контракт вынесен в финальное ревью; R-8 ряд по месяцам по дате приёма; R-9 SQL-оптимизация (T15.2–T15.4) без БД не делалась; R-10 нет фильтра по ЮЛ в UI; R-11 матрица источник × причина тремя запросами; R-12 аудит EXPORT; R-13 `is_scan`/`raw_s3_object_id` — последние колонки (ORM+DBML); R-14 uploads с фактом приёма не экспирится при удалении; R-15 `has_text_layer` в thread, лимит 10 страниц; R-16 схема перенесена в `contracts/metrics/`; R-17 `values[]` по контракту; R-18 `fixes[]` по контракту; R-19 (устарел после R-24); R-20 тест валидации payload; R-21 дата шапки через parse_date; R-22 условие §14.4 про соотношения не проверяется; R-23 состав дельт OpenSpec (MODIFIED копируют требования целиком — возможен конфликт при archive); R-24 `is_recognized` в схеме nullable. Цена ошибки по каждому — в design.md. ## Находки (маркер — статус) Финальное ревью (opus): C-1 fixed 85c5ab8; I-1 fixed e14f1ab; I-2 fixed 8c7ce7c (+docs 55f06e9); I-3 fixed 7a6c364 (+docs 760c3dc); I-4 fixed 7a6c364; I-5 fixed 8c7ce7c; I-6 (=T16.13) fixed 85c5ab8; I-7 fixed 3f6bd96; I-8 fixed 8f90d0c; M-1 fixed 85c5ab8; M-2 fixed 85c5ab8; M-3 fixed 7a6c364; M-4, M-5, M-7 fixed 3f6bd96; M-6 fixed (ledger); M-8 deferred. Повторное ревью: I-9 fixed 24fbec8 (+docs aefbf78); I-10 fixed (merge 5a5e5a2 / docs 75f5b52); M-10 fixed 4593cdc; M-12 fixed 627c3e2 (docs); M-13 fixed 24fbec8; M-14 fixed 4593cdc; M-16 fixed 4593cdc/2f48171; M-17 fixed d1f4f45; M-9 (тест лимита страниц), M-11 (поле `level` в issues[]), M-15 (purge_statements не пишет факты DELETED) — deferred. Задачи 1–16: fixed — T9.1, T10.3, T14.1–T14.4, T15.1, T16.1(ГК)–T16.7, T16.9–T16.11, T16.13; rejected — T1.2, T3.3, T5.1, T5.2, T10.1, T11.2–T11.4, T13.1, T14.5, T15.5–T15.7. ## Deferred (для follow-up в Jira при закрытии) M-8 = R-5a (правка «Дата колонки» не учитывается — нужна схема) и R-5b (`raw_line_codes` не извлекаются); M-9; M-11; M-15; T1.1; T2.1; T2.2; T3.1; T3.2; T3.4; T4.1; T4.2; T5.3; T5.4; T8.1; T9.2; T10.2; T11.1; T12.1; T15.2; T15.3; T15.4; T16.1 (фильтр по ЮЛ, R-10); T16.8; T16.12. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
aep added 45 commits 2026-11-07 03:33:22 +03:00
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OQ-111 уже занят (ретеншн S3 общего назначения) — реальный свободный номер для
срока хранения raw/{job_id}.json нашёлся при работе над arm-ka-docs#202.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Снимки приёма и исхода получают необязательные kwargs: header_source (сырой текст шапки
из financial_statement_meta.header_source) кладётся в header для ББ/ОФР, osv_row_count —
в payload ОСВ как row_count. Без kwargs ключи не эмитятся.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Тонкие функции insert_intake_fact / insert_outcome_fact / insert_failure_fact: session.add
без flush и commit, факт попадает в транзакцию вызывающего (design §4).

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Хук в _map_candidate (result_mapping.py): после map_file, до auto_confirm — для
каждого документа BALANCE/INCOME/OSV строит снимок build_intake_payload и пишет
quality_intake_fact через insert_intake_fact. Чисто добавочный шаг: не меняет
документы, статусы и прочий результат маппинга; DISCLOSURE/OTHER факта не получают.

Счётчики строк/значений — из financial_statement_* (BALANCE/INCOME) и document_issue;
warning_count/osv_warnings — предупреждения источника service (document_issue.source =
service): для BALANCE/ОФР 1.x таких записей нет, для ОСВ это коды OSV_*. Для ОСВ
построчные счётчики не считаются (их в ядре нет), только row_count в payload.

Тесты в test_result_mapping.py (маркер integration, не запускались — нет PostgreSQL
в песочнице, подтверждена только коллекция) + один DB-free тест на множество типов
документов хука.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Хук в auto_confirm: на каждый документ, подтверждённый системой,
пишется quality_outcome_fact (outcome=AUTO_CONFIRMED,
primary_cause=WITHOUT_PARTICIPATION всегда — правок при автоподтверждении
не бывает, D-3 блокирует формы с нарушениями). payload строится
build_outcome_payload по тому же шаблону, что факт приёма (Task 5):
ОСВ — osv_warnings/row_count, формы — lines/values/header_source,
issue_count_at_outcome — все нарушения документа без фильтра.
Запись в транзакции маппинга, без flush/commit (Task 4 insert_outcome_fact).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ревью нашло: хук писал quality_outcome_fact для ЛЮБОГО автоподтверждённого
документа, в том числе DISCLOSURE/OTHER — у них нет факта приёма
(INTAKE_FACT_DOCUMENT_TYPES, Task 5), и факт исхода без пары превратился бы
в сироту при соединении по document_id. Добавлен гейт
OUTCOME_FACT_DOCUMENT_TYPES = {BALANCE, INCOME, OSV} (не импортируется из
result_mapping.py — там уже отложенный импорт auto_confirm во избежание
цикла); auto_confirm для DISCLOSURE/OTHER не меняется — только пропускает
запись факта. Поправлен вводивший в заблуждение комментарий в
_outcome_fact_fields. Тест на DISCLOSURE: автоподтверждение проходит,
quality_outcome_fact не появляется.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
DocumentService._write_confirm_outcome_fact — третий хук quality_outcome_fact
(outcome=CONFIRMED). В отличие от AUTO_CONFIRMED правки возможны: суммы и коды
строк сравниваются с живыми *_modified/original_*/source_text (не с JSON факта
приёма), добавленные строки и правки шапки — через attribute_* (Task 3), период/
счёт ОСВ — всегда "наш разбор". primary_cause — по приоритету сервис > наш разбор
> документ клиента > как есть. Гейт типа документа (BALANCE/INCOME/OSV) —
OUTCOME_FACT_DOCUMENT_TYPES, DISCLOSURE/OTHER без факта.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ревью задачи 7, раунд 1: (1) добавлена строка design.md 06.11.2026 — решение
«нет сырого текста → наш разбор» для value/code/header фиксов задокументировано
отдельно (не то же, что 1250\n1260); тем же рядом исправлено направление для
PDF-добавленной строки — "наш разбор", не "сервис" (пустой raw_line_codes не
должен давать service через attribute_added_row, это противоречило уже принятому
27.10.2026 решению). (2) _added_row_fix для PDF больше не вызывает
attribute_added_row с заведомо пустым raw_line_codes — атрибуция "ours" прямо.
(3) _header_fix теперь возвращает "before" (meta.<field>_original_value), как
у _value_fix/_code_fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Удаление файла (PrimaryFilesService.delete) пишет факт качества quality_outcome_fact
по каждому документу типа BALANCE/INCOME/OSV до физического удаления строк: DELETED —
документ не подтверждался (RECOGNIZED/NEEDS_CHECK), DELETED_AFTER_CONFIRM —
автоподтверждённый CONFIRMED (вручную подтверждённые сюда не доходят, дают 409 раньше
в том же методе). primary_cause=None явно (§14.3 правило 1, вне матрицы причин),
payload — минимальный снимок без построчных данных (build_outcome_payload(document, fixes=())).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Перенос трёх решений, делегированных брифом исполнителю и задокументированных в докстринге
PrimaryFilesService._write_delete_outcome_facts, в раздел «Decisions made without discussion»
по правилу CLAUDE.md о маркировке находок.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
§14.3 (design quality-metrics §4.2, AURORA-1759): supersede_previous пишет
один факт исхода на каждый документ BALANCE/INCOME/OSV заменённого файла —
и выпавший (без ключа среди документов нового файла, OQ-154), и совпавший
по ключу («rest»): замена идёт файлом целиком (design №33), поэтому факт
не ограничивается только выпавшими документами. CONFIRMED на момент замены
(в т. ч. вручную — здесь, в отличие от удаления файла, нет 409-проверки) →
SUPERSEDED_AFTER_CONFIRM, иначе SUPERSEDED; primary_cause всегда NULL (вне
матрицы причин). Счётчик нарушений — одним запросом на все документы всех
заменяемых файлов.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ревью фикс-раунда 1: у old_file в обоих тестах был только один документ
(account="62"), ключ которого не совпадал ни с одним документом нового
файла (account="60") — _replaceable не находила совпадений, file_ids
оставался пустым, и supersede_previous возвращала [] на раннем
`if not file_ids: return []`, до locked/хука. Тесты не проверяли
SUPERSEDED/SUPERSEDED_AFTER_CONFIRM, а падали бы с NoResultFound при
реальном прогоне. Добавлен документ account="60" в old_file каждого
теста — триггер замены файла; account="62" остаётся выпавшим документом,
на котором проверяется факт исхода.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Документирует находку ревью задачи 9 (minor, deferred): почему supersede_previous не
исключает ERROR-статус из факта исхода, в отличие от primary_files.delete.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Задача 10: quality_failure_fact на 6 точках отказа — вахтёр сроков
(expire_deadlines, TIMEOUT, батчем на все просроченные задания), dlq
(_process_one, NO_ACK), невалидный запрос и результат сервиса со
status=error (_process_one/_apply_result, SERVICE_ERROR), неповторяемая
и исчерпанная ошибки маппинга (_fail/_register_failure, MAPPING).
Измерения ЮЛ/ГК/is_scan — отдельным запросом по file_id (батчем у
вахтёра), отсутствие строки ЮЛ не прерывает обработку, только лог.

Восстановление после обрыва сессии: имплементер задачи 10 (agent
a7ad86adedbac1d8a) закончил работу, но коммит и отчёт не состоялись —
диф остался в рабочем дереве незакоммиченным. Коммит и прогон
проверок — контроллером (ruff/mypy/pytest -m "not integration" чисты;
один magic-value в новом тесте поправлен тут же), не ревью находка.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ревью задачи 10 нашло сплайс: при реконструкции после обрыва сессии
`assert DocumentType.OTHER not in rm.INTAKE_FACT_DOCUMENT_TYPES` приклеилась
к концу test_file_reprocess_keeps_failure_fact_of_first_attempt_and_adds_new_intake
(FILE_REPROCESS, §14.5) вместо своего теста
test_intake_fact_document_types_excludes_disclosure_and_other, чья докстрока
обещала проверку обоих исключённых типов (DISCLOSURE/OTHER), но проверяла
только DISCLOSURE. Строка перемещена на своё место — ровно 1 строка туда,
1 строка оттуда, больше ничего в файле не менялось.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
_register_raw_object (по образцу _register_result_object) регистрирует raw_s3_key
в s3_object с bucket_role=raw; бакет берётся у файла задания (stored_file -> s3_object),
не у message.result_s3 (отсутствует при status=error) — именно error чаще всего и даёт
сырой ответ без результата. Регистрация не зависит от message.status (R-2). Поле
RecognitionJob.raw_s3_object_id уже существовало (Task 1) — решение v5.13 «не
регистрируется» отменено v5.25 (OQ-179), комментарий contract/message.py обновлён.

Тесты — tests/test_recognition_inbox.py (там же тестируются _apply_result и
_register_result_object через handle_message, а не в test_recognition_exchange.py,
который покрывает только outbox/вахтёр).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- series/cells/export: одна query-модель на маршрут (QualitySeriesParams, QualityCellParams, QualityExportParams),
  иначе FastAPI требовал параметр `filters` и отвечал 422
- _latest_outcomes не тянет payload jsonb
- выгрузка пишет audit_event(EXPORT), тест; design.md: R-12 и исправлено обоснование
- docstring задачи 10: sites 1-3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	web/src/auth/permissions.ts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
aep merged commit 383113cb2b into main 2026-11-07 07:50:00 +03:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RSHB/arm-core#229
No description provided.