Andrey Epifancev ce0702ac9b chore(openspec): архив contract-identity-window и statements-v2 (#42, #46)
Ссылки README и CLAUDE.md ведут в архив.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 09:26:39 +04:00

38 KiB
Raw Permalink Blame History

image-recognition-gateway Specification

Purpose

Адаптер внешней ИС Сканирование (Content AI) для PDF/PNG/JPG: принимает и подтверждает задание из NATS без JetStream, передаёт файл через горячие папки, ждёт ответ не дольше таймаута, сохраняет сырой ответ в S3 и отвечает ядру по контракту v1.0. Без БД и фронта.

Requirements

Requirement: Сервис не зависит от БД и фронта

Репозиторий MUST NOT содержать web/, alembic/, src/db/, src/models/, src/repositories/; зависимости SQLAlchemy, asyncpg, alembic, raisa_fastapi_protected_api MUST NOT присутствовать; smbprotocol SHALL остаться. Для старта SHALL хватать OPENBAO__MINIO__*, OPENBAO__NATS__NATS_URL, SCAN_HOTFOLDER_MODE=local, SCAN_HOTFOLDER_PATH.

Scenario: Старт в режиме локальных папок

  • WHEN сервис запущен с SCAN_HOTFOLDER_MODE=local и существующим каталогом SCAN_HOTFOLDER_PATH
  • THEN GET /healthcheck → 200 с полями uptime, version, nats, hotfolder = ok; OPENBAO__SMB__* не требуются

Requirement: Подтверждение запроса до обработки

Сервис SHALL быть подписан на files.img_uploaded в queue group scan-gateway. Получив запрос, проходящий схему, сервис SHALL до чтения файла из S3 опубликовать в files.tabular_parsed конверт status = processing (service.name = "scan-gateway", Nats-Msg-Id = ack:<job_id>:<attempt>). Повторный запрос с job_id, который уже обрабатывается или обработан этим процессом, SHALL получить повторный ack, и второй файл во входящий каталог MUST NOT записываться.

Scenario: Ack раньше файла

  • WHEN получен запрос для aep_5.pdf
  • THEN конверт processing опубликован раньше, чем файл появился во входящем каталоге

Scenario: Повторный запрос

  • WHEN тот же запрос доставлен повторно во время ожидания ответа ИС
  • THEN публикуется ещё один ack, во входящем каталоге остаётся один файл <job_id>.pdf

Requirement: Передача файла в ИС Сканирование через горячие папки

После подтверждения сервис SHALL прочитать исходник из S3 (s3.bucket/s3.key) и записать его во входящий каталог как <job_id>.<ext>, где <ext> — расширение original_name запроса в нижнем регистре; запрос без расширения SHALL завершаться конвертом RECOGNITION_FAILED без записи в горячую папку. Файл SHALL записываться сразу во входящий каталог открытием на запись с усечением, как в старом АРМ (решение владельца 09.10.2026): временных файлов, служебного каталога и переименования нет; если <Input>/<job_id>.<ext> уже существует, он SHALL быть перезаписан содержимым объекта S3. Затем сервис SHALL опрашивать исходящий каталог <SCAN_OUTPUT_DIR>/<job_id>/ каждые SCAN_POLL_SECONDS до min(reply_deadline, старт + SCAN_TIMEOUT_SECONDS) (SCAN_TIMEOUT_SECONDS по умолчанию 1200). ИС кладёт в этот каталог все распознанные документы задания — по одному *.json на документ, набор появляется целиком и больше не меняется; пустой или отсутствующий каталог означает ожидание. Ответ SHALL считаться готовым, когда в каталоге есть хотя бы один *.json, а набор имён и размеров *.json не менялся SCAN_SETTLE_SECONDS (по умолчанию 2); готовый набор SHALL включать все *.json каталога по возрастанию имени, остальные файлы игнорируются.

Scenario: Файл положен во входящий каталог

  • WHEN получен запрос для aep_5.PDF с job_id = 3f1c…
  • THEN во входящем каталоге появляется 3f1c….pdf, байт в байт равный объекту S3; каталог Tmp/ не создаётся, временных файлов нет

Scenario: Назначение уже существует

  • WHEN запрос для aep_5.pdf с job_id = 3f1c… обрабатывается, а Input/3f1c….pdf уже лежит с другим содержимым (задание повторено ядром после потери ack в другом процессе)
  • THEN Input/3f1c….pdf перезаписан и байт в байт равен объекту S3, задание продолжается опросом Output/3f1c…/, конверт RECOGNITION_FAILED не публикуется

Scenario: Запрос без расширения

  • WHEN получен запрос с original_name = "scan" без точки
  • THEN опубликован конверт error, error.code = RECOGNITION_FAILED, сообщение о расширении; во входящем каталоге файла нет

Scenario: Ответ из одного файла

  • WHEN в Output/3f1c…/ появляется один *.json и набор не меняется SCAN_SETTLE_SECONDS
  • THEN опрос прекращается не позднее чем через SCAN_SETTLE_SECONDS + SCAN_POLL_SECONDS после первого опроса, увидевшего окончательный набор (отсчёт стабилизации — от этого опроса, а не от времени изменения файла), ответ из одного файла передаётся дальше по конвейеру

Scenario: Ответ из нескольких файлов

  • WHEN в Output/3f1c…/ лежат 3f1c….json и 3f1c…_ofr.json, посторонний файл readme.txt, и набор не меняется SCAN_SETTLE_SECONDS
  • THEN дальше по конвейеру передаются оба *.json по возрастанию имени, readme.txt игнорируется

Scenario: Набор ещё пишется

  • WHEN размер файла или число файлов в Output/3f1c…/ менялось менее SCAN_SETTLE_SECONDS назад
  • THEN ответ не считается готовым, опрос продолжается до дедлайна

Scenario: Ошибка посреди записи

  • WHEN запись Input/3f1c….pdf прервалась ошибкой ввода-вывода
  • THEN опубликован конверт RECOGNITION_FAILED; недописанный файл удалён только при SCAN_CLEANUP=true, при false остаётся на месте

Requirement: Сырой ответ сохраняется в S3 до нормализации

Все файлы ответа ИС SHALL быть склеены в один объект {"job_id": "<job_id>", "files": [{"name": "<имя файла>", "response": <разобранный json> | null, "parse_error": "<текст>" | null}, ...]} (файлы по возрастанию имени) и записаны в raw/{job_id}.json того же бакета до вызова нормализатора; файл, не разобранный как JSON, SHALL получить response = null и заполненный parse_error и MUST NOT прерывать задание. Ключ SHALL передаваться в конверте результата полем raw_s3_key (одна строка raw/{job_id}.json); в БД объект не регистрируется (ядро его не создаёт).

Scenario: Нормализатор упал

  • WHEN ответ ИС получен, а нормализатор бросил исключение
  • THEN объект raw/{job_id}.json существует в S3, конверт содержит raw_s3_key и error.code = UNKNOWN_ERROR

Scenario: Склеенный сырой ответ

  • WHEN ИС положила в Output/<job_id>/ файлы <job_id>.json и <job_id>_ofr.json
  • THEN raw/{job_id}.json содержит job_id и files из двух элементов (<job_id>.json, затем <job_id>_ofr.json), у каждого response — разобранный ответ, parse_error = null

Scenario: Файл не JSON

  • WHEN один из файлов ответа не разбирается как JSON
  • THEN в files для него response = null, parse_error непустой; остальные файлы разобраны, задание не падает

Requirement: Результат по контракту

Сервис SHALL публиковать в files.tabular_parsed конверт по recognition-result-message.schema.json с service.name = "scan-gateway", Nats-Msg-Id = result:<job_id>:<attempt>; при status ∈ {done, needs_check} — файл результата (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». Конверт (включая 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. Если есть хотя бы один успешный файл — документы собираются из успешных файлов по требованию «Нормализация ответа ИС в 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:<i>, 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)
  • THEN начиная со Среза 3a конверт больше не NOT_IMPLEMENTED: поведение — по сценарию «Успешный ответ ИС» ниже

Scenario: Успешный ответ ИС

  • WHEN ответ из одного файла с error = {"code": "", "message": ""} и непустым data_ocr (aep_6.json)
  • THEN конверт status = needs_check, raw_s3_key = raw/<job_id>.json, result_s3 указывает на файл результата с двумя документами (BALANCE, INCOME)

Scenario: Ошибка ИС

  • WHEN единственный файл ответа <job_id>.json содержит error.code = INVALID_DOCUMENT
  • THEN конверт status = error, error.code = INVALID_DOCUMENT, error.message из ответа, raw_s3_key заполнен, metrics.warnings содержит одну структуру JSON_FAILED (error, locator = file:<job_id>.json, message = INVALID_DOCUMENT: <сообщение>)

Scenario: Ошибка одного из документов

  • WHEN ответ состоит из <job_id>.json (успех, aep_3.json) и <job_id>_ofr.json (INVALID_DOCUMENT, aep_3_ofr.json)
  • THEN конверт status = needs_check, документы собраны из <job_id>.json, metrics.warnings содержит JSON_FAILED (error, locator = file:<job_id>_ofr.json, message = INVALID_DOCUMENT: <сообщение>) и предупреждения пакета; ядро не автоподтверждает документы файла

Scenario: Все документы ошибочны

  • WHEN оба файла ответа ошибочны (первый по имени — INVALID_DOCUMENT, второй — не JSON)
  • 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_FAILED с message = INVALID_FORMAT: <сообщение разбора>

Scenario: Корень ответа не объект

  • WHEN файл ответа — корректный JSON, но корень — массив
  • THEN этот файл ошибочен с кодом INVALID_FORMAT; остальные файлы оцениваются по общим правилам

Scenario: Код ошибки ИС вне перечня контракта

  • WHEN единственный файл ответа содержит error.code = 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; повторная отправка файла в ИС MUST NOT выполняться.

Scenario: ИС молчит

  • WHEN запрос с reply_deadline через 10 с, ответ не появляется
  • THEN через ≤ 15 с опубликован конверт TIMEOUT, входящий файл остаётся в каталоге

Scenario: Набор не успел настояться до дедлайна

  • WHEN в Output/<job_id>/ есть *.json, но набор имён или размеров менялся позже, чем SCAN_SETTLE_SECONDS до дедлайна
  • THEN ответ не считается готовым, опубликован конверт TIMEOUT, недостающие файлы не ожидаются

Requirement: Ошибки транспорта превращаются в результат

Сервис SHALL отвечать конвертом status = error с кодом по таблице: запрос не по схеме или MAJOR ≠ 1 → SCHEMA_MISMATCH (ack не публикуется); объект S3 не читается → FILE_READ_ERROR; запись во входящий каталог не удалась или у исходника нет расширения → RECOGNITION_FAILED; файл ответа ИС не разбирается как JSON → parse_error этого файла, а если ошибочны все файлы — INVALID_FORMAT; ни одной таблицы не разобрано нормализатором → RECOGNITION_FAILED; иное исключение → UNKNOWN_ERROR. Ошибки очистки горячей папки MUST NOT влиять на конверт. Во всех случаях конверт SHALL быть опубликован, процесс MUST NOT завершаться.

Scenario: Горячая папка недоступна

  • WHEN каталог SCAN_HOTFOLDER_PATH не существует в момент обработки
  • THEN конверт RECOGNITION_FAILED с сообщением о недоступности горячей папки; /healthcheck.hotfolder = down

Requirement: Параллельная обработка ограничена

Сервис SHALL обрабатывать не более SCAN_MAX_INFLIGHT (по умолчанию 4) заданий одновременно; задания сверх лимита SHALL ждать слот во внутренней очереди, их ack SHALL быть опубликован сразу при получении.

Scenario: Пять заданий подряд

  • WHEN в очереди пять запросов при SCAN_MAX_INFLIGHT = 4
  • THEN пять ack'ов опубликованы сразу, четыре файла появляются во входящем каталоге, пятый — после завершения любого из четырёх

Requirement: Копия контракта идентична источнику

Каждый файл api/contracts/**/*.json SHALL быть байт в байт равен своей паре в ../docs/contracts/ (или в contracts каталога из ARM_KA_DOCS) по правилу спеки docs recognition-contract, требование «Копия контракта в сервисе идентична источнику»: схема с $id …/v<M> — с <имя>.v<M>.schema.json, если такой файл есть в docs, иначе с основным; пример — с examples/v<M>/<имя>.json, если он есть, иначе с examples/<имя>.json; файлы docs другого MAJOR не сверяются. pydantic-модели SHALL валидировать все примеры копии api/contracts/examples/*.json и порождать JSON, проходящий схему копии. Проверка входит в make check.

Scenario: make check

  • 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 требовать stream'ов и consumer'ов JetStream; при разрыве SHALL переподключаться и восстанавливать подписку; /healthcheck.nats SHALL отражать состояние.

Scenario: NATS перезапущен

  • WHEN контейнер NATS перезапущен во время работы сервиса
  • THEN в течение 20 с после старта NATS подписка восстановлена, nats = ok

Requirement: Очистка горячей папки под флагом

При SCAN_CLEANUP=true после того, как готовый набор ответа прочитан и склеен в raw/{job_id}.json (независимо от исхода нормализации), сервис SHALL удалить всё содержимое <SCAN_OUTPUT_DIR>/<job_id>/ (json-файлы ответа и любые прочие) и сам каталог <job_id>, входящий файл <Input>/<job_id>.<ext> и его вариант <Input>/<job_id>.<ext>.done, если они существуют. При SCAN_CLEANUP=false (по умолчанию) сервис MUST NOT удалять ничего, включая недописанный при ошибке записи входящий файл. При TIMEOUT, FILE_READ_ERROR, RECOGNITION_FAILED записи очистка MUST NOT выполняться (при ошибке записи и SCAN_CLEANUP=true снимается только недописанный входящий файл). Ошибка удаления SHALL быть записана в лог одной строкой на задание и MUST NOT менять конверт, metrics.warnings и /healthcheck.

Scenario: Очистка включена

  • WHEN SCAN_CLEANUP=true, ответ на 3f1c….pdf прочитан из Output/3f1c…/
  • THEN после публикации конверта в горячей папке нет Output/3f1c…/, Input/3f1c….pdf и Input/3f1c….pdf.done

Scenario: Очистка выключена

  • WHEN SCAN_CLEANUP=false, ответ прочитан
  • THEN все файлы остаются на месте, сервис ничего не удаляет и не пишет в лог о очистке (решение №27)

Scenario: Нет прав на удаление

  • WHEN SCAN_CLEANUP=true, удаление Output/3f1c…/3f1c….json завершается PermissionError
  • THEN конверт опубликован с тем же статусом, что и без очистки; в логе одна строка об ошибке удаления; /healthcheck.hotfolder = ok

Scenario: Таймаут не чистит

  • WHEN SCAN_CLEANUP=true, ответ не появился до дедлайна
  • THEN опубликован TIMEOUT, входящий файл и каталог Output/<job_id>/ (если есть) не тронуты

Requirement: Нормализация ответа ИС в statementDocument

Шлюз SHALL оставить у себя обмен с ИС Сканирование (горячие папки, опрос, срок, склейку набора json в raw/{job_id}.json до нормализации, очистку папки) и адаптер «таблица table_structure[i] json → FormCandidate»; шаги 2–8 SHALL выполняться копией пакета arm_statements (api/arm_statements/, правке не подлежит). Адаптер SHALL: создавать кандидата на каждую таблицу каждого успешного json; копировать шапку json (organization, inn, units, date) в шапку каждой своей таблицы как Cell (text = form_name, confidence ИС); передавать label.form_name таблицы как заголовок в evidence (тип — общим правилом шага 1 по словарю заголовков пакета, type_hint(okud_text, title_text, rules), у OCR okud_text = None); строить плотную сетку по row_index и column_index ИС (пустые позиции — null), Cell.ref = row:<row_index>; locator = file:<имя json>;table:<i>. value у OCR-ячеек всегда null, source = OCR. Нарушенная структура таблицы ИС SHALL давать исключение адаптера и TABLE_PARSE_ERROR всей таблицы, остальные таблицы разбираются. Статус конверта SHALL определяться только envelope_status пакета; перевод документов и предупреждений в контракт — только to_contract / warning_to_contract пакета.

Scenario: Один json — ББ и ОФР

  • WHEN json aep_3.json содержит две таблицы — «Бухгалтерский баланс» и «Отчет о финансовых результатах» — под общей шапкой
  • THEN два кандидата с locator file:aep_3.json;table:0 и …;table:1, у каждого копия шапки json

Scenario: Таблица упала при разборе

  • WHEN разбор table:1 json aep_3.json бросил исключение, table:0 разобрана
  • THEN документ BALANCE из table:0; в metrics.warnings — TABLE_PARSE_ERROR уровня error с locator = "file:aep_3.json;table:1"; конверт needs_check

Scenario: Таблица ИС только с текстом

  • WHEN в json есть таблица без столбца кодов и без распознанного типа, в ячейках только текст подписей
  • THEN документа нет, в metrics.warnings — TABLE_SKIPPED уровня info с locator таблицы; конверт из-за неё не needs_check

Scenario: Чистый скан с шапкой (aep_6.json)

  • WHEN нормализуется aep_6.json (две таблицы: «Бухгалтерский баланс» и «Отчет о финансовых результатах»)
  • THEN два кандидата, из них два документа BALANCE и INCOME по шагам 2–8 пакета; поля, колонки и значения — по golden fixtures/ocr/expected_normalize/; header.units = "тыс. руб."; статус определяет envelope_status пакета

Scenario: Зашумлённый скан без шапки (aep_3.json)

  • WHEN нормализуется aep_3.json (таблица «Бухгалтерский баланс» без строки-шапки, коды с мусором)
  • THEN разбор столбца кода, синтез шапки, предупреждения и статус документа — правилами пакета (шаги 2–8), шлюз их не дублирует; header.inn и report_date — по правилам очистки пакета; строки без кода сохраняются со статусом строки

Scenario: Суммы в скобках и с пробелами (aep_3.json, ОФР)

  • WHEN ячейка значения содержит (7744536} с confidence 88.9, другая — (6032409), третья — 13 070
  • THEN разбор сумм и is_recognized — общие правила пакета (шаг 4); шлюз передаёт текст ячейки как есть (value = None), без собственного разбора

Scenario: Нечисловая ячейка значения

  • WHEN ячейка значения содержит не **** с confidence 5.6
  • THEN amount = null, string_value = "не ****", is_recognized = false (правило шага 4 пакета)

Scenario: Повтор кода строки

  • WHEN в таблице баланса две строки с кодом 1150
  • THEN поведение определяет шаг пакета для повтора кода; шлюз ничего не отбрасывает до шагов 2–8, предупреждение приходит от пакета структурой metrics.warnings

Scenario: Тип по кодам при зашумлённом label

  • WHEN label.form_name таблицы — Бvxгалтерский баланс (не сопоставлен словарём заголовков), коды строк 1110…1700
  • THEN тип определяется общим правилом шага 1 пакета (по кодам), предупреждение — структурой пакета

Scenario: Label противоречит единодушным кодам

  • WHEN label.form_name таблицы — Бухгалтерский баланс, все коды строк данных 2110…2910
  • THEN тип и предупреждение о расхождении — по правилам пакета; header.form_name — текст label как распознан; статус needs_check

Scenario: Label противоречит смешанным кодам

  • WHEN label.form_name таблицы — Отчет о финансовых результатах, коды строк данных 1150, 1100 и 2110
  • THEN тип и предупреждение о расхождении — по правилам пакета; статус needs_check

Scenario: Таблица без столбца кода

  • WHEN в таблице нет строки с ячейкой «Код» и ни в одном столбце нет кодов
  • THEN документа нет, в metrics.warnings — TABLE_SKIPPED с locator таблицы; остальные таблицы файла разобраны

Requirement: Статус результата нормализации

Статус конверта SHALL определяться функцией envelope_status пакета arm_statements по документам и предупреждениям. Если из успешных файлов не собран ни один документ — status = error, error.code = RECOGNITION_FAILED, error.message = "ни одной таблицы не разобрано", причины по таблицам — в metrics.warnings (TABLE_PARSE_ERROR, TABLE_SKIPPED, TABLE_LOST); если все json ошибочны — код и сообщение первого ошибочного json. Если документы есть — done или needs_check по envelope_status; потерянный json набора (JSON_FAILED) или таблица (TABLE_PARSE_ERROR, TABLE_LOST) исключают done. metrics.rows — число строк lines всех документов, включая строки без кода.

Scenario: Ни одной таблицы

  • WHEN единственный успешный файл содержит table_structure: []
  • THEN json даёт JSON_FAILED, конверт status = error, raw_s3_key заполнен

Scenario: Все таблицы упали при разборе

  • WHEN в успешном json разбор каждой таблицы бросил исключение
  • THEN конверт status = error, error.code = RECOGNITION_FAILED, error.message = "ни одной таблицы не разобрано", metrics.warnings содержит TABLE_PARSE_ERROR каждой таблицы

Scenario: Потерянный документ набора

  • WHEN <job_id>.json успешен, <job_id>_ofr.json ошибочен
  • THEN статус не done, а needs_check

Scenario: Всё распознано

  • WHEN синтетический ответ, где все ячейки распознаны уверенно, шапка и даты разобраны, предупреждений нет
  • THEN status = done, поле metrics.warnings отсутствует

Scenario: Исключение в одной таблице

  • WHEN разбор table_structure[1] бросает исключение, table_structure[0] разбирается
  • THEN один документ, TABLE_PARSE_ERROR (error, locator = file:<имя json>;table:1, message = BACKEND_PARSING_ERROR: <класс>: <сообщение>), статус needs_check

Requirement: Subject'ы и учётные данные NATS из настроек окружения

Сервис SHALL брать subject'ы обмена из OPENBAO__NATS__NATS_TABULAR_REQUEST_SUBJECT, NATS_IMAGE_REQUEST_SUBJECT, NATS_RESULT_SUBJECT (умолчания files.tabular_uploaded, files.img_uploaded, files.tabular_parsed): подписан на image_request (queue group scan-gateway), публикует ack и результат в result. При старте SHALL проверяться все три имени (непустое, без пробельных символов, без * и >, без пустых токенов, попарно различны); при нарушении приложение MUST NOT стартовать. Значения NATS_JWT и NATS_SEED MUST NOT попадать в логи, repr настроек и тексты ошибок валидации. Используемые subject'ы SHALL писаться в лог при старте и отдаваться в /healthcheck объектом subjects с ключами image_request, result при любом значении nats (ok | down | off).

Scenario: Умолчания

  • WHEN приложение стартует без ключей OPENBAO__NATS__NATS_*_SUBJECT
  • THEN /healthcheck.subjects содержит имена files.* по умолчанию, обмен идёт по ним

Scenario: Subject'ы окружения

  • WHEN заданы OPENBAO__NATS__NATS_TABULAR_REQUEST_SUBJECT=test.files.tabular_uploaded, OPENBAO__NATS__NATS_IMAGE_REQUEST_SUBJECT=test.files.img_uploaded, OPENBAO__NATS__NATS_RESULT_SUBJECT=test.files.tabular_parsed
  • THEN подписки и публикации идут только по этим subject'ам, /healthcheck.subjects их показывает

Scenario: Недопустимый subject

  • WHEN OPENBAO__NATS__NATS_RESULT_SUBJECT совпадает с subject'ом запроса или содержит >
  • THEN приложение не стартует, ошибка называет ключ

Scenario: JWT не раскрывается

  • WHEN заданы NATS_JWT и NATS_SEED, а валидация настроек падает на другом поле
  • THEN текст ошибки и лог не содержат значений JWT и seed; repr настроек показывает их скрытыми

Scenario: Тесты на своих subject'ах

  • WHEN тесты интеграционные (tests/integration/) запускаются при работающих контейнерах стенда на том же NATS
  • THEN тесты используют уникальные subject'ы из настроек и не получают и не забирают сообщения стенда