- design.md: Р-20 (законные повторы кода в перечне, синонимы в словаре имён), Р-21 (мягкое соотношение, сошедшееся точно, подтверждает неуверенное значение), Р-22 (S-1…S-25 приняты с оговорками; отмечены утверждённые владельцем); решения при доработке S-26…S-52 с ценой ошибки - statement-normalization: отдельное требование sync-statements (пути, VERSION, коды 0 / 10 / 20 / 2), словарь единиц и ОКЕИ, ref Excel с кавычками, нумерация с двойниками и пропуском, номер колонки, содержимое строки, столбец кодов у DISCLOSURE, упрощённый 2400 мягкий, Р-20 / Р-21 со сценариями, обе редакции перечня, интерфейс функции статуса, TABLE_PARSE_ERROR / SHEET_OTHER уровня файла - arm-core: признак строки правки row_index IS NULL, header_source, формы без checks (1.x и миграция) — §7 ядром в document_issue, codes / column_index, RESULT_STATUS_INCONSISTENT, синонимы и «итого по разделу», PATCH organization_name / confirmed: false / подтверждение даты, LINE_CODE_TAKEN, акцепт и подтверждения, LINES_UNRESOLVED с текстом, «того же периода», откат миграции - recognition-contract: MODIFIED «Копия контракта … идентична» — сверка по MAJOR копии, *.v1.schema.json на окне; 1.x — MAPPING_WARNING / LEGACY_WARNING; okei, column_index, пустые массивы DISCLOSURE, статус ОСВ одной формулировкой - quality-checks (новая дельта): make check охватывает копию arm_statements, make check-full — statements-check - tabular-recognition-service: манифест statements_manifest.yaml — failed_relations страниц 2 баланса (K-25) - scan-gateway, arm-web: TABLE_PARSE_ERROR файла; подсказки шапки, снятие подтверждений, LINE_CODE_TAKEN - proposal: шаг 0 выкладки, перечни кодов, ожидания на реальных сканах, Impact Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
164 lines
17 KiB
Markdown
164 lines
17 KiB
Markdown
## 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:<job_id>:<attempt>`
|
||
|
||
#### 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`.
|
||
Предупреждение уровня файла пишется там, где документа нет (потерянный json, таблица ИС, исключение разбора таблицы,
|
||
лист «прочее»).
|
||
`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?}` (`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 `<лист>!<A1>`,
|
||
имя листа в кавычках по правилу 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`; у документа
|
||
без строк `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: Строка без кода с кодом в поле
|
||
- **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). На окне перехода схемы прежнего `MAJOR` SHALL лежать
|
||
в `docs/contracts/` рядом с текущими как `recognition-result.v1.schema.json` и `recognition-result-message.v1.schema.json`
|
||
(байтовые копии 1.2 и 1.0), их примеры — в `docs/contracts/examples/v1/`; `scripts/validate_contracts.py` проверяет оба
|
||
набора. Они удаляются тем же изменением, что удаляет маппер `1.x` ядра.
|
||
|
||
#### 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`, повтор не
|
||
выполняется (как для любого результата не по схеме)
|
||
|
||
### Requirement: Копия контракта в сервисе идентична источнику
|
||
Каждый сервис, читающий или пишущий контракт, SHALL хранить копию файлов схем в `api/contracts/` и проверять в `make check`
|
||
их байтовую идентичность файлам `docs/contracts/` (`../docs` или путь `ARM_KA_DOCS`; при недоступности — пропуск с предупреждением).
|
||
На окне перехода между `MAJOR` тест идентичности SHALL сверять копию со схемой того `MAJOR`, который сервис пишет (или,
|
||
у ядра, читает): копия с `$id`, оканчивающимся на `/v1`, — с `docs/contracts/<имя>.v1.schema.json`, иначе — с основным
|
||
файлом `docs/contracts/<имя>.schema.json`; ядро держит копии обоих `MAJOR`. Правило теста вводится в `tabular-parser` и
|
||
`scan-gateway` до слияния контракта v2.0 в docs, иначе их `make check` упадёт на окне перехода.
|
||
|
||
#### Scenario: Копия устарела
|
||
- **WHEN** в `docs/contracts/` изменена схема, а копия в сервисе — нет
|
||
- **THEN** тест идентичности в `make check` сервиса падает с указанием имени файла
|
||
|
||
#### Scenario: Сервис ещё пишет 1.x после слияния v2.0 в docs
|
||
- **WHEN** в docs основной `recognition-result.schema.json` уже v2.0, а копия в `tabular-parser` — v1.2 (`$id` `…/v1`)
|
||
- **THEN** тест идентичности сверяет копию с `recognition-result.v1.schema.json` и проходит; посторонний PR в сервис на
|
||
окне перехода не краснеет
|
||
|
||
#### Scenario: Сервис перешёл на 2.0
|
||
- **WHEN** `scan-gateway` в PR шага 5 обновил копию до v2.0 (`$id` `…/v2`)
|
||
- **THEN** тест сверяет её с основным файлом docs
|
||
|
||
## ADDED Requirements
|
||
|
||
### Requirement: Переход на контракт v2.0 — «сначала ядро»
|
||
Ядро SHALL до перехода любого сервиса принимать конверты (включая ack `processing`) и файлы результата и `1.x`, и
|
||
`2.0`, с двумя мапперами файла результата. При приёме `1.x` статус документа формы SHALL считаться по прежнему правилу
|
||
D-6, где «проверки §7 сошлись» — по расчёту ядра копией `arm_statements` (полный набор), записанному в `document_issue`,
|
||
а поля v2.0 заполняться по умолчанию: `line_status = ok`, `row_index` — по порядку строк в результате с 0, `ref =
|
||
null`, `line_code_original_value = line_code`, `text = string_value`, `needs_review` значения — по прежнему `values_ok`
|
||
(`is_recognized` не `false` и `confidence` 100 или пуста), `needs_review` строки — `false`, `period_kind` — `absolute` при
|
||
`period_date`, иначе `none`, `form_variant = null`; строки `metrics.warnings` — в `recognition_job.metrics.warnings`
|
||
структурами `{code: "LEGACY_WARNING", severity: "info", message}`; предупреждения маппера по документу —
|
||
`document_issue` `MAPPING_WARNING` (`info`); `statements_version` задания — `null`. Сервисы SHALL переходить на `2.0`
|
||
только после выкладки ядра, принимающего `2.0`. Маппер `1.x` и схемы `*.v1.schema.json` удаляются после перехода обоих
|
||
сервисов отдельным изменением.
|
||
|
||
#### Scenario: Сервис ещё на v1.2
|
||
- **WHEN** после выкладки ядра tabular-parser присылает результат `1.2` с повтором кода и `checks.duplicate_line_codes`
|
||
- **THEN** ядро принимает его маппером `1.x`, обе строки кода — по прежнему правилу «остаётся первая» с
|
||
`MAPPING_WARNING`, нарушения §7 — в `document_issue` от ядра, статус документа — по 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`) — поэтому порядок
|
||
«сначала ядро» обязателен
|