From 9bcf6373fee443b0ded61fc42d54cf0dc64d004f Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 7 Nov 2026 08:51:25 +0400 Subject: [PATCH] =?UTF-8?q?=D0=90=D1=80=D1=85=D0=B8=D0=B2=20change=20quali?= =?UTF-8?q?ty-metrics=20(AURORA-1759)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../.openspec.yaml | 0 .../2026-11-07-quality-metrics}/design.md | 0 .../2026-11-07-quality-metrics}/proposal.md | 0 .../specs/audit-trail/spec.md | 0 .../specs/authn-authz/spec.md | 0 .../specs/documents-api/spec.md | 0 .../specs/osv-result-mapping/spec.md | 0 .../specs/primary-files/spec.md | 0 .../specs/quality-metrics-ui/spec.md | 0 .../specs/quality-metrics/spec.md | 0 .../specs/recognition-job-exchange/spec.md | 0 .../specs/reference-data-seed/spec.md | 0 openspec/specs/audit-trail/spec.md | 11 ++ openspec/specs/authn-authz/spec.md | 6 +- openspec/specs/documents-api/spec.md | 6 +- openspec/specs/osv-result-mapping/spec.md | 16 ++- openspec/specs/primary-files/spec.md | 12 +- openspec/specs/quality-metrics-ui/spec.md | 42 ++++++ openspec/specs/quality-metrics/spec.md | 123 ++++++++++++++++++ .../specs/recognition-job-exchange/spec.md | 16 ++- openspec/specs/reference-data-seed/spec.md | 10 +- 21 files changed, 231 insertions(+), 11 deletions(-) rename openspec/changes/{quality-metrics => archive/2026-11-07-quality-metrics}/.openspec.yaml (100%) rename openspec/changes/{quality-metrics => archive/2026-11-07-quality-metrics}/design.md (100%) rename openspec/changes/{quality-metrics => archive/2026-11-07-quality-metrics}/proposal.md (100%) rename openspec/changes/{quality-metrics => archive/2026-11-07-quality-metrics}/specs/audit-trail/spec.md (100%) rename openspec/changes/{quality-metrics => archive/2026-11-07-quality-metrics}/specs/authn-authz/spec.md (100%) rename openspec/changes/{quality-metrics => archive/2026-11-07-quality-metrics}/specs/documents-api/spec.md (100%) rename openspec/changes/{quality-metrics => archive/2026-11-07-quality-metrics}/specs/osv-result-mapping/spec.md (100%) rename openspec/changes/{quality-metrics => archive/2026-11-07-quality-metrics}/specs/primary-files/spec.md (100%) rename openspec/changes/{quality-metrics => archive/2026-11-07-quality-metrics}/specs/quality-metrics-ui/spec.md (100%) rename openspec/changes/{quality-metrics => archive/2026-11-07-quality-metrics}/specs/quality-metrics/spec.md (100%) rename openspec/changes/{quality-metrics => archive/2026-11-07-quality-metrics}/specs/recognition-job-exchange/spec.md (100%) rename openspec/changes/{quality-metrics => archive/2026-11-07-quality-metrics}/specs/reference-data-seed/spec.md (100%) create mode 100644 openspec/specs/quality-metrics-ui/spec.md create mode 100644 openspec/specs/quality-metrics/spec.md diff --git a/openspec/changes/quality-metrics/.openspec.yaml b/openspec/changes/archive/2026-11-07-quality-metrics/.openspec.yaml similarity index 100% rename from openspec/changes/quality-metrics/.openspec.yaml rename to openspec/changes/archive/2026-11-07-quality-metrics/.openspec.yaml diff --git a/openspec/changes/quality-metrics/design.md b/openspec/changes/archive/2026-11-07-quality-metrics/design.md similarity index 100% rename from openspec/changes/quality-metrics/design.md rename to openspec/changes/archive/2026-11-07-quality-metrics/design.md diff --git a/openspec/changes/quality-metrics/proposal.md b/openspec/changes/archive/2026-11-07-quality-metrics/proposal.md similarity index 100% rename from openspec/changes/quality-metrics/proposal.md rename to openspec/changes/archive/2026-11-07-quality-metrics/proposal.md diff --git a/openspec/changes/quality-metrics/specs/audit-trail/spec.md b/openspec/changes/archive/2026-11-07-quality-metrics/specs/audit-trail/spec.md similarity index 100% rename from openspec/changes/quality-metrics/specs/audit-trail/spec.md rename to openspec/changes/archive/2026-11-07-quality-metrics/specs/audit-trail/spec.md diff --git a/openspec/changes/quality-metrics/specs/authn-authz/spec.md b/openspec/changes/archive/2026-11-07-quality-metrics/specs/authn-authz/spec.md similarity index 100% rename from openspec/changes/quality-metrics/specs/authn-authz/spec.md rename to openspec/changes/archive/2026-11-07-quality-metrics/specs/authn-authz/spec.md diff --git a/openspec/changes/quality-metrics/specs/documents-api/spec.md b/openspec/changes/archive/2026-11-07-quality-metrics/specs/documents-api/spec.md similarity index 100% rename from openspec/changes/quality-metrics/specs/documents-api/spec.md rename to openspec/changes/archive/2026-11-07-quality-metrics/specs/documents-api/spec.md diff --git a/openspec/changes/quality-metrics/specs/osv-result-mapping/spec.md b/openspec/changes/archive/2026-11-07-quality-metrics/specs/osv-result-mapping/spec.md similarity index 100% rename from openspec/changes/quality-metrics/specs/osv-result-mapping/spec.md rename to openspec/changes/archive/2026-11-07-quality-metrics/specs/osv-result-mapping/spec.md diff --git a/openspec/changes/quality-metrics/specs/primary-files/spec.md b/openspec/changes/archive/2026-11-07-quality-metrics/specs/primary-files/spec.md similarity index 100% rename from openspec/changes/quality-metrics/specs/primary-files/spec.md rename to openspec/changes/archive/2026-11-07-quality-metrics/specs/primary-files/spec.md diff --git a/openspec/changes/quality-metrics/specs/quality-metrics-ui/spec.md b/openspec/changes/archive/2026-11-07-quality-metrics/specs/quality-metrics-ui/spec.md similarity index 100% rename from openspec/changes/quality-metrics/specs/quality-metrics-ui/spec.md rename to openspec/changes/archive/2026-11-07-quality-metrics/specs/quality-metrics-ui/spec.md diff --git a/openspec/changes/quality-metrics/specs/quality-metrics/spec.md b/openspec/changes/archive/2026-11-07-quality-metrics/specs/quality-metrics/spec.md similarity index 100% rename from openspec/changes/quality-metrics/specs/quality-metrics/spec.md rename to openspec/changes/archive/2026-11-07-quality-metrics/specs/quality-metrics/spec.md diff --git a/openspec/changes/quality-metrics/specs/recognition-job-exchange/spec.md b/openspec/changes/archive/2026-11-07-quality-metrics/specs/recognition-job-exchange/spec.md similarity index 100% rename from openspec/changes/quality-metrics/specs/recognition-job-exchange/spec.md rename to openspec/changes/archive/2026-11-07-quality-metrics/specs/recognition-job-exchange/spec.md diff --git a/openspec/changes/quality-metrics/specs/reference-data-seed/spec.md b/openspec/changes/archive/2026-11-07-quality-metrics/specs/reference-data-seed/spec.md similarity index 100% rename from openspec/changes/quality-metrics/specs/reference-data-seed/spec.md rename to openspec/changes/archive/2026-11-07-quality-metrics/specs/reference-data-seed/spec.md diff --git a/openspec/specs/audit-trail/spec.md b/openspec/specs/audit-trail/spec.md index fb817ec..99a8a0e 100644 --- a/openspec/specs/audit-trail/spec.md +++ b/openspec/specs/audit-trail/spec.md @@ -212,3 +212,14 @@ object_type = document, object_id документа, user_id инициатор #### Scenario: Возврат к исходному - **WHEN** аналитик возвращает исходные значения после предыдущей правки - **THEN** новое RECOGNIZED_DATA_EDIT отражает эту правку; старые события не меняются + +### Requirement: Аудит выгрузки метрик качества +Каждый успешный `GET /quality-metrics/export` SHALL добавлять отдельное неизменяемое событие `audit_event` с +`action = EXPORT`, `section = EXPORT`, `object_type = quality_metrics`, `object_id = null`, `user_id` инициатора, +`before_value = null` и `after_value = {report, format, filters, rows}`: `report` — тип выгрузки, `format` — `csv` или +`json`, `filters` — применённые фильтры, `rows` — число выгруженных строк. Выгрузка сводной ОСВ и шахматки продолжают +писать собственные события `EXPORT`. + +#### Scenario: Выгрузка метрик оставляет след +- **WHEN** пользователь с `QUALITY_METRICS_VIEW` выгружает факты за период в CSV +- **THEN** записано одно событие `EXPORT` с `object_type = quality_metrics`, форматом, фильтрами и числом строк diff --git a/openspec/specs/authn-authz/spec.md b/openspec/specs/authn-authz/spec.md index e6b37a8..5cfc215 100644 --- a/openspec/specs/authn-authz/spec.md +++ b/openspec/specs/authn-authz/spec.md @@ -49,12 +49,16 @@ claim `AUTH_EMAIL_CLAIM` (по умолчанию `email`) и найти `app_us Маршрут SHALL возвращать текущего пользователя: `id`, `email`, `full_name`, `regional_office {id, name, is_head_office}`, `business_segment`, `roles` (список `code`), `role_details` (те же активные роли списком `{code, name}`, `name` — `role.name`, порядок как в `roles`), `permissions` (отсортированный список `code`). Это единственный маршрут -Среза 0, доступный любому аутентифицированному пользователю без `require`. +Среза 0, доступный любому аутентифицированному пользователю без `require`. Состав `permissions` определяется матрицей ролей (§10), включая `QUALITY_METRICS_VIEW`, который seed выдаёт всем ролям. #### Scenario: Ответ для dev-пользователя - **WHEN** `ka_rf@armka.local` (роль `KA_RF`) запрашивает `/api/v1/me` - **THEN** ответ 200 содержит `roles = ["KA_RF"]`, `role_details` с одним элементом `{code: "KA_RF", name: <непустое название>}` и `permissions`, равные правам `KA_RF` из матрицы §10 +#### Scenario: Право метрик в профиле +- **WHEN** любой пользователь запрашивает `/api/v1/me` после `make seed` +- **THEN** `permissions` содержит `QUALITY_METRICS_VIEW`, отсортированно, перед `USERS_ADMIN` + ### Requirement: Dev-эмитент токенов только в режиме разработки При `AUTH_DEV_ISSUER=true` **и** `DEBUG=true` ядро SHALL предоставлять `POST /api/v1/dev/token` с телом `{"email"}`, возвращающий HS256 JWT (срок 8 часов) для существующего активного пользователя. При любом другом сочетании флагов diff --git a/openspec/specs/documents-api/spec.md b/openspec/specs/documents-api/spec.md index 9cf7aca..30109a0 100644 --- a/openspec/specs/documents-api/spec.md +++ b/openspec/specs/documents-api/spec.md @@ -58,7 +58,7 @@ v5.18 — консолидация, в контексте которой фай `period_quarter`, признак нарастающего: `is_cumulative` true или NULL; для ОСВ за `Q1` признак не различается) в той же версии SHALL отвязываться (R-1). Каждая версия, получившая привязку, из `INITIATED` или `DOCUMENTS_PROCESSING` SHALL переходить в `DATA_CORRECTION` (`changed_by` = -подтвердивший). Блокировка версии (`consolidation_lock`) для подтверждения MUST NOT требоваться. +подтвердивший). Блокировка версии (`consolidation_lock`) для подтверждения MUST NOT требоваться. В той же транзакции ручное подтверждение документа `BALANCE` / `INCOME` / `OSV` SHALL записывать `quality_outcome_fact` (`outcome = CONFIRMED`) с отнесением правок по живым колонкам документа (`is_modified`, `original_*`, `source_text`), а не по `payload` приёма; `checks_accepted` — признак акцепта проверок шапкой (на ячейку матрицы не влияет: при правках приоритет у «сервиса» и «нашего разбора»). `DISCLOSURE` / `OTHER` факта не получают. #### Scenario: Причина ошибки маппинга в карточке файла - **WHEN** файл в `ERROR` после неудачного маппинга (задание `done`, `error_code` пуст) @@ -109,6 +109,10 @@ v5.18 — консолидация, в контексте которой фай - **WHEN** в файле два ББ на 30.09.2025, первый подтверждён, аналитик подтверждает второй - **THEN** 409 `DUPLICATE_FORM` со ссылкой на первый +#### Scenario: Подтверждение с правкой суммы +- **WHEN** аналитик исправил сумму и подтвердил ББ +- **THEN** записан `quality_outcome_fact` `CONFIRMED` со счётчиком правки значения по источнику и `payload.fixes` + ### Requirement: Подхват документов при создании версии и расширении периметра При создании версии и при добавлении ЮЛ в периметр ядро SHALL привязать уже подтверждённые документы этого ЮЛ из незаменённых файлов, подходящие версии (условия — как при подтверждении); на один ключ (`document_type`, diff --git a/openspec/specs/osv-result-mapping/spec.md b/openspec/specs/osv-result-mapping/spec.md index 4e7946b..c81ca43 100644 --- a/openspec/specs/osv-result-mapping/spec.md +++ b/openspec/specs/osv-result-mapping/spec.md @@ -15,7 +15,7 @@ LOCKED`), проверять, что `result_s3_object` относится к б в одной транзакции создать документы и строки (листы ОСВ и формы ББ/ОФР), записать `recognition_job.group_schema`, перевести файл в `PROCESSED` и выполнить автозамену. Результат без документов SHALL давать неповторяемую ошибку маппинга «в результате нет документов»; результат с формами, но без ОСВ, ошибкой MUST NOT быть. Inbox MUST NOT читать -файл результата и MUST NOT создавать документы. +файл результата и MUST NOT создавать документы. После `map_file` и до автоподтверждения транзакция маппинга SHALL писать факты приёма `quality_intake_fact` по документам `BALANCE` / `INCOME` / `OSV`; неповторяемая ошибка маппинга (файл → `ERROR`) SHALL писать `quality_failure_fact` (`failure_kind = MAPPING`, `error_code = MAPPING_FAILED`). #### Scenario: Результат смапплен - **WHEN** в S3 лежит результат парсера для `rb/2024-FY/ОСВ_66 за 2024.xls`, задание `done`, файл `PROCESSING` @@ -44,6 +44,14 @@ LOCKED`), проверять, что `result_s3_object` относится к б - **WHEN** результат без документов (`documents = []`): контракт v1.x такого результата не допускает — его останавливает проверка схемы, а прямой вызов `map_file` даёт `MappingFatal` - **THEN** файл `ERROR`, причина в `metrics.mapping_error` (при проверке схемы — ошибка схемы, в `map_file` — «в результате нет документов»), повтор не выполняется +#### Scenario: Факт приёма в транзакции маппинга +- **WHEN** результат смаплен и создан документ ОСВ +- **THEN** в той же транзакции записан `quality_intake_fact` документа до перехода в `CONFIRMED` + +#### Scenario: Неповторяемая ошибка маппинга +- **WHEN** маппинг завершился неповторяемой ошибкой +- **THEN** файл `ERROR` и записан `quality_failure_fact` с `failure_kind = MAPPING` + ### Requirement: Документ по листу ОСВ На каждый `osvDocument` ядро SHALL создать `document` с `document_type = OSV`, `osv_account_code` = нормализованный код из шапки (`60.1 → 60.01`, `58.03`, буквенные сегменты как есть), `period_year`, `period_quarter`, `is_cumulative` @@ -222,7 +230,7 @@ object_type = document)` с `user_id = NULL`, `before_value.status = RECOGNIZED` получившая привязку, из `INITIATED` или `DOCUMENTS_PROCESSING` SHALL переходить в `DATA_CORRECTION` с `changed_by = stored_file.uploaded_by` и `comment = «автоподтверждение документа »` (счёт — только у ОСВ). Если `uploaded_by` NULL, привязка SHALL создаваться, а переход — пропускаться с записью в лог. Пересчёт статуса -формы после правки или акцепта MUST NOT подтверждать документ автоматически. +формы после правки или акцепта MUST NOT подтверждать документ автоматически. Для каждого автоподтверждённого документа `BALANCE` / `INCOME` / `OSV` SHALL записываться `quality_outcome_fact` (`AUTO_CONFIRMED`, `primary_cause = WITHOUT_PARTICIPATION`, `confirmed_by` пуст); автозамена прежних версий SHALL писать `SUPERSEDED` / `SUPERSEDED_AFTER_CONFIRM` по вытесненным документам. #### Scenario: Чистая ОСВ сразу «Загружен» - **WHEN** смаплен результат `fixtures/osv/rb/2025-Q1/ОСВ сч 60 за 1 кв 2025 год.xlsx` для ЮЛ АО «Русский бисквит» @@ -257,6 +265,10 @@ stored_file.uploaded_by` и `comment = «автоподтверждение до (чистый), A заменён автозаменой - **THEN** привязка документа A снята, документ B `CONFIRMED` и привязан к версии +#### Scenario: Факт автоподтверждения +- **WHEN** документ автоподтверждён +- **THEN** есть `quality_outcome_fact` `AUTO_CONFIRMED` для этого документа + ### Requirement: Хранение результата формы v2.0 (DBML v5.19) Маппер результата `2.0` SHALL писать документ формы как пришёл: строки, коды и значения не сливаются и не дедуплицируются (повтор кода — отдельные строки), каждая строка результата — `financial_statement_line` (`row_index`, diff --git a/openspec/specs/primary-files/spec.md b/openspec/specs/primary-files/spec.md index d7a06dd..6972e76 100644 --- a/openspec/specs/primary-files/spec.md +++ b/openspec/specs/primary-files/spec.md @@ -22,7 +22,7 @@ запрашиваться (макет `40`). Без доступного S3 — 503 `STORAGE_UNAVAILABLE`. Query-параметр `consolidation_version_id` MUST NOT приниматься (удалён в v5.18): загрузка этим маршрутом — без контекста консолидации и не меняет статусы версий; загрузка в консолидацию — требование «Загрузка файлов в -консолидацию». +консолидацию». Для PDF `stored_file.is_scan` SHALL определяться при загрузке по наличию текстового слоя в первых 10 страницах (разбор pypdf — вне event loop, `asyncio.to_thread`): нет текста — скан (`true`), есть — цифровой (`false`); для не-PDF — NULL. PDF со скрытым OCR-слоем считается цифровым. #### Scenario: Реальная ОСВ принята и ушла в outbox - **WHEN** `ka_rf` загружает `rb/2024-FY/ОСВ_66 за 2024.xls` для ЮЛ своего филиала @@ -51,6 +51,10 @@ Query-параметр `consolidation_version_id` MUST NOT приниматьс - **WHEN** ЮЛ не входит в периметр ни одной версии, файл загружен маршрутом ЮЛ - **THEN** 201 — маршрут ЮЛ периметр не проверяет; проверка периметра — только у загрузки в консолидацию +#### Scenario: Признак скана +- **WHEN** загружается PDF без текстового слоя +- **THEN** `stored_file.is_scan = true`; у xlsx `is_scan IS NULL` + ### Requirement: Дедуп по содержимому Файл с тем же sha256, что у существующего `stored_file` того же ЮЛ со статусом не `SUPERSEDED`, SHALL отклоняться с кодом `DUPLICATE_FILE` и `file_id` существующего. Тот же файл у другого ЮЛ SHALL приниматься. @@ -147,7 +151,7 @@ NULL), или если автоподтверждённый документ ф файл ссылаются `superseded_by_file_id`. При удалении файла с автоподтверждёнными документами их привязки `consolidation_document` SHALL сниматься, статус версий MUST NOT меняться, `after_value.unbound_version_ids` — id версий, где привязки сняты (по возрастанию; пустой список, если привязок не было). Блокировки: задания → файл → документы → -advisory-блокировка привязок ГК → версии по `id`. Файл `SUPERSEDED` удалить можно. +advisory-блокировка привязок ГК → версии по `id`. Файл `SUPERSEDED` удалить можно. Если у файла есть `quality_intake_fact` (по `file_id`), объект `uploads` файла MUST NOT получать `expires_at` — он нужен эталонному прогону качества; сырой ответ ИС (`raw`) при удалении `expires_at` не получает никогда. Для документов с исходом удаление SHALL писать факт исхода `DELETED` / `DELETED_AFTER_CONFIRM` в той же транзакции (спека `quality-metrics`). #### Scenario: Удаление ошибочного файла - **WHEN** файл в `ERROR` удаляется владельцем @@ -170,6 +174,10 @@ advisory-блокировка привязок ГК → версии по `id`. - **WHEN** удаляется файл, все документы которого автоподтверждены и ни к одной версии не привязаны - **THEN** 204; `after_value.unbound_version_ids = []` +#### Scenario: Файл с фактом приёма +- **WHEN** удаляется файл, по документам которого записан `quality_intake_fact` +- **THEN** 204; объект `uploads` и сырой ответ остаются без `expires_at`, результат (`results`) экспирится; записан факт исхода + ### Requirement: Загрузка файлов в консолидацию `POST /api/v1/consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files` (multipart `files`, право `DOCUMENT_EDIT`; §9.5 п. 8 v5.18) SHALL до приёма файлов проверить: консолидация видима (ГК в области пользователя; diff --git a/openspec/specs/quality-metrics-ui/spec.md b/openspec/specs/quality-metrics-ui/spec.md new file mode 100644 index 0000000..0b5c176 --- /dev/null +++ b/openspec/specs/quality-metrics-ui/spec.md @@ -0,0 +1,42 @@ +# quality-metrics-ui Specification + +## Purpose +Экран «Качество первички» веб-интерфейса: матрица «источник × причина», динамика по месяцам, список документов ячейки, +выгрузка фактов; пункт меню по праву. + +## Requirements + +### Requirement: Экран «Качество первички» +Веб-интерфейс SHALL содержать экран «Качество первички» с фильтрами (период, ГК, тип документа, источник, скан/цифровой), +матрицей «источник × причина» (колонки PDF, Excel, Всего; технические отказы — отдельным столбцом по файлам с пометкой, +что фильтр типа на них не действует), динамикой по месяцам выбранной причины и кнопкой выгрузки фактов (CSV / JSON). +Фильтра по ЮЛ на экране нет (API его поддерживает). Справочник ГК для фильтра SHALL запрашиваться один раз за +жизнь экрана, в том числе при пустом ответе или ошибке. + +#### Scenario: Пустой справочник ГК +- **WHEN** справочник ГК пуст или запрос завершился ошибкой +- **THEN** экран не повторяет запрос бесконечно + +#### Scenario: Выбран источник +- **WHEN** в фильтре выбран PDF +- **THEN** матрица показывает одну колонку + +### Requirement: Переход из ячейки в документ +Клик по ячейке матрицы SHALL открывать список документов ячейки (постранично, со счётчиками правок «сервис» и «наш +разбор» и исходом); из строки списка, если документ ещё существует, SHALL быть переход в экран проверки документа. + +#### Scenario: Документ удалён +- **WHEN** документ ячейки уже удалён +- **THEN** строка показана без ссылки на документ + +### Requirement: Пункт меню по праву +Боковое меню SHALL показывать пункт «Качество первички» только при наличии `QUALITY_METRICS_VIEW` и снова SHALL +скрывать пункты по полю `permission`: «Пользователи и роли» скрыт без `USERS_ADMIN`. Доступ по-прежнему решает сервер. + +#### Scenario: Нет права +- **WHEN** в `/me` нет `QUALITY_METRICS_VIEW` +- **THEN** пункта «Качество первички» в меню нет + +#### Scenario: Нет USERS_ADMIN +- **WHEN** в `/me` нет `USERS_ADMIN` +- **THEN** пункт «Пользователи и роли» скрыт diff --git a/openspec/specs/quality-metrics/spec.md b/openspec/specs/quality-metrics/spec.md new file mode 100644 index 0000000..41b02f0 --- /dev/null +++ b/openspec/specs/quality-metrics/spec.md @@ -0,0 +1,123 @@ +# 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` diff --git a/openspec/specs/recognition-job-exchange/spec.md b/openspec/specs/recognition-job-exchange/spec.md index 3a63913..3fceee6 100644 --- a/openspec/specs/recognition-job-exchange/spec.md +++ b/openspec/specs/recognition-job-exchange/spec.md @@ -58,7 +58,7 @@ заданию) SHALL получить `stored_file.status = ERROR`; при `done`/`needs_check` файл остаётся `PROCESSING` и SHALL быть обработан циклом маппинга (спека `osv-result-mapping`); inbox MUST NOT читать файл результата. Неизвестный `job_id` SHALL быть залогирован. Результат для задания, уже находящегося в финальном статусе (`done`, `needs_check`, `error`, -`timeout`, `dlq`), SHALL логироваться и не применяться. +`timeout`, `dlq`), SHALL логироваться и не применяться. Если в конверте есть `raw_s3_key`, ядро SHALL независимо от `status` зарегистрировать сырой ответ ИС в `s3_object` (`bucket_role = raw`, ключ `raw/{job_id}.json`) и проставить `recognition_job.raw_s3_object_id`. При `status = error` в той же транзакции SHALL записываться `quality_failure_fact` (`failure_kind = SERVICE_ERROR`, `error_code` из конверта). #### Scenario: Успешный результат - **WHEN** сервис публикует конверт `status = done` с `result_s3 = {arm-ka, results///result.json}` и @@ -79,15 +79,27 @@ - **WHEN** задание уже `timeout`, и приходит конверт `done` - **THEN** задание не меняется, событие залогировано +#### Scenario: Сырой ответ зарегистрирован +- **WHEN** конверт `done` содержит `raw_s3_key = raw/.json` +- **THEN** создан `s3_object` с `bucket_role = raw`, `recognition_job.raw_s3_object_id` указывает на него + +#### Scenario: Факт отказа сервиса +- **WHEN** конверт содержит `status = error` +- **THEN** кроме `ERROR` файла записан `quality_failure_fact` с `failure_kind = SERVICE_ERROR` + ### Requirement: Таймаут ожидания результата Фоновый вахтёр SHALL с периодом 30 с переводить задания `processing` и `published` с `reply_deadline < now()` в `status = timeout`, `error_code = TIMEOUT`, `finished_at = now()`, а их файлы (по `current_recognition_job_id`) — в -`stored_file.status = ERROR`. Публикатор при переводе задания в `dlq` SHALL так же переводить файл в `ERROR`. Ручной повтор после TIMEOUT/NO_ACK SHALL создавать новое задание по требованию «Новая попытка обработки файла». +`stored_file.status = ERROR`. Публикатор при переводе задания в `dlq` SHALL так же переводить файл в `ERROR`. Ручной повтор после TIMEOUT/NO_ACK SHALL создавать новое задание по требованию «Новая попытка обработки файла». Каждый перевод в `timeout` и в `dlq` SHALL писать `quality_failure_fact` (`TIMEOUT` / `NO_ACK`) в той же транзакции. #### Scenario: Сервис взял, но не ответил - **WHEN** задание в `processing` и `reply_deadline` прошёл без результата - **THEN** не позднее чем через 30 с задание имеет `status = timeout`, файл — `ERROR` с `error_code = TIMEOUT` +#### Scenario: Факт таймаута +- **WHEN** вахтёр перевёл задание в `timeout` +- **THEN** записан `quality_failure_fact` с `failure_kind = TIMEOUT` + ### Requirement: Соединение с NATS по шаблону, без JetStream Ядро SHALL подключаться к NATS с nkeys-seed и JWT, если заданы `OPENBAO__NATS__NATS_SEED/NATS_JWT`, и без учётных данных, если они пусты; подписываться на subject ответа `NATS_RESULT_SUBJECT` (по умолчанию `files.tabular_parsed`) в queue group `arm-core`; переподключаться автоматически. diff --git a/openspec/specs/reference-data-seed/spec.md b/openspec/specs/reference-data-seed/spec.md index 084375d..307c454 100644 --- a/openspec/specs/reference-data-seed/spec.md +++ b/openspec/specs/reference-data-seed/spec.md @@ -9,7 +9,7 @@ dev-данные загружаются из файлов репозитория ### Requirement: Идемпотентный seed справочников продукта Команда `python -m scripts.seed` (из `api/`) SHALL загрузить из `api/seed/core/`: `regional_office` (Головной офис, `is_head_office = true`), `role` — 7 системных ролей ТЗ 8.1 (`KA_RF`, `KA_GO`, `RKA`, `CLIENT_MANAGER`, `CURATOR_GO`, -`EXPERT`, `ADMIN`, `is_system = true`), `permission` — 16 сочетаний объект × действие из §10 правил с кодами +`EXPERT`, `ADMIN`, `is_system = true`), `permission` — 17 сочетаний объект × действие из §10 правил с кодами `_` (`EXPORT` при совпадении), `role_permission` по матрице §10 (ASSUMPTION(OQ-16)), `chart_of_account`, `elimination_rule` + `elimination_rule_account` версии 1 для ББ (§4), `required_document_rule` для `BASIC` и `EXTENDED`. Записи SHALL вставляться или обновляться по натуральному ключу; удаление MUST NOT выполняться. @@ -18,11 +18,11 @@ dev-данные загружаются из файлов репозитория матрица файла SHALL загружаться целиком; в заполненную SHALL вставляться только пары, у которых роль или право вставлены этим же запуском seed (их администратор снять не мог), а остальные пары файла MUST NOT добавляться — право, снятое администратором, не возвращается, добавленное сверх файла не отзывается. Отчёт команды SHALL сообщать, что -матрица пропущена (или сколько добавлено пар новых ролей и прав). +матрица пропущена (или сколько добавлено пар новых ролей и прав). Право `QUALITY_METRICS_VIEW` (объект `QUALITY_METRICS`, действие `VIEW`) SHALL выдаваться всем ролям; на стенде, где матрица уже заполнена, пара добавляется только как пара впервые созданного права — для появления пункта меню нужен повторный `make seed`. #### Scenario: Первый запуск на пустой схеме - **WHEN** после `alembic upgrade head` выполняется `python -m scripts.seed` -- **THEN** в `role` 7 строк, в `permission` 16, в `role_permission` — число строк равно сумме прав по матрице §10, +- **THEN** в `role` 7 строк, в `permission` 17, в `role_permission` — число строк равно сумме прав по матрице §10, в `elimination_rule` 8 строк с `statement_type = balance`, `version_no = 1`, в `required_document_rule` для `BASIC` 10 строк (BALANCE, INCOME, OSV × 01, 58, 60, 62, 66, 67, 76, 90) и для `EXTENDED` 27 строк @@ -48,6 +48,10 @@ dev-данные загружаются из файлов репозитория - **WHEN** в `roles.yaml` изменено `name` роли `EXPERT` и seed запущен снова - **THEN** строка `role` с `code = EXPERT` обновлена, новая не создана +#### Scenario: Право метрик на заполненной матрице +- **WHEN** матрица заполнена, право `QUALITY_METRICS_VIEW` создаётся впервые и seed запущен +- **THEN** добавлены пары права со всеми ролями; ранее снятые администратором связи не возвращены + ### Requirement: Счета правил и чек-листа существуют в плане счетов Все коды счетов, на которые ссылаются `elimination_rules_v1.yaml` и `required_document_rules.yaml`, SHALL резолвиться в `chart_of_account.code` после seed; `elimination_rule_account.is_excluded = true` SHALL быть у исключений вида -- 2.47.2