Ссылки README и CLAUDE.md ведут в архив. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
38 KiB
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 два кандидата с
locatorfile:aep_3.json;table:0и…;table:1, у каждого копия шапки json
Scenario: Таблица упала при разборе
- WHEN разбор
table:1jsonaep_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 пакета; поля, колонки и значения — по goldenfixtures/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'ы из настроек и не получают и не забирают сообщения стенда