## MODIFIED Requirements ### Requirement: Сообщение результата распознавания Тело сообщения на subject ответа (настройка окружения `NATS_RESULT_SUBJECT`, по умолчанию `files.tabular_parsed`) SHALL соответствовать `docs/contracts/recognition-result-message.schema.json` v2.0: обязательные поля `schema_version` (`"2.0"`), `job_id`, `file_id`, `status` (`processing` | `done` | `needs_check` | `error`), `service {name, version}`; при `status ∈ {done, needs_check}` обязательны `result_s3 {bucket, key}` и `result_schema_version`; при `status = error` обязателен `error {code, message}`; опциональны `raw_s3_key` (только `scan-gateway`) и `metrics`. `metrics.warnings[]` — структуры `warning` (как в файле результата), не строки. Поле `group_schema` в конверт NOT включается — источник `recognition_job.group_schema` — файл результата. Производитель v2.0 SHALL писать в конверт тот же MAJOR, что и в файл результата. #### Scenario: Успешный результат - **WHEN** сервис завершил обработку файла - **THEN** файл результата записан в S3 по ключу `results/{file_id}/{job_id}/result.json`, а затем опубликован конверт со `status = done | needs_check`, `result_s3` указывает на этот файл, `result_schema_version = "2.0"`, заголовок `Nats-Msg-Id` = `result::` #### Scenario: Ошибка обработки - **WHEN** сервис не смог обработать файл (нечитаем, таймаут внешней системы, тип не поддерживается) - **THEN** публикуется конверт со `status = error` и `error.code` из перечня схемы (`UNSUPPORTED_FORMAT`, `UNREADABLE`, `TIMEOUT`, `SCHEMA_MISMATCH`, `NOT_IMPLEMENTED`, `FILE_READ_ERROR`, `INVALID_FORMAT`, `INVALID_DOCUMENT`, `RECOGNITION_FAILED`, `UNKNOWN_ERROR`); `result_s3` может отсутствовать #### Scenario: Предупреждения в конверте - **WHEN** у файла есть предупреждение уровня файла (ошибочный json набора ИС) - **THEN** `metrics.warnings` конверта содержит объект `{code: "JSON_FAILED", severity: "error", message, locator}` ### Requirement: Файл результата Файл результата SHALL соответствовать `docs/contracts/recognition-result.schema.json` v2.0: один файл (`stored_file`) → один `job_id` → массив `documents` из `osvDocument` и/или `statementDocument`; денежные значения — строки `^-?\d+(\.\d{1,2})?$`; `confidence` — 0–100. Пустой массив `documents` допустим только при `status = error`. `service.statements_version` — коммит копии общего кода `arm_statements`, которым посчитан результат (обязателен при наличии `statementDocument`). `metrics.warnings[]` — предупреждения уровня файла (структура `warning` с `locator`). Каждый документ (и `osvDocument`, и `statementDocument`) SHALL иметь `status` (`done` | `needs_check`) и `warnings[]` — структуры `{code, severity, message, line_refs?, column_index?, field?}`; у `osvDocument` все предупреждения — `info`, `status = done`, когда ведомость разобрана. Остальное в `osvDocument` — как в v1.2 (`extra`, `period_start`, `period_end`, `is_from_year_start`). `statementDocument` SHALL иметь `form_variant` (`full` | `simplified` | `null`); `header` — поля `organization_name`, `inn`, `report_date`, `units`, `okud`, `okpo`, `okopf`, `okfs`, `address` и для каждого `<поле>_text` и `<поле>_confidence`, плюс `needs_review_fields[]`; `columns[]` — `column_index`, `label`, `label_confidence`, `period_date`, `period_kind` (`absolute` | `relative:current` | `relative:prev_year_end` | `relative:prev_prev_year_end` | `relative:prev_period` | `none`); `lines[]` — `row_index`, `ref`, `line_status` (`ok` | `unreadable` | `missing` | `detail`), `line_code` (строка 4–5 цифр или `null`; `null` ⇔ статус не `ok`), `line_code_text`, `line_code_confidence`, `line_name` (строка или `null`), `line_name_confidence`, `parent_ref` (у `detail`), `needs_review`, `values[]` — `column_index`, `amount`, `string_value`, `text`, `confidence`, `is_recognized`, `needs_review`. `checks` SHALL присутствовать у `BALANCE` / `INCOME` со строками и отсутствовать у документа без строк; `relations_failed` и `soft_relations_failed` — `{column_index, relation, codes[], expected, actual}`; `duplicate_line_codes` удалён. `document_type` — `BALANCE` | `INCOME` | `DISCLOSURE` | `OTHER`. `ref` уникален в документе. #### Scenario: Результат валидируется примерами - **WHEN** CI сервиса запускает тесты контракта - **THEN** все файлы `docs/contracts/examples/result-*.json` проходят схему файла результата v2.0, а все `request-*.json` / `result-message-*.json` — схемы сообщений #### Scenario: Лишняя колонка сохранена - **WHEN** в ОСВ между ИНН и суммами есть колонка «КПП» - **THEN** у строк контрагентов `extra = {"КПП": "<значение>"}`, файл проходит схему v2.0 #### Scenario: Форма из Excel с проверками - **WHEN** tabular-parser разобрал книгу с балансом на двух листах, ОФР со строкой без кода под 2110 и пояснениями (`examples/result-statement-excel.json`) - **THEN** файл проходит схему v2.0: `BALANCE` с `locator` из двух листов, `status`, `checks` и мягким нарушением; `INCOME` со строкой `detail` (`line_code = null`, `parent_ref`); `DISCLOSURE` без `columns`/`lines` и без `checks` #### Scenario: Форма из OCR с сомнительными значениями - **WHEN** scan-gateway разобрал ББ, где одна сумма собрана из грязного текста и покрыта соотношением, а одна строка `unreadable` (`examples/result-statement-minimal.json`) - **THEN** файл проходит схему v2.0; у покрытого значения `is_recognized = false`, `needs_review = false`, `text` — сырой; у строки `unreadable` `line_code = null`, `line_code_text` — сырой текст; документ `needs_check` #### Scenario: Границы периода ОСВ - **WHEN** в шапке ОСВ период «Октябрь 2024 г. – Сентябрь 2025 г.» - **THEN** `period_start = "2024-10-01"`, `period_end = "2025-09-30"`, `is_from_year_start = false`; документ ОСВ с `status` и `warnings`, файл проходит схему v2.0 #### Scenario: Строка без кода с кодом в поле - **WHEN** у строки `line_status = unreadable` передан непустой `line_code` - **THEN** файл не проходит схему v2.0 ### Requirement: Версионирование контракта Поле `schema_version` SHALL быть строкой `MAJOR.MINOR`. Добавление необязательных полей или значений enum — `MINOR` (обратно совместимо); удаление/переименование полей, смена типов или семантики — `MAJOR`. Сервис SHALL отвергать запрос с неизвестным `MAJOR` (`error.code = SCHEMA_MISMATCH`); ядро SHALL принимать результат с известным `MAJOR` и любым известным или новым `MINOR`. Запрос `recognition-request` остаётся v1.0; файл результата и конверт — v2.0 (`MAJOR` поднят: `line_code` стал nullable, `metrics.warnings` и `document.warnings` — структуры, удалён `checks.duplicate_line_codes`, изменён смысл `is_recognized` у OCR). #### Scenario: Несовместимая версия запроса - **WHEN** сервис, поддерживающий запрос `1.x`, получает запрос `schema_version = "2.0"` - **THEN** публикуется результат `status = error`, `error.code = SCHEMA_MISMATCH` **без ack `processing`**: ответом на запрос служит сам финальный конверт, ядро закрывает им задание (`published → error`), и повторной доставки запроса не будет. Конверт публикуется только если из тела извлекаются корректные `job_id` и `file_id`; иначе — как для нечитаемого запроса (ответа нет, ядро закроет задание по `NO_ACK` / `dlq`). `attempt` для `Nats-Msg-Id` берётся из тела запроса; при его отсутствии реализации расходятся (`tabular-parser` берёт `attempt` из заголовка `Nats-Msg-Id` запроса или не отвечает, `scan-gateway` подставляет 1) — см. OQ-71 #### Scenario: Ядро получает MAJOR, которого не знает - **WHEN** ядро, знающее `1.x` и `2.x`, получает конверт или файл результата `schema_version = "3.0"` - **THEN** результат не проходит проверку схемы, файл получает `ERROR` с причиной в `metrics.mapping_error`, повтор не выполняется (как для любого результата не по схеме) ## ADDED Requirements ### Requirement: Переход на контракт v2.0 — «сначала ядро» Ядро SHALL до перехода любого сервиса принимать конверты (включая ack `processing`) и файлы результата и `1.x`, и `2.0`, с двумя мапперами файла результата. При приёме `1.x` статус документа формы SHALL считаться по прежнему правилу D-6, а поля v2.0 заполняться по умолчанию: `line_status = ok`, `row_index` — по порядку строк в результате, `ref = null`, `text = string_value`, `needs_review` значения — по прежнему `values_ok` (`is_recognized` не `false` и `confidence` 100 или пуста), `needs_review` строки — `false`, `period_kind` — `absolute` при `period_date`, иначе `none`, `form_variant = null`, предупреждения-строки — `warning` `{code: "LEGACY_WARNING", severity: "info", message}`. Сервисы SHALL переходить на `2.0` только после выкладки ядра, принимающего `2.0`. Маппер `1.x` удаляется после перехода обоих сервисов отдельным изменением. #### Scenario: Сервис ещё на v1.2 - **WHEN** после выкладки ядра tabular-parser присылает результат `1.2` с повтором кода и `checks.duplicate_line_codes` - **THEN** ядро принимает его маппером `1.x`, статус документа — по D-6, файл не `ERROR` #### Scenario: Ack нового сервиса - **WHEN** scan-gateway после перехода публикует ack `processing` со `schema_version = "2.0"` - **THEN** ядро переводит задание в `processing`, как для ack `1.0` #### Scenario: Порядок нарушен - **WHEN** сервис прислал результат `2.0` с `line_code = null` ядру, знающему только `1.x` - **THEN** результат не проходит проверку схемы, файл получает `ERROR` (`metrics.mapping_error`) — поэтому порядок «сначала ядро» обязателен