272 lines
72 KiB
Markdown
272 lines
72 KiB
Markdown
# Дизайн: метрики качества первички (AURORA-1759)
|
||
|
||
## 1. Зачем и для кого
|
||
|
||
**Заказчик** (пользователи АРМ в банке) сам открывает экран в веб-интерфейсе и видит, какую работу снял АРМ: доля
|
||
документов, прошедших без участия аналитика или подтверждённых без правок, доля правленых, технические отказы — за
|
||
период, по ГК, по типу документа и по источнику. Цифры подтверждают эффект инициативы.
|
||
|
||
**Владелец проекта** использует те же цифры как аргумент о качестве сервиса разбора PDF (ИС Сканирование): если они
|
||
плохие — довод за переход на другой сервис. Поэтому каждая ошибка относится к источнику: **сервис разбора**, **наш
|
||
разбор** (нормализация `arm_statements`, маппинг ядра, tabular-parser) или **документ клиента** (контрольные цифры не
|
||
сходятся в оригинале). Без такого разделения цифра «30 % правят» против сервиса не работает.
|
||
|
||
Позже метрики переедут в отдельное решение на платформе банка (там точно есть Superset, хранилище неизвестно) — перенос
|
||
не должен требовать доработки ядра.
|
||
|
||
## 2. Цели, не-цели, анти-цели
|
||
|
||
**Цели**
|
||
- Факты качества по каждому документу ББ / ОФР / ОСВ, загруженному после выкладки, и по каждому техническому отказу.
|
||
- Отнесение каждой правки аналитика к источнику ошибки по сырому тексту сервиса.
|
||
- Экран: матрица «источник × причина» за период + динамика по месяцам; переход из ячейки в список документов и далее
|
||
в документ (правки, комментарии, сам файл).
|
||
- Факты пригодны для Superset как есть и для переноса в другое хранилище.
|
||
- Сохранить вход для будущего эталонного прогона: сырые ответы ИС и исходные xlsx, подтверждённый результат.
|
||
|
||
**Не-цели** — см. `proposal.md`, раздел Non-goals (в т. ч. ретро-метрики, время аналитика, видимость по филиалам, сам
|
||
эталонный прогон).
|
||
|
||
**Анти-цели** (провал, даже если работает)
|
||
- Цифры, которые нельзя проверить: каждая ячейка матрицы раскрывается до конкретных документов.
|
||
- Вина сервиса там, где ошиблись мы (или наоборот): правило атрибуции — механическое и записанное (§5), спорные случаи
|
||
уходят в «наш разбор», а не в «сервис».
|
||
- Лишняя работа аналитика: никаких отметок, анкет и кнопок ради метрик.
|
||
- Метрики, которые меняются задним числом: факт записан — не переписывается (append-only), поздние события — новые
|
||
факты.
|
||
- Связка с рабочими таблицами, из-за которой перенос на платформу требует переписать ядро.
|
||
|
||
## 3. Ограничения
|
||
|
||
- Инварианты `../CLAUDE.md`: первичка не мутируется; единственный писатель в БД — arm-core; производные не хранятся.
|
||
Факты — не производные консолидации, а журнал событий (как `audit_event` / `ex_snapshot`); метрики — чистая функция
|
||
от фактов, не хранятся.
|
||
- Поля — только из DBML: таблицы фактов, роль бакета, признак скана, объект права появляются в DBML v5.25 (change в
|
||
`../docs`) до кода.
|
||
- Нормализацию ББ/ОФР делают сервисы; в ядро приходит нормализованный `result.json`. «Сырой текст сервиса» в ядре — это
|
||
`financial_statement_value.source_text`, `financial_statement_line.line_code_text`,
|
||
`financial_statement_meta.header_source`, `osv_entry.row_value` (§11.5: `text` — всегда сырой текст источника).
|
||
Сырой JSON ИС лежит в S3 `raw/{job_id}.json`, в БД не зарегистрирован.
|
||
- Пересчёт после правки перезаписывает `needs_review`, `line_status`, статус документа и `document_issue`
|
||
(`relation`/`soft`) — поэтому состояние «как распознано» фиксируется фактом в момент приёма, до любых правок.
|
||
- Удаление файла физически удаляет строки документа (`primary_files.delete`) — факты не ссылаются на них FK.
|
||
- Сейчас у заказчика все работают под одной учётной записью (до РАИСА/SSO, AURORA-1404).
|
||
|
||
## 4. Факты
|
||
|
||
Отдельная схема PostgreSQL (имя — настройка, по умолчанию `dkb_metrics`), миграции — общей цепочкой Alembic. Таблицы
|
||
плоские: измерения — обычные столбцы, подробности — `jsonb` с версией схемы (`schema_version`). Без FK на рабочие
|
||
таблицы; идентификаторы документов, файлов, заданий, ЮЛ, ГК — просто `uuid`, имена ЮЛ и ГК копируются на момент факта.
|
||
Контракт столбцов и `payload` — `../docs/contracts/metrics/quality-facts.schema.json`. Записи только добавляются.
|
||
|
||
### 4.1 `quality_intake_fact` — документ «как распознано»
|
||
|
||
Пишется в транзакции маппинга (`result_mapping._map_candidate`, после `map_file`, **до** `auto_confirm`), по одному на
|
||
созданный документ ББ / ОФР / ОСВ (`DISCLOSURE` / `OTHER` — не пишутся).
|
||
|
||
Измерения: `document_id`, `file_id`, `job_id`, `attempt`, `legal_entity_id` + имя, `group_id` + имя (ГК ЮЛ на момент
|
||
факта; ЮЛ вне ГК — пусто), `document_type`, `source_kind` (`pdf` / `excel`), `is_scan` (только PDF), `service_name`,
|
||
`service_version`, `statements_version` (ББ/ОФР), `result_schema_version`, `report_date` (как распознано),
|
||
`intake_at`, `intake_status` (статус документа после маппинга: `RECOGNIZED` / `NEEDS_CHECK`), счётчики: строк,
|
||
значений, значений с `needs_review`, строк `unreadable` / `missing`, нарушений `relation` / `soft`, предупреждений
|
||
сервиса; `raw_s3_key` (PDF), `result_s3_key`, `upload_s3_key`.
|
||
|
||
`payload`: снимок «как распознано» — шапка (`header_source`, распознанные поля), строки (`ref`, `row_index`,
|
||
`line_code`, `line_code_text`, `line_status`, `needs_review`), значения (`line ref`, колонка, `amount`, `string_value`,
|
||
`source_text`, `confidence`, `is_recognized`, `needs_review`), нарушения и коды предупреждений. Для ОСВ — счётчики
|
||
строк и коды предупреждений `OSV_*`, без построчного снимка (правок ОСВ по суммам в ядре нет).
|
||
|
||
### 4.2 `quality_outcome_fact` — исход документа
|
||
|
||
Один на документ, пишется при первом из событий:
|
||
- **подтверждение** — `documents.confirm` (вручную) и `auto_confirm` (автоматически), в той же транзакции;
|
||
- **удаление до подтверждения** — `primary_files.delete`;
|
||
- **замена** — автозамена `FILE_REPLACE` (документ вытеснен новым файлом).
|
||
|
||
Измерения: `document_id` (связь с 4.1), `outcome` (`AUTO_CONFIRMED` / `CONFIRMED` / `DELETED` / `SUPERSEDED`),
|
||
`outcome_at`, `primary_cause` (§6), счётчики правок по источнику (§5): значения, коды, добавленные строки, шапка —
|
||
`service` / `ours` / `client`; `checks_accepted` (подтверждено с акцептом нарушенных проверок), число нарушений на
|
||
момент исхода, `report_date` подтверждённый, `confirmed_by` (пользователь; для агрегатов не используется).
|
||
|
||
`payload`: подтверждённый результат (то же строение, что снимок приёма) и список правок с отнесением — каждая правка:
|
||
что, было (сырой текст / распознано), стало, источник, правило §5, которое сработало.
|
||
|
||
Подтверждённую ОСВ можно править (правка сбрасывает подтверждение) — повторное подтверждение пишет **новый** факт
|
||
исхода; метрики берут последний.
|
||
|
||
Удаление файла с уже подтверждённым документом (автоподтверждённые при версии «в работе», OQ-157) и автозамена
|
||
подтверждённого документа пишут **новый** факт `DELETED_AFTER_CONFIRM` / `SUPERSEDED_AFTER_CONFIRM` с исходом, который
|
||
был до него. Метрики берут последний факт: такой документ уходит из своей ячейки в отдельный столбец «удалён / заменён
|
||
после подтверждения» (решение владельца 27.10.2026) — в доли не входит, но виден, и из него раскрывается список с
|
||
прежним исходом.
|
||
|
||
### 4.3 `quality_failure_fact` — технический отказ
|
||
|
||
Пишется при переходе задания распознавания в `error` / `timeout` / `dlq` (`recognition_exchange`) и при неповторяемой
|
||
ошибке маппинга (`MappingFatal`, файл → `ERROR`). Измерения: `job_id`, `file_id`, `attempt`, `legal_entity_id`,
|
||
`group_id`, `source_kind`, `is_scan`, `service_name`, `failure_kind` (`SERVICE_ERROR` / `TIMEOUT` / `NO_ACK` /
|
||
`MAPPING`), `error_code`, `failed_at`. Повторная обработка (`FILE_REPROCESS`) создаёт новое задание: отказ первой
|
||
попытки остаётся фактом, документы удачной попытки получают свои 4.1 / 4.2.
|
||
|
||
## 5. Отнесение правки к источнику
|
||
|
||
Правка — отличие подтверждённого результата от снимка приёма (4.1). Правило одно: **верный ответ есть в сыром тексте
|
||
сервиса — ошибка наша; нет — ошибка сервиса.** Для Excel сервис разбора — наш tabular-parser, поэтому у Excel любая
|
||
ошибка распознавания — «наш разбор».
|
||
|
||
| Что правили | «Наш разбор», если | Иначе |
|
||
|---|---|---|
|
||
| Сумма | цифры подтверждённой суммы (без знака) совпадают с цифрами сырого текста после замены букв-двойников §11.3 — сервис прочитал верно, а мы неверно разобрали (знак, скобки, разряды, мусор по краям) | сервис |
|
||
| Код строки | подтверждённый код есть среди кодов, извлекаемых из `line_code_text` (в т. ч. одна из частей склейки `1250\n1260`) | сервис |
|
||
| Добавленная строка (`row_index IS NULL`) | её код есть в наборе кодов сырого ответа (для PDF — четырёхзначные токены, стоящие отдельной строкой в тексте ячеек `raw/{job_id}.json`; набор извлекается при приёме, вне транзакции, рядом с чтением `result.json`, и кладётся в снимок 4.1; для Excel — всегда «наш разбор») | сервис |
|
||
| Шапка: период, ИНН, единицы, наименование | подтверждённое значение извлекается из `header_source[*].text` | сервис |
|
||
| Дата колонки | подтверждённая дата извлекается из подписи колонки (`label`) | сервис |
|
||
| Период / счёт ОСВ | всегда «наш разбор» (Excel) | — |
|
||
|
||
**Документ клиента** — не правка, а исход: подтверждён **вручную** с акцептом нарушенных **жёстких** проверок
|
||
(`checks_accepted`), и нарушенное соотношение не задето правками. Мягкие нарушения (в т. ч. «в пределах округления» и
|
||
межформенная 1370↔2400) не учитываются — это в основном шум округления, и подтверждение они не блокируют.
|
||
Автоподтверждение жёстких нарушений иметь не может. Если правки чинят часть нарушений, а остальные акцептованы — у
|
||
документа и правки, и «документ клиента».
|
||
|
||
Сравнение — чистая функция `domain/quality_attribution.py` (§14 `05-domain-rules.md`), без БД. Разбор сырого текста —
|
||
публичным API `arm_statements` (`fix_lookalikes`, `parse_amount`, `parse_code`, `parse_date`, `default_rules`), тем же
|
||
кодом, что разбирал сервис; сам пакет не меняется (и не зависит от того, слита ли AURORA-1677: имя модуля то же).
|
||
|
||
## 6. Причина документа (ячейка матрицы)
|
||
|
||
Каждый документ с исходом попадает ровно в одну ячейку — по первому сработавшему правилу:
|
||
|
||
1. исход `DELETED` / `SUPERSEDED` → «удалён / заменён до подтверждения»; `DELETED_AFTER_CONFIRM` /
|
||
`SUPERSEDED_AFTER_CONFIRM` → «удалён / заменён после подтверждения» (оба столбца в доли не входят);
|
||
2. есть правки «сервис» → **ошибка сервиса**;
|
||
3. есть правки «наш разбор» → **наш разбор**;
|
||
4. подтверждён с акцептом нарушений (§5) → **документ клиента**;
|
||
5. `AUTO_CONFIRMED` → **без участия**;
|
||
6. `CONFIRMED` без правок → **как есть** (распознано верно, но потребовалась проверка аналитика).
|
||
|
||
Документы без исхода — **в работе** (отдельный столбец, в доли не входит). Технические отказы — отдельный столбец из
|
||
4.3, считаются по заданиям. Все счётчики правок по источнику остаются в факте, поэтому глубже матрицы видно и
|
||
«сервис + наш разбор» в одном документе.
|
||
|
||
Знаменатель долей — документы с исходом `AUTO_CONFIRMED` / `CONFIRMED`. Технические отказы считаются **по файлам**
|
||
(документов у них нет, тип неизвестен): отдельный столбец со своей долей «отказавших файлов от загруженных файлов того
|
||
же источника»; фильтр по типу документа на него не действует (на экране — пометка). Рядом с матрицей — счётчик
|
||
«уникальных отчётов» (ЮЛ + подтверждённая отчётная дата + тип), чтобы было видно, сколько повторных загрузок; это
|
||
счётчик, а не режим матрицы.
|
||
|
||
## 7. Расчёт, API, экран
|
||
|
||
- **Расчёт** — чистая функция `domain/quality_metrics.py` от фактов: фильтры (период по дате приёма, ГК, ЮЛ, тип,
|
||
источник, скан/цифровой), матрица и ряд по месяцам. В БД не пишется.
|
||
- **API** (`/api/v1/quality-metrics`, право §8): матрица за период; ряд по месяцам по выбранной причине; список
|
||
документов ячейки (документ, ЮЛ, дата, причина, счётчики правок, ссылка на документ — если он ещё существует);
|
||
выгрузка фактов за период (CSV / JSON) для переноса.
|
||
- **Экран** «Качество первички» (`web/`, пункт меню по праву): фильтры, матрица «источник × причина», динамика по
|
||
месяцам, переход ячейка → список документов → документ (существующий экран проверки распознавания с правками и
|
||
комментариями). Точная раскладка — при реализации (владелец: «главное наличие данных»).
|
||
|
||
## 8. Доступ
|
||
|
||
Новый объект права `QUALITY_METRICS` в `permission_object`, право `QUALITY_METRICS_VIEW`; seed выдаёт его всем ролям.
|
||
Позже права сужаются данными (`role_permission`), без кода. Видимость по филиалу / ГК не применяется. Список
|
||
документов показывает `confirmed_by`, агрегаты — без фамилий.
|
||
|
||
Superset читает схему фактов отдельной ролью БД только на чтение (создаёт платформа; в репозитории — описание в
|
||
`03-architecture.md`).
|
||
|
||
## 9. Сырые ответы и признак скана
|
||
|
||
- **Сырой ответ ИС.** Ядро уже получает `raw_s3_key` в сообщении результата — регистрирует объект в `s3_object` с новой
|
||
ролью бакета `raw` и ссылкой из `recognition_job`. Удаление файла не ставит `expires_at` сырому ответу и `uploads/`,
|
||
на которые ссылаются факты: они нужны эталону. Срок хранения — вопрос платформы (OQ-179), фиксируется в
|
||
`07-open-questions.md`.
|
||
- **Скан / цифровой PDF** определяется при загрузке по наличию текстового слоя (шрифтов в PDF) и хранится в
|
||
`stored_file`. PDF со скрытым OCR-слоем считается цифровым — допущение, записывается в §14.
|
||
|
||
## 10. Что решено владельцем (27.10.2026)
|
||
|
||
- Метрики — для заказчика (эффект инициативы) и как аргумент о сервисе разбора PDF.
|
||
- Охват — PDF и Excel; разрезы — качество сервиса, нашего разбора, документа клиента, технические отказы.
|
||
- Атрибуция по сырому тексту сервиса + сохранить данные для эталонного прогона («оставляем»).
|
||
- Экран в веб-интерфейсе, заказчик открывает сам; матрица и динамика; раскладка — при реализации.
|
||
- Только данные после выкладки.
|
||
- Без счётчика «есть, но не разбирается».
|
||
- Время аналитика — собирать в будущем (визиты с привязкой к документу, без нагрузки на аналитика; фамилии — для
|
||
разбора, агрегаты — без них); в этом раунде — только технические метрики.
|
||
- Отдельное право; сейчас видят все.
|
||
- Документ, удалённый или заменённый после подтверждения, — в отдельный столбец «удалён / заменён после
|
||
подтверждения» (не остаётся в «без участия» и не считается ошибкой).
|
||
- Перенос на платформу без доработки ядра; там есть Superset.
|
||
|
||
## 11. Оставлено исполнителю
|
||
|
||
Имена таблиц и столбцов (в рамках DBML v5.25), формат `payload`, раскладка экрана, способ определения текстового слоя
|
||
PDF, точный вид выгрузки, разбиение на задачи.
|
||
|
||
## Decisions made without discussion
|
||
|
||
| Дата | Решение | Обоснование | Цена ошибки |
|
||
|---|---|---|---|
|
||
| 27.10.2026 | Сам эталонный прогон — не в этом change; здесь только сохранение входа (сырые ответы, xlsx, подтверждённый результат) | прогон требует адаптера шага 1 сервисов (он в scan-gateway / tabular-parser) и отдельного инструмента сравнения; без данных он невозможен, а данные копятся только с выкладки | прогон приедет позже; данные для него уже будут |
|
||
| 27.10.2026 | Правки шапки (период, ИНН, единицы, наименование) — ошибка распознавания, атрибуция по `header_source` | шапку тоже распознаёт сервис | неверная классификация части правок |
|
||
| 27.10.2026 | Документы без исхода — столбец «в работе», в доли не входят | неподтверждённый документ ещё не оценён | — |
|
||
| 27.10.2026 | Повторная обработка: отказ первой попытки — технический отказ; документ — по итоговой попытке | отказ был, но документ в итоге получен | двойной учёт файла в отказах и в исходах — виден по `file_id` |
|
||
| 27.10.2026 | Одна ячейка на документ по приоритету §6 (сервис > наш разбор > документ клиента > без участия > как есть) | матрица должна суммироваться в 100 %; приоритет «сервис» — потому что правку сервиса аналитик делает руками всегда | документ с ошибками обеих сторон виден только в «сервисе»; счётчики в факте сохраняют обе |
|
||
| 27.10.2026 | Спорное — в «наш разбор» (верный ответ есть в сыром тексте, напр. склейка `1250\n1260`) | анти-цель: не приписывать сервису наши недоработки | метрика сервиса выглядит лучше, чем есть |
|
||
| 27.10.2026 | Отдельная схема БД `dkb_metrics` под факты, плоские таблицы + `jsonb` | Superset подключается к схеме ролью только на чтение; перенос — одна схема с контрактом | лишняя сложность миграций со второй схемой |
|
||
| 27.10.2026 | Признак скана — по текстовому слою PDF в ядре при загрузке, без изменения scan-gateway | ИС такой признак не отдаёт; ядро видит файл при загрузке | PDF со скрытым OCR-слоем — «цифровой» |
|
||
| 27.10.2026 | Период метрик — по дате приёма результата | исход приходит позже, документ остаётся в своей когорте | недавние периоды долго «в работе» |
|
||
| 27.10.2026 | Право `QUALITY_METRICS_VIEW` всем ролям в seed | владелец: сейчас видят все, потом — отдельная роль | — |
|
||
| 27.10.2026 | «Документ клиента» — только ручное подтверждение с акцептом жёстких нарушений; мягкие и 1370↔2400 не учитываются (builder check) | мягкие — шум округления и не блокируют подтверждение; иначе автоподтверждённые уйдут из «без участия» | реальные дефекты отчётности в пределах мягких соотношений не видны |
|
||
| 27.10.2026 | Код добавленной строки ищется среди четырёхзначных токенов сырого ответа ИС, извлечённых при приёме, без переноса адаптера шага 1 в ядро (builder check) | адаптер живёт в scan-gateway; совпадение с суммой даёт «наш разбор» — по правилу «спорное — нам» | метрика сервиса чуть лучше реальной на добавленных строках |
|
||
| 27.10.2026 | Технические отказы — по файлам, своей долей; фильтр по типу документа на них не действует; «уникальные отчёты» — счётчик, не режим (builder check) | у отказавшего файла нет документов и типа; смешивать файлы и документы в одном знаменателе нельзя | доля отказов не складывается с долями матрицы в 100 % |
|
||
| 27.10.2026 | ОСВ — без построчного снимка, только статусы, предупреждения, отказы, правки периода/счёта | правок сумм ОСВ в ядре нет | при появлении правок ОСВ — доработка снимка |
|
||
| 05.11.2026 | Task 1: `api/scripts/dbml_parity.py` (схема таблицы — через явный allowlist `METRICS_TABLES`, не единственная константа `SCHEMA`), `api/scripts/check_dbml_vs_orm.py` (фиксирует `QUALITY_METRICS_SCHEMA=dkb_metrics` в окружении проверки, как уже делает для `DB_SCHEMA`) и `api/alembic/env.py` (`include_name` признаёт обе схемы) — доработаны под вторую схему; план задачи 1 предполагал, что `check_dbml_vs_orm.py` «скорее всего» не потребует правок | без этого `make dbml-check` безусловно падал бы на `quality_intake_fact: схема: ожидалось dkb / фактически dkb_metrics» для всех трёх таблиц фактов; это ровно тот структурный сюрприз, о котором бриф просил не обходить тихо, а чинить или сообщать — чинить оказалось дешевле и находится в рамках задачи (`make dbml-check` — приёмный критерий шага 7) | без правки вторая схема логически невозможна — любой PR с `dkb_metrics` падает на CI/проверке |
|
||
| 05.11.2026 | Три новых ORM-класса (`QualityIntakeFact/OutcomeFact/FailureFact`) — на ОБЩЕМ `Base`/`Base.metadata` (как и всё остальное), схема переопределена точечно через `__table_args__ = {"schema": METRICS_SCHEMA}`; второй `DeclarativeBase` НЕ заводился | ровно второй вариант, предложенный самим брифом («…или аналог существующего паттерна»); так `Base.metadata` остаётся единой точкой для `check_dbml_vs_orm.py`/`alembic/env.py`/`create_all` — отдельная metadata потребовала бы протащить её через все три места отдельно | при введении третьей схемы в будущем придётся повторить тот же приём, а не скопировать общий механизм |
|
||
| 05.11.2026 | Три новых enum-типа (`quality_outcome`, `quality_cause`, `quality_failure_kind`) — значения живут в схеме `SCHEMA` (`dkb`), НЕ в `METRICS_SCHEMA`, хотя используют их только таблицы `dkb_metrics` | `pg_enum()` (src/models/enums.py) без правок — единственный источник схемы enum'а; разводить enum'ы по двум схемам потребовало бы параметра схемы в фабрике без текущей пользы (Superset читает только таблицы, не типы) | таблица `dkb_metrics` зависит от типов в `dkb`; перенос схемы метрик на другой сервер БД без `dkb` невозможен без правки (сейчас design §10 этого не требует) |
|
||
| 05.11.2026 | Откат миграции 0012 НЕ удаляет схему `dkb_metrics` (оставляет её пустой), симметрично тому, что `SCHEMA` (`dkb`) никогда не удаляется в `env.py`/0001 | тест `test_upgrade_into_preprovisioned_schema_without_database_create` (api/tests/test_migrations.py) показал конкретный отказ: роль без `CREATE` на базу не могла бы создать `dkb_metrics` заново при повторном `upgrade head`, если бы платформа выдавала схему готовой, а откат её удалял; симметрия с `dkb` — тот же риск, то же решение | если отладочный откат до base оставляет пустую схему `dkb_metrics` — незначительный мусор, не ошибка |
|
||
| 05.11.2026 | В план (`docs/superpowers/plans/2026-11-05-quality-metrics-arm-core.md`) добавлена строка `**Ветка:**` (`feat/190-ocr-metrics`, worktree, issue, Jira) — без неё хук `require-plan.sh` блокировал любую правку в `api/src/**`, так как не находил имя ветки ни в одном файле `docs/superpowers/plans/*.md` | во всех остальных активных планах эта строка есть (сверено на `2026-10-28-corrections-table-ui.md`); её отсутствие — пропуск на этапе написания плана, не содержательное решение | минимальный — чисто метаданные плана |
|
||
| 06.11.2026 | Task 13: `web/src/api/types.ts` — `'QUALITY_METRICS'` дописан в конец union `PermissionObject` ОДНОЙ строкой (не многострочным форматом), итоговая строка ровно 140 символов — на пределе, но не сверх лимита ширины biome | сохраняет однострочный стиль, которым уже написана эта строка (и соседняя `PermissionAction`); многострочный вариант рассматривался как равноценная по типам альтернатива, выбор между ними непринципиален | нулевая — чисто форматирование, обе формы типобезопасно идентичны |
|
||
| 05.11.2026 | Коммит задачи 1 включает больше файлов, чем перечислено в брифе (`Makefile`, `README.md`, `api/.env.example`, `api/.env.test`, `api/alembic/env.py`, `api/scripts/check_dbml_vs_orm.py`, `api/scripts/dbml_parity.py`, `api/src/models/base.py`, `api/src/models/__init__.py`, восемь файлов `api/tests/test_*.py`) | все они требуют правки для согласованности версии v5.25 (имя DBML-файла в `Makefile`/README, `HEAD`-ревизия и допущение «всё в dkb» в тестах) или для поддержки второй схемы; список `git add` в брифе соответствовал более узкому ожидаемому дифу до того, как выяснилось, что `check_dbml_vs_orm.py` и тесты миграций не знают о второй схеме | коммит по списку брифа оставил бы `make test`/`make lint`/`make dbml-check` красными — хуже, чем более широкий, но согласованный коммит |
|
||
| 05.11.2026 | `api/contracts/quality-facts.schema.json` НЕ скопирован из `../docs/contracts/` в Task 1 (`test_contract.py::test_vendored_contracts_are_identical_to_docs` остаётся красным на этом файле) — копирование отложено на задачу, которая реально строит `payload` по этой схеме | файл есть в `../docs/contracts/metrics/` (сиблинг-репозиторий arm-ka-docs, ветка feat/202, PR docs #203 — НЕ слита; порядок merge: docs PR #203 сливается раньше PR arm-core; позже файл вендорен — R-16), а в `api/contracts/` копии нет и несовпадение чисто механическое; копия сейчас была бы мёртвой — Task 1 (схема/ORM/миграция) не читает и не пишет `payload`, валидация полезной нагрузки — логика, не схема БД, и не входит в её рамки | следующая задача может не заметить, что это отложено осознанно, и либо повторно принять то же решение не зная о нём, либо оставить контракт невендоренным навсегда Перекрыто R-16 (07.11.2026): контракт вендорен, `test_contract` зелёный. |
|
||
| 06.11.2026 | Task 7 (`CONFIRMED`): нет сырого текста для сравнения у правки суммы/кода/шапки (`source_text`/`line_code_text`/`header_source[field].text` = `None`) — атрибуция «наш разбор» (`ours`), не пропуск правки. Уточняет направление решения 27.10.2026 «Спорное — в наш разбор» на случай «данных для сравнения нет вообще» — это НЕ тот же случай, что пример `1250\n1260` в исходном решении (там текст есть, но двусмысленный); `attribute_*` (Task 3) типизированы на `str`, случай отсутствия текста не предусматривают. Этим же решением подтверждается (не меняется) уже принятое 27.10.2026 направление для добавленной строки PDF (строка «Код добавленной строки ищется среди четырёхзначных токенов…», та же дата) — черновая реализация задачи 7 свела этот случай к вызову `attribute_added_row(code, raw_line_codes=(), is_excel=False)`, который из-за пустого `raw_line_codes` всегда возвращает «сервис», что противоречило уже принятому там направлению «совпадение с суммой даёт наш разбор»; исправлено прямой атрибуцией «наш разбор» для PDF-добавленных строк без вызова `attribute_added_row` (сырых токенов всё равно нет, инфраструктура их извлечения не построена — тот же пробел, что в строке 27.10.2026) | анти-цель §2/§5 («вина сервиса там, где ошиблись мы; спорные случаи — в наш разбор, не в сервис») распространяется и на случай, когда данных для сравнения нет вовсе: отсутствие доказательства ошибки сервиса не есть доказательство ошибки сервиса | если отсутствие сырого текста окажется частым, а не редким краевым случаем, `OUR_ERROR` может быть систематически завышен, а `SERVICE_ERROR` — недосчитан; метрика ослабнет как рычаг давления на поставщика распознавания (прямая цель §1), хотя остаётся безопасным дефолтом по анти-цели §2 |
|
||
| 06.11.2026 | Task 8 (`DELETED`/`DELETED_AFTER_CONFIRM`): (1) документ в статусе, отличном от `CONFIRMED`/`RECOGNIZED`/`NEEDS_CHECK` (практически только `ERROR`), при удалении файла НЕ получает факт исхода — у него никогда не было содержательного состояния «как распознано» для фиксации (нет формы/строк, не проходил ни через `AUTO_CONFIRMED`, ни через intake-факт); (2) `issue_count_at_outcome` вычисляется как `counts.error + counts.info` из уже посчитанного агрегата `issue_counts_by_document` вместо отдельного запроса — тот же смысл «все нарушения документа без фильтра», что `len(issues)` в Task 6/7, дешевле на один запрос; (3) `payload` для DELETED*-фактов — единый минимальный вызов `build_outcome_payload(document, fixes=())` без дополнительных запросов к строкам/значениям формы или ОСВ (эти строки всё равно будут физически удалены этим же вызовом `delete`, повторный их снимок здесь экономически не оправдан — а в матрицу §6 эти факты и не попадают, доступны только как «удалён/заменён», drill-down уже видит прежний исход через более ранний intake/outcome факт того же `document_id`) | бриф задачи 8 явно делегировал все три пункта исполнителю («если неоднозначно — зафиксировать выбор, не угадывать»); исполнитель выбрал и задокументировал в докстрингах кода, перенесено сюда контроллером по правилу маркировки находок (решение агента без обсуждения с человеком — в design.md) | (1): при появлении у `ERROR`-документов содержательного состояния до удаления (гипотетически) — нужно будет расширить список статусов; (2)/(3): минимальный риск — агрегаты эквивалентны, а payload не используется для атрибуции (там `primary_cause=None`, вне матрицы) |
|
||
| 06.11.2026 | Task 10 (`quality_failure_fact`): `error_code` для отказов маппинга (`_fail`, `_register_failure`) — константа `MAPPING_FAILURE_ERROR_CODE = "MAPPING_FAILED"` (`result_mapping.py`), не свободный текст из `message`/описания исключения | `QualityFailureFact.error_code` не имеет свободнотекстового столбца-соседа для деталей отказа маппинга (в отличие от `recognition_job.error_message`); деталь остаётся в `metrics.mapping_error`/логе, код — единая константа, отличимая от кодов обмена (`ERROR_NO_ACK`/`ERROR_INVALID_REQUEST`), что проверяет отдельный тест | если деталь отказа маппинга понадобится в матрице причин без обращения к логам — нужен отдельный столбец/payload-поле, не замена константы |
|
||
| 06.11.2026 | Task 9 (`SUPERSEDED`/`SUPERSEDED_AFTER_CONFIRM`): документ в статусе `ERROR` НЕ исключён из факта-исхода при автозамене — `outcome = SUPERSEDED_AFTER_CONFIRM if status is CONFIRMED else SUPERSEDED`, без третьей ветки, в отличие от Task 8 (`delete`), где `ERROR`/прочие статусы факт не получают вовсе. Выбор противоположен Task 8-й строке выше, но осознанно: подтверждён построчной проверкой всех мест `Document.status = ...` в `src/` (`documents.py`, `osv_header.py`, `auto_confirm.py`, `domain/osv_mapping.py`) — `DocumentStatus.ERROR` НИГДЕ не присваивается документу в продакшн-коде сегодня, различие с Task 8 необнаружимо на практике | бриф задачи 9 дал буквальную формулу (`CONFIRMED` → `..._AFTER_CONFIRM`, иначе → без суффикса) без явного исключения `ERROR`; поскольку путь недостижим, более простой код (без третьей ветки) предпочтён усложнению ради симметрии с Task 8 | если в будущем появится код, присваивающий `Document.status = ERROR`, `delete()` и `supersede_previous()` разойдутся в поведении (один пропустит факт, другой запишет `SUPERSEDED`) без того, чтобы это было осознанным решением на тот момент — стоит перепроверить эту строку, если `ERROR`-статус когда-нибудь станет достижим |
|
||
| 07.11.2026 | Task 14 (`compute_matrix`): по `document_id` берётся последний факт по `outcome_at`; при равенстве `outcome_at` — `*_AFTER_CONFIRM` > `CONFIRMED`/`AUTO_CONFIRMED` > `DELETED`/`SUPERSEDED`, затем поздний во входе | результат не должен зависеть от порядка строк SELECT; design §4.2 — метрики берут последний факт | при редком совпадении времени документ посчитается подтверждённым вместо удалённого или наоборот |
|
||
| 07.11.2026 | Task 14: два столбца вне долей — `removed_before_confirm` (DELETED, SUPERSEDED) и `removed_after_confirm` (оба `*_AFTER_CONFIRM`), без разбивки на удалён/заменён; доли — float, при нулевом знаменателе 0.0 (не None); исход из знаменателя с `primary_cause=None` и неизвестный `outcome` — `ValueError`, без молчаливого пропуска | design §6 п.1 называет два столбца; битый факт или новый enum не должен тихо искажать матрицу | разбивку придётся добавлять позже; битый факт даёт 500 на экране вместо искажённой матрицы; фронт отличает 0% от «нет данных» только по `denominator` |
|
||
| 07.11.2026 | Task 14 (R-8): ряд по месяцам идёт по месяцу даты приёма (`received_at` = `quality_intake_fact.intake_at`), а не по `outcome_at`; поле добавлено во входную строку `QualityOutcomeFactRow`, Task 15 подставляет его join'ом intake↔outcome по `document_id`; пропущенные месяцы заполняются нулями | design §7 и решение 27.10: период по дате приёма (когорта) | документ попадёт в месяц исхода, а не приёма, метрики недавних периодов исказятся |
|
||
| 07.11.2026 | Task 15 (API): последний факт исхода на документ — `DISTINCT ON (document_id) ORDER BY document_id, outcome_at DESC, ранг DESC, id DESC` во всех запросах; ранг (`*_AFTER_CONFIRM` 2, подтверждение 1, остальное 0) совпадает с tie-break домена | SQL и домен должны выбирать один и тот же факт | расхождение выбора даёт разные числа в матрице и в списке ячейки |
|
||
| 07.11.2026 | Task 15: выборка идёт от фактов приёма (LEFT JOIN последнего исхода); исход без факта приёма в метрики не входит; границы периода включительные по UTC (`date_to` отсекается в 00:00 UTC следующего дня) | у исхода без приёма нет даты приёма (когорта); UTC — единая зона БД | для пользователей в МСК граница периода сдвинута на 3 часа |
|
||
| 07.11.2026 | Task 15: технические отказы — поле `technical_failures {failed_files_count, total_files_count, share}`, период по `failed_at`, `total_files_count` — уникальные `file_id` из приёма и отказов; «в работе» (`in_progress_count`) и `unique_reports_count` (ЮЛ + отчётная дата + тип) — отдельные поля; в ответе отдельно `superseded_after_confirm_count`, `deleted_after_confirm_count` | design §6, решение 27.10 про отказы по файлам | доля отказов считается от другого знаменателя, чем доли причин |
|
||
| 07.11.2026 | Task 15: ячейки списка документов — `{cell}`: пять причин, `REMOVED_BEFORE_CONFIRM`, `REMOVED_AFTER_CONFIRM`, `IN_PROGRESS`; в списке `confirmed_by_id/name`, `document_link` (null, если документа нет); экспорт — CSV (UTF-8 с BOM, запятая, значения с формулой экранируются) и JSON, включает документы «в работе», аудит `EXPORT` пишется (см. R-12; исходное «нет в `audit_action`» было неверно) | design §7/§8; ячейки шире причин | Excel может показать BOM в чужих импортёрах |
|
||
| 07.11.2026 | Task 15: матрица читает факты периода в Python (8 лёгких колонок), подзапрос последних исходов не ограничен периодом, выгрузка целиком в памяти без стриминга и лимита (T15.2, T15.3, R-9) | первая версия, домен остаётся эталоном; без Docker производительность не измерить | на десятках тысяч документов запросы замедлятся — перенести агрегацию в GROUP BY |
|
||
| 07.11.2026 | Task 16 (R-10): на экране нет фильтра по ЮЛ (есть по ГК, периоду, типу, источнику, скану); API параметр `legal_entity_id` поддерживает | нужен отдельный пикер ЮЛ (тысячи записей); владелец: «главное наличие данных» | в UI нельзя посмотреть качество по одному ЮЛ — только через API/выгрузку |
|
||
| 07.11.2026 | Task 16 (R-11): матрица «источник × причина» — три параллельных запроса (`pipeline=image`, `pipeline=tabular`, без `pipeline`), колонки «PDF / Excel / Всего»; при выбранном источнике — одна колонка; «Всего» берётся с бэка, не суммой колонок | design §2/§7 требуют сравнения PDF и Excel; бэкенд отдаёт источник только фильтром | три запроса вместо одного на каждую смену фильтров |
|
||
| 07.11.2026 | Task 16: «Правки: сервис / наш разбор» в списке документов — суммы полей `*_service` и `*_ours`, `osv_period_account` не входит | в задаче названы только эти два семейства | правки периода и счёта ОСВ в колонке не видны |
|
||
| 07.11.2026 | Task 16: `Sidebar` снова фильтрует пункты по `permission` (поле было в типе, но не применялось после слияния репозиториев); пункт «Пользователи и роли» скрыт без `USERS_ADMIN`; `web/CLAUDE.md` исправлен | это восстановление прежнего поведения (коммит d3c2f8a); сервер и раньше отдавал 403 на эти страницы | у ролей без `USERS_ADMIN` пункт меню пропадает (раньше вёл на экран 403) |
|
||
| 07.11.2026 | Task 16: тесты фронта — на чистые функции (`qualityView.ts`), не на рендер: в проекте нет jsdom/testing-library; экран в браузере не проверялся (нет стенда); экспорт — через `apiFetchBlob` (токен), `getExportUrl` из брифа не создан | нет инфраструктуры рендер-тестов; обычная ссылка не передаёт токен | раскладка, Drawer и скачивание проверены только чтением кода — обязательна ручная проверка на стенде до merge |
|
||
| 07.11.2026 | Выкладка: пункт меню «Качество первички» виден только после `make seed` на стенде (seed добавляет пары только для впервые созданного права) | поведение seed | без seed меню скрыто у всех, API отвечает 403 |
|
||
| 07.11.2026 | R-12 (final review M-2): `GET /quality-metrics/export` пишет `audit_event` (`action=EXPORT`, `section=EXPORT`, `object_type=quality_metrics`, `object_id=null`, `after_value={report, format, filters, rows}`) в транзакции запроса; `AuditAction.EXPORT` существует (как у экспорта сводной ОСВ, `consolidated_osv.py`); права — прежние `QUALITY_METRICS_VIEW` | выгрузка содержит названия ЮЛ и массовая; обоснование «нет в `audit_action`» в Task 15 было ошибочным | без записи выгрузка не оставляла следа; при записи в аудит каждый экспорт добавляет строку журнала |
|
||
| 07.11.2026 | R-13 (final review I-3, рулинг владельца): `stored_file.is_scan` и `recognition_job.raw_s3_object_id` — последние колонки таблиц, и в ORM, и в DBML v5.25 (docs); версия DBML не меняется. Тест миграции 0006 проверяет «последняя колонка» на ревизии `0006_dbml_v5_18`, а не на head | миграция 0012 добавляет колонки `ADD COLUMN` (в конец), `test_migrated_schema_equals_orm_create_all` и 0006-тест требуют порядок ORM = порядок миграции; прецедент v5.18 `consolidation_id` | порядок колонок в DBML v5.25 не совпадает с черновиками аналитики; смена порядка в ORM требует правки docs PR #203 (merge вместе) |
|
||
| 07.11.2026 | R-14 (final review I-4): при удалении файла, у которого есть `quality_intake_fact` с `file_id`, объект `uploads` файла не получает `expires_at` (исключён из `object_ids` и из префикса `uploads/{file_id}/`); определение — один запрос `has_intake_fact_for_file` по `file_id`, а не сравнение `upload_s3_key` | spec quality-metrics-model: объект нужен эталонному прогону (OQ-179); сравнение по `file_id` дешевле и покрывает все факты файла; объект `raw` в `object_ids` не входит и раньше | если у файла несколько документов и факт есть не у всех, объект сохраняется целиком (бакет растёт на размер файла); если `file_id` в факте ошибочен — объект экспирится и эталон потеряется |
|
||
| 07.11.2026 | R-15 (final review M-3): `has_text_layer` вызывается через `asyncio.to_thread` и разбирает не больше `MAX_PAGES_CHECKED = 10` первых страниц | pypdf синхронен и работал в event loop под advisory-lock загрузки; текстовый слой, если есть, виден на первых страницах формы отчётности (ББ/ОФР — 1-3 листа), 10 — запас | PDF, у которого текст только после 10-й страницы (титулы-сканы), будет помечен `is_scan = true` — метрика «скан» чуть завысит долю сканов |
|
||
| 07.11.2026 | R-16 (final review I-2, рулинг владельца-сессии): Контракт `quality-facts.schema.json` перенесён в docs из `docs/contracts/` в `docs/contracts/metrics/` (git mv, коммит в docs-ветке feat/202); копия в arm-core — `api/contracts/metrics/quality-facts.schema.json` (cp, байт-в-байт); ссылки в docs (DBML-заметки, design/proposal/spec/план change) и arm-core обновлены. `contract_identity.py` НЕ менялся | для пути глубины 2 не из `examples/` `_is_required_in_copy` копию не требует, а `docs_pair` сверяет копию arm-core по тому же относительному пути; `tabular-parser` и `scan-gateway` остаются зелёными без копии (проверено в arm-core: `test_contract*` 296 passed с копией). Схема не привязана к NATS-контракту распознавания, подкаталог это подчёркивает | если docs PR #203 сольётся без переноса, тесты идентичности в двух сервисах покраснеют на `quality-facts.schema.json` в корне; обратный перенос потребует правки ссылок в обоих репозиториях |
|
||
| 07.11.2026 | R-17 (I-2): Payload `values[]` по контракту: `line_id`/`column_id` → `line_ref` (= `ref` строки в `lines[]`; нет строки в снимке — `str(line_id)`) и `column` (= `str(column_id)`); `confidence` — числом (`float(Decimal)`, не деньги); `lines[].ref` = `source_ref`, при `null` — `str(id)`; `Decimal` сериализуется `format(d, 'f')` (без экспоненты, как требует `$defs/money`) | схема требует `line_ref`/`column`/`ref` строкой и `confidence` number; `column` — идентификатор колонки, а не её подпись/индекс: у значения есть только `column_id`, подпись требовала бы дополнительной выборки колонок в каждом хуке | `column` — UUID, а не читаемая дата колонки: аналитик по payload колонку не опознаёт без join; смена на индекс колонки потребует передавать колонки во все хуки |
|
||
| 07.11.2026 | R-18 (I-2): Запись `fixes[]` по контракту: `kind`→`field`, `attribution`→`source`, новые `rule` (строка таблицы §14.2: «Сумма», «Код строки», «Добавленная строка», «Шапка», «Период / счёт ОСВ»), `before_raw` (сырой текст сервиса: `source_text` / `line_code_text` / `header_source[field].text`), `before_recognized` (бывшее `before`), `line_id`→`line_ref` (для значения ещё `column`), для шапки поле — `header_field` (в `field` — `header`, как требует enum). `after` у ОСВ — `{period_year, period_quarter, osv_account_code}` | схема требует `field`/`after`/`source`/`rule` и допускает дополнительные свойства: `line_ref`, `column`, `header_field` — для разбора правки без join; `after` у `osv_period_account` обязателен, а «значение до» в `document` не хранится (`before_*` = null) | `header_field` и `line_ref`/`column` вне обязательных свойств схемы — потребитель не может на них полагаться как на контракт; у ОСВ нет «до» значения периода/счёта |
|
||
| 07.11.2026 | R-19 (I-2): Схема контракта НЕ менялась | все поля контракта заполняются; `raw_line_codes` необязателен и по-прежнему не пишется (R-5b, deferred) | — |
|
||
| 07.11.2026 | R-20 (I-2): Тест валидации: хелпер `api/tests/quality_payload_contract.py` (jsonschema Draft 2020-12, `$defs/intake-payload` и `$defs/outcome-payload`); хелперы чтения фактов в интеграционных тестах (`_outcome_fact`, `_intake_fact` в test_api_documents/test_api_files_delete/test_result_mapping) валидируют каждый прочитанный payload, test_auto_confirm — явно. Покрыты: приём ББ/ОФР и ОСВ, AUTO_CONFIRMED (форма и ОСВ), CONFIRMED с правками суммы/кода/шапки/ОСВ, DELETED(_AFTER_CONFIRM), SUPERSEDED(_AFTER_CONFIRM) | payload строится реальными сервисными функциями на PostgreSQL, не ручными литералами; `jsonschema` уже в `requirements-dev.txt` | валидация — в тестах, не в рантайме: выход payload за контракт в проде не отсекается |
|
||
| 07.11.2026 | R-21 (I-5): Правка отчётной даты шапки (`report_date`): сырой текст `header_source['report_date'].text` разбирается `parse_date` пакета `arm_statements` (после `fix_lookalikes`, `default_rules()`), даты сравниваются как даты (`attribute_header_date_fix`): равны — «наш разбор», различаются — «сервис», текст не разобрался — «наш разбор». Прочие поля шапки — прежнее сравнение подстрокой | ISO `2025-03-31` как подстрока `31.03.2025` не находился → почти любая правка даты шла в «сервис», вопреки анти-цели §2 («спорное — в наш разбор»); §14.2 и design §5 называют `parse_date` | если `parse_date` ошибётся в формате, дата различится — правка уйдёт в «сервис» (завышение доли ошибок сервиса); правка «Дата колонки» по-прежнему не учитывается (R-5a) |
|
||
| 06.11.2026 | R-4 (Task 7): атрибуция правок суммы/кода/шапки читает ЖИВЫЕ колонки (`is_modified`, `original_amount`, `source_text`, `line_code_modified`, `line_code_original_value`, `*_modified`/`*_original_value` шапки), а не JSON `quality_intake_fact.payload`, как буквально предлагал план; в `documents.py` docstring ссылается на «отчёт задачи 7» | `source_text` неизменяем (инвариант «первичка не мутируется»), поэтому живые колонки и снимок приёма равны по содержанию; типизированные колонки проще и надёжнее, чем разбор чужого JSON; функции `attribute_*` принимают строки и источник-агностичны | если понадобится сравнение по payload приёма — сменить источник в одном месте (`_write_confirm_outcome_fact`); docstring ссылается на файл отчёта, который удаляется вместе с SDD-workspace |
|
||
| 06.11.2026 | R-5a (Task 7): правка «Дата колонки» (§14.2) НЕ учитывается в счётчиках атрибуции | у `financial_statement_column` нет `*_modified`/`original_*`; правки дат колонок есть лишь в общем механизме аудита `services/statements.py` без чистого запроса «менялась ли дата колонки»; нужен новый столбец (схема) — вне объёма change | правки дат колонок в метриках не видны (занижены правки «наш разбор»/«сервис»); deferred, в Jira при закрытии (M-8) |
|
||
| 06.11.2026 | R-5b (Task 7): набор четырёхзначных кодов сырого ответа ИС (`raw_line_codes`, design §5) при приёме не извлекается и в снимок не кладётся; поле контракта необязательно и не пишется | задачи 2/5 извлечение не реализовали; адаптер шага 1 живёт в scan-gateway; извлечение вне объёма change | атрибуция добавленных строк PDF неполна; направление — см. R-6; deferred, в Jira (M-8) |
|
||
| 06.11.2026 | R-6 (Task 7, меняет направление R-5b): добавленная строка PDF при отсутствии набора кодов относится к «нашему разбору» (прямая атрибуция без вызова `attribute_added_row` с пустым набором) | пустой набор давал бы «сервис» всегда, вопреки уже принятому решению 27.10 («спорное — в наш разбор», строка про код добавленной строки) и анти-цели §2 | метрика сервиса чуть лучше реальной на добавленных строках (стоимость уже заложена в записи 27.10) |
|
||
| 06.11.2026 | R-7 (Task 7, сведён I-2): несоответствие формы `payload.fixes`/`values` контракту `quality-facts.schema.json` было отложено и вынесено в финальное ревью | ни одна задача плана не владела приведением payload к контракту, контракт не был вендорен, тестов валидации не было | закрыто в I-2: R-16…R-21 (вендоринг, приведение payload, тест валидации) |
|
||
| 07.11.2026 | R-22 (final review M-7): условие §14.4 «нарушенное соотношение не задето правками» НЕ проверяется: `checks_accepted = meta.checks_accepted_by is not None` (`documents._write_confirm_outcome_fact`), то есть любой акцепт нарушений при ручном подтверждении | на ячейку матрицы не влияет: при любой правке (`service`/`ours`) приоритет §6 п.2-3 выше «документа клиента», а без правок задетое правками соотношение невозможно; влияет только на поле `checks_accepted` в факте и на «документ клиента» у документа без правок — где условие выполняется тривиально. Точная проверка требует пересчёта соотношений до/после правок | если акцепт дан, а часть соотношений исправлена правками, поле `checks_accepted` у такого факта завышено; ячейка остаётся «сервис»/«наш разбор» |
|
||
| 07.11.2026 | R-23 (final review I-7): дельты OpenSpec — ADDED `quality-metrics`, `quality-metrics-ui`; MODIFIED `primary-files` (загрузка, удаление), `recognition-job-exchange` (inbox, таймаут), `documents-api` (подтверждение), `osv-result-mapping` (цикл маппинга, автоподтверждение), `authn-authz` (`GET /me`), `reference-data-seed` (seed, 16 → 17 прав) | `openspec validate --strict` требует полного текста изменяемого требования, поэтому MODIFIED копируют текущее требование из `openspec/specs/` с добавлением; авторизационная спека не перечисляет права, поэтому право отражено сценарием `/me` | при архивации дельты заменят требования целиком — правки спек основной ветки, сделанные параллельно, потеряются (проверить конфликт перед `openspec archive`) |
|
||
| 05.11.2026 | R-1 (Task 3): имена экспорта `arm_statements` в плане (`fix_lookalikes`/`parse_amount`/`parse_code`/`parse_date`/`default_rules`) предварительные; исполнитель задачи 3 сверяет их с реальным `dir(arm_statements)` до написания `domain/quality_attribution.py` и фиксирует расхождения строкой в ledger | это собственная инструкция плана, в рулинг вынесена, чтобы попала в бриф задачи | если имена не совпали, а шаг пропущен — `quality_attribution.py` не импортируется, ловится красным тестом сразу, правка дешёвая |
|
||
| 06.11.2026 | R-2 (Task 11): регистрация сырого объекта в `recognition_exchange._apply_result` не зависит от `message.status`: срабатывает при `message.raw_s3_key is not None`, независимо от проверки `status is ERROR` для факта отказа (Task 10); оба хука живут в одной функции на разных условиях | план задачи 11 уже говорит «если `message.raw_s3_key is not None`» без условия по статусу; design §9: сырой ответ нужен эталонному прогону при любом исходе | если регистрация должна быть условной, упавшее задание получит лишнюю строку `s3_object` на полезный сырой ответ — дёшево и, скорее всего, правильно |
|
||
| 06.11.2026 | R-3 (Task 2): сигнатуры `build_intake_payload` / `build_outcome_payload` расширены необязательными `header_source: Mapping \| None` (кладётся в `header` для ББ/ОФР) и `osv_row_count: int \| None` (ключ `row_count` в ветке ОСВ) | исполнитель задачи 2 заметил, что бриф «Consumes» называет `FinancialStatementMeta`, а сигнатура в «Produces» параметра для него не давала; берётся существующее поле ORM `header_source`, новой формы не изобретали; параметры необязательные — прежние тесты не менялись | если реальному хуку задачи 5 нужна иная форма, сигнатуру правят позже — локальная и дешёвая правка |
|
||
| 07.11.2026 | R-24 (повторное ревью I-9): в схеме `quality-facts.schema.json` поле `intake-payload/values/items/is_recognized` — `["boolean", "null"]` (docs, пока PR #203 не слит; копия `api/contracts/metrics/` байт-в-байт). Остальные поля сверены скриптом по моделям: NULL допускают только `is_recognized` (правка аналитика `add_value`, пустая ячейка источника при приёме) и уже допускающие null поля | `FinancialStatementValue.is_recognized` — `bool \| None`; NULL означает «не из источника», а не «не распознана» (P3-54), поэтому тип поля менять не стали, а ослабили контракт; тесты: unit-валидация NULL-ячейки и `CONFIRMED` с ячейкой аналитика | потребитель фактов обязан обрабатывать `null` в `is_recognized`; ужесточение схемы позже потребует миграции данных |
|