openspec init (config с контекстом docs-репозитория) и change slice-0-skeleton: proposal, design (11 решений без обсуждения, открытые вопросы к человеку), спеки recognition-contract, dev-environment, ci-template, tasks в порядке зависимостей между репозиториями. 06-test-data.md §6: справочники продукта — seed arm-core, не фикстуры. 09-delivery-plan.md: Срез 0 по факту (change'и, compose в docs/dev, контракт копией без общего пакета, seed). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
73 lines
6.3 KiB
Markdown
73 lines
6.3 KiB
Markdown
# Spec Delta: recognition-contract
|
||
|
||
## Purpose
|
||
|
||
Контракт обмена между ядром `arm-core` и stateless-сервисами распознавания (`tabular-parser`, `scan-gateway`):
|
||
тела сообщений NATS `recognition.{tabular|image}.request` и `recognition.result`, файл результата в S3 и правила
|
||
версионирования и идемпотентности. Сервисы не знают БД; ядро — единственный писатель (DECISION-4).
|
||
|
||
## ADDED Requirements
|
||
|
||
### Requirement: Сообщение запроса распознавания
|
||
Тело сообщения на subject `recognition.tabular.request` или `recognition.image.request` SHALL соответствовать
|
||
`docs/contracts/recognition-request.schema.json` v1.0: обязательные поля `schema_version` (`"1.0"`), `job_id` (uuid =
|
||
`recognition_job.id`), `file_id` (uuid = `stored_file.id`), `pipeline` (`tabular` | `image`), `file_type`
|
||
(`XLS` | `XLSX` | `XLSB` | `PDF` | `PNG` | `JPG`), `original_name`, `s3 {bucket, key}` исходника в роли `uploads`,
|
||
`reply_deadline` (date-time, UTC), `attempt` (≥ 1). Subject SHALL соответствовать `pipeline`.
|
||
|
||
#### Scenario: Публикация запроса ядром
|
||
- **WHEN** ядро переводит `recognition_job` из `queued` в `published`
|
||
- **THEN** в subject, соответствующий `recognition_job.pipeline`, публикуется сообщение, проходящее схему запроса, с
|
||
заголовком `Nats-Msg-Id` = `<job_id>:<attempt>`
|
||
|
||
#### Scenario: Сообщение не по схеме
|
||
- **WHEN** сервис получает запрос, не проходящий схему (нет `s3.key`, неизвестный `file_type`)
|
||
- **THEN** сервис публикует результат со `status = error`, `error.code = SCHEMA_MISMATCH` и не падает
|
||
|
||
### Requirement: Сообщение результата распознавания
|
||
Тело сообщения на subject `recognition.result` SHALL соответствовать `docs/contracts/recognition-result-message.schema.json`
|
||
v1.0: обязательные поля `schema_version` (`"1.0"`), `job_id`, `file_id`, `status` (`done` | `needs_check` | `error`),
|
||
`service {name, version}`; при `status ≠ error` обязательны `result_s3 {bucket, key}` и `result_schema_version`;
|
||
при `status = error` обязателен `error {code, message}`; опциональны `raw_s3_key` (только `scan-gateway`) и `metrics`.
|
||
Поле `group_schema` в конверт NOT включается — источник `recognition_job.group_schema` — файл результата.
|
||
|
||
#### Scenario: Успешный результат
|
||
- **WHEN** сервис завершил обработку файла
|
||
- **THEN** файл результата записан в S3 по ключу `results/{file_id}/{job_id}/result.json`, а затем опубликован конверт
|
||
со `status = done | needs_check`, `result_s3` указывает на этот файл, заголовок `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` может отсутствовать
|
||
|
||
### Requirement: Файл результата
|
||
Файл результата SHALL соответствовать `docs/contracts/recognition-result.schema.json` v1.0: один файл (`stored_file`)
|
||
→ один `job_id` → массив `documents` из `osvDocument` и/или `statementDocument`; денежные значения — строки
|
||
`^-?\d+(\.\d{1,2})?$`; `confidence` — 0–100. Пустой массив `documents` допустим только при `status = error`.
|
||
|
||
#### Scenario: Результат валидируется примерами
|
||
- **WHEN** CI сервиса запускает тесты контракта
|
||
- **THEN** все файлы `docs/contracts/examples/result-*.json` проходят схему файла результата, а все
|
||
`request-*.json` / `result-message-*.json` — схемы сообщений
|
||
|
||
### Requirement: Версионирование контракта
|
||
Поле `schema_version` SHALL быть строкой `MAJOR.MINOR`. Добавление необязательных полей или значений enum —
|
||
`MINOR` (1.1, обратно совместимо); удаление/переименование полей, смена типов или семантики — `MAJOR` (2.0).
|
||
Сервис SHALL отвергать запрос с неизвестным `MAJOR` (`error.code = SCHEMA_MISMATCH`); ядро SHALL принимать результат
|
||
с тем же `MAJOR` и любым известным или новым `MINOR`.
|
||
|
||
#### Scenario: Несовместимая версия запроса
|
||
- **WHEN** сервис, поддерживающий `1.x`, получает запрос `schema_version = "2.0"`
|
||
- **THEN** публикуется результат `status = error`, `error.code = SCHEMA_MISMATCH`, запрос подтверждается (ack), чтобы не
|
||
зациклить доставку
|
||
|
||
### Requirement: Копия контракта в сервисе идентична источнику
|
||
Каждый сервис, читающий или пишущий контракт, SHALL хранить копию файлов схем в `api/contracts/` и проверять в CI их
|
||
байтовую идентичность файлам `docs/contracts/` (checkout `arm-ka-docs` или путь `ARM_KA_DOCS`).
|
||
|
||
#### Scenario: Копия устарела
|
||
- **WHEN** в `docs/contracts/` изменена схема, а копия в сервисе — нет
|
||
- **THEN** тест идентичности в CI сервиса падает с указанием имени файла
|