diff --git a/CLAUDE.md b/CLAUDE.md index b0b9a54..739db8d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,7 +25,7 @@ (`OcrNormalizer`); своего конвертера и своего уплощения шапки нет, шлюз только валидирует словарь моделями `src/contract`. Переносы старого ядра (`ocr/text.py … document.py`) и справочники `api/rules/` удалены: словари меняются через - репозиторий `arm-statements`, не здесь. Решения по адаптеру (P5-n, R-n) — `openspec/changes/statements-v2/design.md`. + репозиторий `arm-statements`, не здесь. Решения по адаптеру (P5-n, R-n) — `openspec/changes/archive/2026-10-19-statements-v2/design.md`. - Статус документа и конверта определяет пакет (`envelope_status`); шлюз его не пересчитывает. Все json набора ошибочны — `error` с кодом первого ошибочного файла, а `metrics.warnings` всё равно перечисляют `JSON_FAILED` каждого json. Успешный json без таблиц — `JSON_FAILED`. Ни одной таблицы — `error RECOGNITION_FAILED`. @@ -55,7 +55,7 @@ `_<суффикс>.json` (атомарно, через `Tmp`). Промах → набор `SCAN_EMULATOR_DEFAULT`, иначе ответ-ошибка `INVALID_DOCUMENT`. Имя входящего файла на выбор пары не влияет. - Описание сервиса, протокол горячих папок, нормализация, env, запуск и коды ошибок — `README.md`; требования и решения - текущей фичи — `openspec/changes/statements-v2/`, Среза 3a — `openspec/changes/archive/2026-10-07-slice-3a-ocr-normalizer/`, Среза 0 — + единой нормализации — `openspec/changes/archive/2026-10-19-statements-v2/`, Среза 3a — `openspec/changes/archive/2026-10-07-slice-3a-ocr-normalizer/`, Среза 0 — `openspec/changes/archive/2026-10-04-slice-0-skeleton/`. ## Соглашения шаблона diff --git a/README.md b/README.md index 373c9ed..9180cd2 100644 --- a/README.md +++ b/README.md @@ -391,7 +391,7 @@ make golden-update # пересобрать эталоны нормализ ## Известные ограничения Нормализатор (правила шагов 2–8 — пакет `arm_statements`, ограничения — в `05-domain-rules.md` §11.9 и спеке -`statement-normalization`; решения шлюза — `openspec/changes/statements-v2/design.md`): +`statement-normalization`; решения шлюза — `openspec/changes/archive/2026-10-19-statements-v2/design.md`): - Сетка OCR строится по возрастанию `row_index`, пропуски `row_index` пустыми строками не заполняются (P5-1): шаг 2 не видит «дыр» между строками ИС (в реальных json их нет). Ячейка `value` не строка приводится через `str`, `confidence` diff --git a/openspec/changes/contract-identity-window/proposal.md b/openspec/changes/archive/2026-10-19-contract-identity-window/proposal.md similarity index 100% rename from openspec/changes/contract-identity-window/proposal.md rename to openspec/changes/archive/2026-10-19-contract-identity-window/proposal.md diff --git a/openspec/changes/contract-identity-window/specs/image-recognition-gateway/spec.md b/openspec/changes/archive/2026-10-19-contract-identity-window/specs/image-recognition-gateway/spec.md similarity index 100% rename from openspec/changes/contract-identity-window/specs/image-recognition-gateway/spec.md rename to openspec/changes/archive/2026-10-19-contract-identity-window/specs/image-recognition-gateway/spec.md diff --git a/openspec/changes/contract-identity-window/tasks.md b/openspec/changes/archive/2026-10-19-contract-identity-window/tasks.md similarity index 100% rename from openspec/changes/contract-identity-window/tasks.md rename to openspec/changes/archive/2026-10-19-contract-identity-window/tasks.md diff --git a/openspec/changes/statements-v2/design.md b/openspec/changes/archive/2026-10-19-statements-v2/design.md similarity index 100% rename from openspec/changes/statements-v2/design.md rename to openspec/changes/archive/2026-10-19-statements-v2/design.md diff --git a/openspec/changes/statements-v2/proposal.md b/openspec/changes/archive/2026-10-19-statements-v2/proposal.md similarity index 100% rename from openspec/changes/statements-v2/proposal.md rename to openspec/changes/archive/2026-10-19-statements-v2/proposal.md diff --git a/openspec/changes/statements-v2/specs/image-recognition-gateway/spec.md b/openspec/changes/archive/2026-10-19-statements-v2/specs/image-recognition-gateway/spec.md similarity index 100% rename from openspec/changes/statements-v2/specs/image-recognition-gateway/spec.md rename to openspec/changes/archive/2026-10-19-statements-v2/specs/image-recognition-gateway/spec.md diff --git a/openspec/changes/statements-v2/tasks.md b/openspec/changes/archive/2026-10-19-statements-v2/tasks.md similarity index 100% rename from openspec/changes/statements-v2/tasks.md rename to openspec/changes/archive/2026-10-19-statements-v2/tasks.md diff --git a/openspec/specs/image-recognition-gateway/spec.md b/openspec/specs/image-recognition-gateway/spec.md index 09ce667..7925d11 100644 --- a/openspec/specs/image-recognition-gateway/spec.md +++ b/openspec/specs/image-recognition-gateway/spec.md @@ -93,18 +93,15 @@ SHALL хватать `OPENBAO__MINIO__*`, `OPENBAO__NATS__NATS_URL`, `SCAN_HOTFO ### Requirement: Результат по контракту Сервис SHALL публиковать в `files.tabular_parsed` конверт по `recognition-result-message.schema.json` с `service.name = "scan-gateway"`, `Nats-Msg-Id = result::`; при `status ∈ {done, needs_check}` — файл -результата (`recognition-result.schema.json`, `schema_version = "1.0"` — `CONTRACT_VERSION` сервиса, решение №14; схема допускает любой MINOR) в `results/{file_id}/{job_id}/result.json` и `result_s3`; +результата (`recognition-result.schema.json`, `schema_version = "2.0"` — `CONTRACT_VERSION` сервиса; запрос `recognition-request` остаётся `1.x`, запрос `2.0` → `SCHEMA_MISMATCH`) в `results/{file_id}/{job_id}/result.json` и `result_s3`; при неудаче публикации SHALL повторить её до трёх раз. Нормализатор получает склеенный ответ и SHALL считать файл успешным, если `error.code == ""` (поле `error` реального успешного ответа — `{"code": "", "message": ""}`, а не `null`; отсутствующий или `null` `error` ошибкой не считается), и ошибочным, если `error.code` непуст, `parse_error` не `null` либо корень ответа файла не является объектом JSON (ошибка этого файла — `INVALID_FORMAT`); успех MUST NOT определяться -проверкой «`error` не `null`». Если ошибочны **все** файлы — `status = error`, `error.code` и `error.message` берутся у +проверкой «`error` не `null`». Конверт (включая ack `processing`) и файл результата несут `schema_version = "2.0"`, `result_schema_version = "2.0"`; `service.statements_version` (содержимое `api/arm_statements/VERSION`) SHALL быть в файле результата, в конверте `service` — `{name, version}`. Если ошибочны **все** файлы — `status = error`, `error.code` и `error.message` берутся у первого ошибочного файла по имени (для `parse_error` и не-объекта — `INVALID_FORMAT`); если код ИС этого файла не входит в перечень кодов контракта, `error.code = UNKNOWN_ERROR`, а исходный код SHALL быть указан в `error.message`. -Если есть хотя бы один успешный файл — документы собираются из успешных файлов по требованию «Нормализация ответа ИС», -а статус определяется по требованию «Статус результата нормализации». В обоих случаях `metrics.warnings` SHALL -содержать по одной строке `<имя файла>: <код>: <сообщение>` на каждый ошибочный файл, где `<код>` — исходный код ИС как -получен (для `parse_error` и не-объекта — `INVALID_FORMAT`); если предупреждений нет, поле `warnings` SHALL быть опущено. +Если есть хотя бы один успешный файл — документы собираются из успешных файлов по требованию «Нормализация ответа ИС в `statementDocument`», а статус определяется по требованию «Статус результата нормализации». `metrics.warnings` SHALL быть списком структур предупреждений (`code`, `severity`, `message`, `locator`, …) по схеме v2.0 — одним и тем же в конверте и в файле результата (предел 100 структур, `message` до 1000 символов; при переполнении остаются все `error`, затем `info`). Каждый ошибочный json набора SHALL давать предупреждение `JSON_FAILED` (`error`, `locator = file:<имя json>`, `message = "<код ИС>: <сообщение ИС>"`; для `parse_error` и не-объекта — код `INVALID_FORMAT`), в том числе когда ошибочны все json; исключение при разборе таблицы — `TABLE_PARSE_ERROR` (`error`, `locator = file:<имя json>;table:`, `message = "BACKEND_PARSING_ERROR: <тип>: <текст>"`). Успешный json без `data_ocr`, без `table_structure` или с пустым `table_structure` SHALL давать `JSON_FAILED`. Если предупреждений нет, поле `warnings` SHALL быть опущено. #### Scenario: Успешный ответ ИС в Срезе 0 - **WHEN** ответ из одного файла с `error = {"code": "", "message": ""}` и непустым `data_ocr` (по образцу `aep_6.json`) @@ -116,19 +113,19 @@ SHALL хватать `OPENBAO__MINIO__*`, `OPENBAO__NATS__NATS_URL`, `SCAN_HOTFO #### Scenario: Ошибка ИС - **WHEN** единственный файл ответа `.json` содержит `error.code = INVALID_DOCUMENT` -- **THEN** конверт `status = error`, `error.code = INVALID_DOCUMENT`, `error.message` из ответа, `raw_s3_key` заполнен, `metrics.warnings` содержит одну строку `.json: INVALID_DOCUMENT: <сообщение>` +- **THEN** конверт `status = error`, `error.code = INVALID_DOCUMENT`, `error.message` из ответа, `raw_s3_key` заполнен, `metrics.warnings` содержит одну структуру `JSON_FAILED` (`error`, `locator = file:.json`, `message = INVALID_DOCUMENT: <сообщение>`) #### Scenario: Ошибка одного из документов - **WHEN** ответ состоит из `.json` (успех, `aep_3.json`) и `_ofr.json` (`INVALID_DOCUMENT`, `aep_3_ofr.json`) -- **THEN** конверт `status = needs_check`, документы собраны из `.json`, `metrics.warnings` содержит строку `_ofr.json: INVALID_DOCUMENT: <сообщение>` и предупреждения нормализации +- **THEN** конверт `status = needs_check`, документы собраны из `.json`, `metrics.warnings` содержит `JSON_FAILED` (`error`, `locator = file:_ofr.json`, `message = INVALID_DOCUMENT: <сообщение>`) и предупреждения пакета; ядро не автоподтверждает документы файла #### Scenario: Все документы ошибочны - **WHEN** оба файла ответа ошибочны (первый по имени — `INVALID_DOCUMENT`, второй — не JSON) -- **THEN** конверт `status = error`, `error.code = INVALID_DOCUMENT` (код первого ошибочного файла по имени), `metrics.warnings` содержит две строки; файл не JSON в warnings несёт код `INVALID_FORMAT` +- **THEN** конверт `status = error`, `error.code = INVALID_DOCUMENT` (код первого ошибочного файла по имени), `metrics.warnings` содержит два `JSON_FAILED` (по одному на json); у файла не JSON сообщение начинается с `INVALID_FORMAT` #### Scenario: Все файлы не JSON - **WHEN** единственный файл ответа не разбирается как JSON -- **THEN** конверт `status = error`, `error.code = INVALID_FORMAT`, `metrics.warnings` содержит одну строку `.json: INVALID_FORMAT: <сообщение разбора>` +- **THEN** конверт `status = error`, `error.code = INVALID_FORMAT`, `metrics.warnings` содержит один `JSON_FAILED` с `message = INVALID_FORMAT: <сообщение разбора>` #### Scenario: Корень ответа не объект - **WHEN** файл ответа — корректный JSON, но корень — массив @@ -136,7 +133,15 @@ SHALL хватать `OPENBAO__MINIO__*`, `OPENBAO__NATS__NATS_URL`, `SCAN_HOTFO #### Scenario: Код ошибки ИС вне перечня контракта - **WHEN** единственный файл ответа содержит `error.code = SOME_NEW_CODE` -- **THEN** конверт `status = error`, `error.code = UNKNOWN_ERROR`, `error.message` содержит `SOME_NEW_CODE`, строка в `metrics.warnings` — `.json: SOME_NEW_CODE: <сообщение>` +- **THEN** конверт `status = error`, `error.code = UNKNOWN_ERROR`, `error.message` содержит `SOME_NEW_CODE`, `JSON_FAILED` в `metrics.warnings` с `message = SOME_NEW_CODE: <сообщение>` + +#### Scenario: Все json набора ошибочны +- **WHEN** оба json набора ошибочны (первый по имени — `INVALID_DOCUMENT`) +- **THEN** конверт `status = error`, `error.code = INVALID_DOCUMENT` (код и сообщение первого ошибочного json), `metrics.warnings` содержит `JSON_FAILED` каждого json + +#### Scenario: Успешный json без таблиц +- **WHEN** успешный json не содержит `data_ocr.table_structure` или `table_structure: []` +- **THEN** этот json даёт `JSON_FAILED`; если успешных json с таблицами нет — конверт `status = error` ### Requirement: Таймаут ожидания ответа Если готовый набор ответа (см. «Передача файла…») не появился до дедлайна, сервис SHALL опубликовать конверт `status = error`, `error.code = TIMEOUT`; @@ -171,12 +176,25 @@ SHALL ждать слот во внутренней очереди, их ack SHA - **THEN** пять ack'ов опубликованы сразу, четыре файла появляются во входящем каталоге, пятый — после завершения любого из четырёх ### Requirement: Копия контракта идентична источнику -`api/contracts/*.schema.json` SHALL быть идентичны `../docs/contracts/`; pydantic-модели SHALL валидировать примеры docs; -проверка входит в `make check`. +Каждый файл `api/contracts/**/*.json` SHALL быть байт в байт равен своей паре в `../docs/contracts/` (или в `contracts` +каталога из `ARM_KA_DOCS`) по правилу спеки docs `recognition-contract`, требование «Копия контракта в сервисе +идентична источнику»: схема с `$id` `…/v` — с `<имя>.v.schema.json`, если такой файл есть в docs, иначе с +основным; пример — с `examples/v/<имя>.json`, если он есть, иначе с `examples/<имя>.json`; файлы docs другого +`MAJOR` не сверяются. pydantic-модели SHALL валидировать все примеры копии `api/contracts/examples/*.json` и порождать +JSON, проходящий схему копии. Проверка входит в `make check`. #### Scenario: `make check` -- **WHEN** выполняются тесты контракта при доступном `../docs` -- **THEN** идентичность подтверждена, примеры валидны +- **WHEN** выполняются тесты контракта при доступном `../docs` (или `ARM_KA_DOCS`) +- **THEN** идентичность подтверждена, примеры валидны; при недоступном docs тест идентичности пропущен с предупреждением + +#### Scenario: Окно перехода на контракт 2.0 +- **WHEN** в docs основной `recognition-result.schema.json` уже v2.0, рядом `recognition-result.v1.schema.json` и + `examples/v1/`, а копия сервиса — v1.2 +- **THEN** копия сверяется с файлами `.v1` и `examples/v1/` (запрос — с основным файлом), тест проходит + +#### Scenario: Копия устарела +- **WHEN** в docs изменён файл, с которым сверяется копия, а копия — нет +- **THEN** тест идентичности падает, сообщение называет файл ### Requirement: Соединение с NATS без JetStream При пустых `NATS_SEED`/`NATS_JWT` сервис SHALL подключаться без учётных данных, иначе — с nkeys/JWT из `OPENBAO__NATS__NATS_SEED`/`NATS_JWT` (значения — секреты); MUST NOT требовать @@ -213,134 +231,78 @@ SHALL отражать состояние. - **THEN** опубликован `TIMEOUT`, входящий файл и каталог `Output//` (если есть) не тронуты ### Requirement: Нормализация ответа ИС в `statementDocument` -Для каждого успешного файла ответа нормализатор SHALL разобрать `data_ocr.table_structure[i]` в отдельный -`statementDocument` по §11 и §11.1 `05-domain-rules.md`: -- `document_type` — по нормализованному `label.form_name` таблицы через `statement_labels.yaml`; если `label` не - сопоставлен — по первой цифре кодов строк данных (`1` → `BALANCE`, `2` → `INCOME`, по большинству); иначе `OTHER`. - Если `label` и коды дают разный тип: при единодушных кодах (все коды данных одной группы) — тип по кодам, - предупреждение `тип по label (