## MODIFIED Requirements ### 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`. Предупреждение уровня файла пишется там, где документа нет (потерянный json, таблица ИС, исключение разбора таблицы, лист «прочее»). `service.statements_version` — версия установленной библиотеки `arm_statements` (дистрибутив `pylib-ai-arm-statements`), которой посчитан результат (обязателен при наличии `statementDocument`); у результатов, посчитанных до перехода на библиотеку, — хэш коммита копии общего кода. `metrics.warnings[]` — предупреждения уровня файла (структура `warning` с `locator`). Каждый документ (и `osvDocument`, и `statementDocument`) SHALL иметь `status` (`done` | `needs_check`) и `warnings[]` — структуры `{code, severity, message, line_refs?, column_index?, field?}` (`line_refs` — только `ref` строк из `lines[]`); у `osvDocument` все предупреждения — `info`, `status = done`, когда контрольные суммы §1.4 листа сошлись, иначе `needs_check`. Остальное в `osvDocument` — как в v1.2: у строки `rows[]` необязательное поле `extra` — объект «заголовок колонки → строка или null» для колонок внутри таблицы, не сопоставленных с шестью суммами и ИНН (§1.2 «не терять»); у шапки — `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`, `okei`, `address` и для каждого `<поле>_text` и `<поле>_confidence`, плюс `needs_review_fields[]`; `columns[]` — `column_index` (номер столбца в сетке кандидата: у OCR — `column_index` ИС, у Excel — номер столбца листа с 0), `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` (у Excel `<лист>!`, имя листа в кавычках по правилу Excel), `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` в v2.0 не порождается: лист «прочее» — предупреждение файла `SHEET_OTHER`, документа нет; значение остаётся только в схеме 1.x и в `document_type` ядра для документов прежних результатов); `form_variant` обязателен (`null` у `DISCLOSURE`); у документа без строк `columns` и `lines` — пустые массивы. `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: Документ OTHER в v2.0 - **WHEN** в результате v2.0 `statementDocument` с `document_type = OTHER` или без `form_variant` - **THEN** файл не проходит схему v2.0 #### Scenario: Строка без кода с кодом в поле - **WHEN** у строки `line_status = unreadable` передан непустой `line_code` - **THEN** файл не проходит схему v2.0