From 8abbf50dd3dedb00847db12a30983889818ca544 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Thu, 5 Nov 2026 15:02:48 +0400 Subject: [PATCH] =?UTF-8?q?docs(#202):=20=D0=BF=D1=80=D0=B0=D0=B2=D0=BA?= =?UTF-8?q?=D0=B8=20=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=20=D0=B2=D0=B5=D1=82?= =?UTF-8?q?=D0=BA=D0=B8=20=E2=80=94=20is=5Fscan,=20=D0=9E=D0=A1=D0=92=20?= =?UTF-8?q?=D0=B1=D0=B5=D0=B7=20lines/values,=20retention=20raw/uploads?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Критично: stored_file.is_scan был описан наоборот (true = цифровой вместо true = скан), противоречил §14.6 и собственному имени столбца. Важно: §14 не говорил явно, что ОСВ не получает lines/values в payload (теперь dependentSchemas в контракте это ещё и проверяет); §14.3 не говорил, что quality_outcome_fact.document_id не unique и метрики берут последний факт; амount в контракте был float вместо decimal-строки (как money в recognition-result.schema.json); правка "дата колонки" не имела счётчика — явно отнесена к fixes_header_*; правило хранения S3 при удалении файла (OQ-179) не было согласовано между domain-rules, architecture и spec.md (spec.md ошибочно утверждал, что uploads экспирится в отличие от raw — не экспирится ни то, ни другое, если на них ссылается факт). Co-Authored-By: Claude Sonnet 5 --- context/03-architecture.md | 4 +++- context/05-domain-rules.md | 14 +++++++++++++- contracts/quality-facts.schema.json | 16 +++++++++++++--- model/data-model-v5.25.dbml | 2 +- .../specs/quality-metrics-model/spec.md | 11 +++++++---- 5 files changed, 37 insertions(+), 10 deletions(-) diff --git a/context/03-architecture.md b/context/03-architecture.md index ed40cc1..4640399 100644 --- a/context/03-architecture.md +++ b/context/03-architecture.md @@ -223,7 +223,9 @@ sha256, size, expires_at). Lifecycle-правила — по префиксам. известен до записи в БД; имя очищается от путей и управляющих символов); `results/{file_id}/{job_id}/…` пишет сервис распознавания, а ядро перед чтением проверяет, что ключ лежит именно в папке задания (OQ-70). При удалении файла объектам `uploads` и `results` проставляется `s3_object.expires_at = now()`, физически они не стираются (ретеншн — OQ-111, ждёт -lifecycle-правил платформы); объект, записанный в S3 до сбоя БД, остаётся сиротой (то же правило). +lifecycle-правил платформы); объект, записанный в S3 до сбоя БД, остаётся сиротой (то же правило). **v5.25:** исключение — +объект роли `raw` и объект `uploads`, на который ссылается `quality_intake_fact`: `expires_at` им не ставится вовсе +(вход для эталонного прогона метрик качества, срок — OQ-179). ## Аутентификация и авторизация diff --git a/context/05-domain-rules.md b/context/05-domain-rules.md index 73cfd14..f350299 100644 --- a/context/05-domain-rules.md +++ b/context/05-domain-rules.md @@ -1192,7 +1192,7 @@ attempt+1») устарела — исправить в следующей ве как у ОСВ (§3 п. 1, §9.1): ББ/ОФР за квартал версии (`Q1` — только версии за `Q1`, R-14a Среза 4). 5. **Удаление** (`DELETE /files/{id}`, `DOCUMENT_EDIT`): запрещено, если у файла есть документ, подтверждённый пользователем (`confirmed_by` не NULL), или автоподтверждённый документ привязан к версии вне `INITIATED`/`DOCUMENTS_PROCESSING`/`DATA_CORRECTION` (`FILE_HAS_CONFIRMED_DOCUMENTS`; OQ-157, уточняет OQ-110); привязки автоподтверждённых документов снимаются, статус версий не меняется, `FILE_DELETE.after_value.unbound_version_ids`; запрещено также, если файл заменил другие (`FILE_SUPERSEDES_OTHERS`); `SUPERSEDED` файл удалить можно. Удаляются строки, документы (вместе с данными форм `financial_statement_*` и комментариями документов), задания и `stored_file`; объектам S3 ставится `expires_at = now()` (физически не - стираются — OQ-111); аудит `FILE_DELETE` с карточкой файла в `before_value`. **OQ-110 закрыт для Среза 4:** к версии + стираются — OQ-111) — **кроме** объекта роли `raw` и объекта `uploads`, на который ссылается `quality_intake_fact` (`upload_s3_key`): им `expires_at` не ставится вовсе, срок — OQ-179 (§14); аудит `FILE_DELETE` с карточкой файла в `before_value`. **OQ-110 закрыт для Среза 4:** к версии привязываются только `CONFIRMED` документы, а файл с таким документом удалить нельзя — удалить файл с документом в версии невозможно; расширять удаление не требуется (уточнено OQ-157: для автоподтверждённых документов — см. выше). 6. **Аудит.** `FILE_UPLOAD`, `FILE_REPLACE`, `FILE_DELETE`, `FILE_REPROCESS` (п. 13, v5.21), `DOCUMENT_CONFIRM` (раздел @@ -2086,6 +2086,10 @@ Excel — §11.2, раскладка — §11.3, строки — §11.4, зна Факт `quality_intake_fact`/`quality_outcome_fact` пишется на документ типа `BALANCE`, `INCOME`, `OSV`. `DISCLOSURE`/`OTHER` факт не получают (это расшифровки и прочие документы — не форма с правилами подсчёта). +Для `OSV` `payload` обоих фактов **не содержит `lines`/`values`** — построчного снимка нет (правок сумм ОСВ в ядре +нет, §14.2 для ОСВ не применяется, кроме строки «Период / счёт ОСВ»); вместо них — счётчики строк и предупреждения +`OSV_*` (`osv_warnings`). Для `BALANCE`/`INCOME` `payload` содержит `lines`/`values` построчно. + ### §14.2. Атрибуция правки (сервис / наш разбор) Правка — отличие подтверждённого результата от снимка приёма (`quality_intake_fact.payload`). Правило одно: **верный @@ -2101,6 +2105,10 @@ tabular-parser, поэтому у Excel любая ошибка распозна | Дата колонки | подтверждённая дата извлекается из подписи колонки (`label`) | сервис | | Период / счёт ОСВ | всегда «наш разбор» (Excel) | — | +Правка «Дата колонки» считается вместе с «Шапкой»: обе пишутся как `fixes_header_service`/`fixes_header_ours` +(`quality_outcome_fact`) и как `fixes[].field = "header"` (`quality-facts.schema.json`) — отдельного счётчика для +колонки нет, это та же структурная область документа, что и шапка. + Сравнение — чистая функция (без БД), тем же кодом, что разбирал сервис (`arm_statements`: `fix_lookalikes`, `parse_amount`, `parse_code`, `parse_date`, `default_rules`). Только два источника правки — «сервис» и «наш разбор»; «документ клиента» (§14.4) — свойство исхода, не правки. @@ -2121,6 +2129,10 @@ tabular-parser, поэтому у Excel любая ошибка распозна Знаменатель долей — документы с `outcome ∈ {AUTO_CONFIRMED, CONFIRMED}`. Документы без исхода (нет `quality_outcome_fact`) — отдельный столбец «в работе», в доли не входит. +У документа может быть несколько фактов `quality_outcome_fact` во времени (переподтверждение ОСВ после правки, +исход `*_AFTER_CONFIRM` поверх прежнего, §1.5 модели — `document_id` там не unique). Правило этого параграфа +применяется к **последнему** факту по `outcome_at`; более ранние факты того же документа в матрицу не входят. + ### §14.4. «Документ клиента» Не правка, а исход: подтверждён **вручную** (не `AUTO_CONFIRMED`) с акцептом нарушенных **жёстких** проверок §7 diff --git a/contracts/quality-facts.schema.json b/contracts/quality-facts.schema.json index 6669344..ff63bcf 100644 --- a/contracts/quality-facts.schema.json +++ b/contracts/quality-facts.schema.json @@ -4,9 +4,19 @@ "title": "Payload фактов качества распознавания первички (AURORA-1759)", "description": "Структура jsonb-столбца payload таблиц quality_intake_fact и quality_outcome_fact (model/data-model-v5.25.dbml, context/05-domain-rules.md §14). quality_failure_fact не описан здесь — у него нет payload, все поля плоские. НЕ валидируется make check / scripts/validate_contracts.py: этот скрипт жёстко завязан на три сообщения NATS-контракта распознавания (request-/result-message-/result- примеры с дедупом и версией сообщения); payload фактов не ходит по NATS и не требует той же идемпотентности. Схему проверяет тест в arm-core, где payload формируется (см. openspec/changes/quality-metrics/design.md §2 этого репозитория).", "$defs": { + "money": { + "type": ["string", "null"], + "pattern": "^-?\\d+(\\.\\d{1,2})?$", + "description": "Decimal как строка (как contracts/recognition-result.schema.json#/$defs/money) — деньги не float" + }, "intake-payload": { "type": "object", - "description": "Снимок документа на момент приёма результата (до подтверждения). Для OSV — без lines/values, только osv_warnings.", + "description": "Снимок документа на момент приёма результата (до подтверждения). Для OSV — без lines/values, только osv_warnings (взаимоисключение — см. dependentSchemas ниже).", + "dependentSchemas": { + "osv_warnings": { + "not": { "anyOf": [{ "required": ["lines"] }, { "required": ["values"] }] } + } + }, "properties": { "header": { "type": "object", @@ -38,7 +48,7 @@ "properties": { "line_ref": { "type": "string" }, "column": { "type": "string" }, - "amount": { "type": ["number", "null"] }, + "amount": { "$ref": "#/$defs/money" }, "string_value": { "type": ["string", "null"] }, "source_text": { "type": ["string", "null"] }, "confidence": { "type": ["number", "null"] }, @@ -83,7 +93,7 @@ "type": "object", "required": ["field", "after", "source", "rule"], "properties": { - "field": { "enum": ["value", "code", "added_row", "header", "osv_period_account"] }, + "field": { "enum": ["value", "code", "added_row", "header", "osv_period_account"], "description": "header покрывает и правку даты колонки — §14.2 считает их вместе" }, "before_raw": { "type": ["string", "null"], "description": "сырой текст сервиса на этом поле, если был" }, "before_recognized": { "description": "как было распознано до правки" }, "after": { "description": "подтверждённое значение" }, diff --git a/model/data-model-v5.25.dbml b/model/data-model-v5.25.dbml index aae0cba..91c3ab1 100644 --- a/model/data-model-v5.25.dbml +++ b/model/data-model-v5.25.dbml @@ -897,7 +897,7 @@ Table stored_file { status file_status [not null] version_no int [not null, default: 1, note: 'ADD ТЗ 8.2: версия при замене'] superseded_by_file_id uuid [ref: > stored_file.id, note: 'ссылка на файл-замену; статус SUPERSEDED. Замена — файл ЦЕЛИКОМ со всеми документами в нём (решение 01.10)'] - is_scan boolean [note: 'v5.25: только PDF — признак текстового слоя при загрузке; true = цифровой (в т. ч. скрытый OCR-слой — допущение §14); NULL для не-PDF'] + is_scan boolean [note: 'v5.25: только PDF — признак скана по отсутствию текстового слоя при загрузке; true = скан (нет текстового слоя); false = цифровой, в т. ч. скрытый OCR-слой — допущение §14; NULL для не-PDF'] current_recognition_job_id uuid [ref: > recognition_job.id, note: 'авторитетный прогон; его результат породил актуальные document'] uploaded_by uuid [ref: > app_user.id] uploaded_at timestamptz diff --git a/openspec/changes/quality-metrics/specs/quality-metrics-model/spec.md b/openspec/changes/quality-metrics/specs/quality-metrics-model/spec.md index b78be97..9d23a8a 100644 --- a/openspec/changes/quality-metrics/specs/quality-metrics-model/spec.md +++ b/openspec/changes/quality-metrics/specs/quality-metrics-model/spec.md @@ -43,12 +43,15 @@ ### Requirement: Сырой ответ ИС Сканирование регистрируется и не экспирится с файлом `s3_object` SHALL поддерживать роль бакета `raw`; `recognition_job.raw_s3_object_id` SHALL ссылаться на -зарегистрированный объект для image pipeline. Удаление `stored_file` SHALL НЕ устанавливать `expires_at` объекту -с ролью `raw`. +зарегистрированный объект для image pipeline. Удаление `stored_file` SHALL НЕ устанавливать `expires_at` ни объекту +с ролью `raw`, ни объекту `uploads`, на который ссылается `quality_intake_fact` (`upload_s3_key`) — оба нужны +эталонному прогону (OQ-179). Объекты `results` и прочие `uploads` (без факта) — без изменений, `expires_at = now()` +при удалении файла, как и раньше. #### Scenario: Удаление файла с зарегистрированным сырым ответом -- **WHEN** PDF-файл с заполненным `recognition_job.raw_s3_object_id` удаляется -- **THEN** `s3_object.expires_at` объекта роли `raw` остаётся `NULL`, в отличие от объектов ролей `uploads`/`results` +- **WHEN** PDF-файл с заполненным `recognition_job.raw_s3_object_id` и существующим `quality_intake_fact` удаляется +- **THEN** `s3_object.expires_at` остаётся `NULL` у объекта роли `raw` и у объекта `uploads`, на который ссылается + факт; у объекта `results` того же файла `expires_at` проставляется `now()`, как для любого удаления того же файла ### Requirement: Контракт payload не входит в `make check` этого репозитория