20 KiB
tabular-recognition-service Specification
Purpose
Stateless-сервис разбора табличных файлов (XLS/XLSX/XLSB): принимает задания из NATS без JetStream, подтверждает их,
читает исходник из S3, отдаёт результат в S3 и конверт в NATS по контракту v1.1, не имея доступа к БД и фронта.
С Среза 2a реализация по умолчанию разбирает ОСВ (спека osv-parsing); заглушка NullParser — только для тестов воркера.
Requirements
Requirement: Сервис не зависит от БД и фронта
Репозиторий MUST NOT содержать каталоги web/, alembic/, src/db/, src/models/, src/repositories/; зависимости
SQLAlchemy, asyncpg, alembic, smbprotocol, raisa_fastapi_protected_api MUST NOT присутствовать в
requirements.txt; настройки OPENBAO.DATABASE и OPENBAO.SMB MUST NOT требоваться для старта.
Scenario: Старт с минимальным окружением
- WHEN сервис запускается с
.env, содержащим толькоOPENBAO__MINIO__*,OPENBAO__NATS__NATS_URL,DEBUG=true - THEN
GET /healthcheckотвечает 200 с полямиuptime,version,nats
Requirement: Подтверждение запроса до обработки
Сервис SHALL быть подписан на files.tabular_uploaded в queue group tabular-parser. Получив запрос, проходящий
recognition-request.schema.json, сервис SHALL до чтения файла из S3 опубликовать в files.tabular_parsed конверт
status = processing с job_id, file_id, service {name: "tabular-parser", version} и заголовком
Nats-Msg-Id = ack:<job_id>:<attempt>. Повторный запрос с job_id, который уже обрабатывается или обработан этим
процессом, SHALL получить повторный ack и MUST NOT обрабатываться второй раз.
Scenario: Ack раньше результата
- WHEN опубликован запрос из
docs/contracts/examples/request-tabular.json - THEN первым сообщением сервиса в
files.tabular_parsedявляется конвертprocessing, и только затем — результат
Scenario: Повторный запрос во время обработки
- WHEN тот же запрос доставлен второй раз, пока первый обрабатывается
- THEN публикуется второй ack
processing, файл из S3 повторно не читается, результат публикуется один раз
Requirement: Результат по контракту
После подтверждения сервис SHALL прочитать объект s3.key из бакета s3.bucket, вызвать парсер, записать файл результата
(по recognition-result.schema.json v2.0, schema_version = "2.0") в results/{file_id}/{job_id}/result.json и
опубликовать конверт результата (по recognition-result-message.schema.json v2.0, result_schema_version = "2.0") с
заголовком Nats-Msg-Id = result:<job_id>:<attempt>. Ack processing SHALL нести schema_version = "2.0"; запрос
recognition-request остаётся v1.0. service.statements_version файла результата SHALL быть коммитом копии пакета
arm_statements (файл VERSION копии; сервис без него не стартует) и записываться при наличии statementDocument.
metrics.warnings конверта SHALL содержать предупреждения уровня файла структурами {code, severity, message, locator?},
не строками. Статус конверта — по общему правилу: done ⇔ все документы done ∧ нет предупреждений файла уровня
error. При неудаче публикации сервис SHALL повторить её до трёх раз с паузой, затем залогировать ошибку.
Scenario: Запрос с заглушкой парсера (Срез 0)
- WHEN воркеру явно передана заглушка
NullParser(только тесты воркера; реализация по умолчанию — реальный парсер), запрос подтверждён и объект существует вuploads/ - THEN в
files.tabular_parsedпоявляется конвертstatus = error,error.code = NOT_IMPLEMENTED,job_id/file_idиз запроса,metrics.duration_ms ≥ 0; файл результата может отсутствовать
Scenario: Запрос с реальным парсером
- WHEN запрос подтверждён и объект в
uploads/существует, но не является книгой Excel (сигнатура не XLS/XLSX/XLSB) - THEN в
files.tabular_parsedпоявляется конвертstatus = error,error.code = UNSUPPORTED_FORMAT(повреждённая книга —UNREADABLE),job_id/file_idиз запроса,metrics.duration_ms ≥ 0; файл результата может отсутствовать; корректная ОСВ в том же потоке даётdone(см. «Парсер — заменяемый интерфейс»)
Scenario: Результат и ack версии 2.0
- WHEN обработан запрос
schema_version = "1.0"с книгой, содержащей баланс - THEN ack
processingи конверт результата несутschema_version = "2.0",result_schema_version = "2.0", файл результата проходит схему v2.0,service.statements_versionравен коммиту копииarm_statements
Scenario: Предупреждение файла структурой
- WHEN в книге лист «Примечания» без признаков ОСВ и формы
- THEN
metrics.warningsконверта содержит{code: "SHEET_OTHER", severity: "info", message, locator: "sheet:Примечания"}
Scenario: Повтор публикации
- WHEN первая публикация конверта результата завершается ошибкой соединения, вторая — успешно
- THEN конверт опубликован один раз с тем же
Nats-Msg-Id, обработка не повторяется
Requirement: Ошибки транспорта превращаются в результат
Сервис SHALL отвечать конвертом status = error в случаях: тело не проходит схему или MAJOR версии ≠ 1 →
SCHEMA_MISMATCH; объект S3 не читается → FILE_READ_ERROR; исключение парсера → UNKNOWN_ERROR (сообщение без
трассировки). Сервис MUST NOT падать по этим причинам. SCHEMA_MISMATCH SHALL публиковаться (без ack), когда из тела
читаемы job_id и file_id (uuid), а attempt — из тела или из заголовка запроса Nats-Msg-Id = <job_id>:<attempt>,
и этот job_id сервису ещё не известен (не в работе и не среди обработанных). Иначе валидный конверт собрать нельзя
либо он ушёл бы под тем же Nats-Msg-Id, что и настоящий результат: сервис SHALL записать в лог ошибку
(error_category=schema_mismatch, причина) — уровнем error, если идентификаторы не читаемы, уровнем warning, если
job_id уже известен, — и MUST NOT публиковать ни ack, ни результат; ядро обнаружит такое задание по dlq/timeout
(явная запись в контракт — открытый вопрос «невалидный запрос без читаемых job_id/file_id/attempt» в
open-questions-proposal.md). FILE_READ_ERROR и UNKNOWN_ERROR публикуются всегда: сервис MUST NOT молчать по ним.
Scenario: Нет объекта в S3
- WHEN
s3.keyзапроса указывает на несуществующий объект - THEN публикуется конверт
error.code = FILE_READ_ERROR, процесс продолжает работу
Scenario: Несовместимая версия
- WHEN запрос содержит
schema_version = "2.0" - THEN публикуется конверт
error.code = SCHEMA_MISMATCH, ack не публикуется
Scenario: Невалидный запрос без читаемых идентификаторов
- WHEN тело запроса не JSON либо в нём не читаются
job_idилиfile_id, либоattemptнет ни в теле, ни в заголовкеNats-Msg-Id - THEN в лог пишется ошибка
error_category=schema_mismatch, ни ack, ни результат не публикуются, процесс продолжает работу
Scenario: Невалидный запрос с уже известным job_id
- WHEN запрос не проходит схему, а его
job_idуже в работе или среди обработанных - THEN в лог пишется предупреждение
error_category=schema_mismatch, ни ack, ни результат не публикуются; обработка или результат задания с этимjob_idне меняются
Requirement: Парсер — заменяемый интерфейс
Разбор файла SHALL выполняться через единый интерфейс «байты + тип файла → документы/метрики/ошибка», не знающий о
NATS и S3; воркер MUST NOT зависеть от конкретной реализации. Реализация по умолчанию (get_parser()) SHALL быть
оркестратором книги BookParser: он читает книгу один раз, классифицирует листы по содержимому и отдаёт листы ОСВ
конвейеру спеки osv-parsing, листы форм и их продолжения — конвейеру спеки statement-parsing (адаптер Excel и копия
пакета arm_statements); прочие листы дают предупреждение файла SHEET_OTHER. Файл результата SHALL писаться с
schema_version = "2.0"; документы книги — ОСВ в порядке листов, затем формы в порядке листов. Заглушка NullParser
(error NOT_IMPLEMENTED) SHALL использоваться только в тестах воркера и MUST NOT быть реализацией по умолчанию.
Реализация по умолчанию SHALL создаваться один раз при старте приложения (словари ОСВ читаются и проверяются там же,
словари форм — default_rules() пакета). Каталог api/rules/ SHALL содержать словари ОСВ (osv_headers, osv_groups,
periods, data_types, osv_checks) и README; словарей форм ББ/ОФР в сервисе нет — они часть копии arm_statements.
Scenario: Подмена реализации в тесте
- WHEN в тесте воркеру передан парсер, возвращающий один
osvDocument - THEN файл результата с этим документом записан в S3, конверт имеет
status = doneиresult_s3на этот файл
Scenario: Реальная ОСВ через воркер
- WHEN в интеграционном тесте в S3 лежит
rb/2024-FY/ОСВ_66 за 2024.xlsи опубликован запросfile_type = XLS - THEN после ack публикуется конверт
doneсresult_schema_version = "2.0", файл результата проходит схему v2.0 и содержит одинosvDocumentсоstatus = doneиaccount_code_raw = "66"
Scenario: Реальные формы через воркер
- WHEN в интеграционном тесте в S3 лежит книга
statements-xlsx/npz/2025-Q3/…xlsxи опубликован запросfile_type = XLSX - THEN после ack публикуется конверт
doneсresult_schema_version = "2.0", файл результата проходит схему v2.0 и содержит дваstatementDocument(BALANCE,INCOME) со строкамиline_status,ref,checksи пустымиchecks.relations_failed; расхождения итогов в пределах округления (1600 = 1700на 1.94 / 9.02 / 2.48,2100/2200/2300) — вsoft_relations_failed(OQ-138);service.statements_versionравен коммиту копии пакета
Scenario: Книга без ОСВ и форм
- WHEN в S3 лежит книга Excel, ни один лист которой не имеет признаков ОСВ или формы
- THEN публикуется конверт
error,error.code = INVALID_DOCUMENT, сообщение называет число просмотренных листов
Scenario: Сервис без версии общего кода не стартует
- WHEN в копии
arm_statementsнет файлаVERSION - THEN
require_statements_version()бросаетRuntimeErrorс текстом «копия arm_statements без VERSION», сервис не стартует
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 SHALL отражать состояние (nats = ok | down; off — NATS выключен флагом
NATS_ENABLED=false).
Scenario: NATS перезапущен
- WHEN контейнер NATS перезапущен во время работы сервиса
- THEN в течение 20 с после старта NATS подписка восстановлена,
nats = ok, новый запрос обрабатывается
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): подписан на tabular_request (queue group tabular-parser), публикует ack и результат в result.
При старте SHALL проверяться все три имени (непустое, без пробельных символов, без * и >, без пустых токенов,
попарно различны); при нарушении приложение MUST NOT стартовать. Значения NATS_JWT и NATS_SEED MUST NOT попадать в
логи, repr настроек и тексты ошибок валидации. Используемые subject'ы SHALL писаться в лог при старте и отдаваться в
/healthcheck объектом subjects с ключами tabular_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 тесты roundtrip (
tests/integration/test_roundtrip.py) запускаются при работающих контейнерах стенда на том же NATS - THEN тесты используют уникальные subject'ы из настроек и не получают и не забирают сообщения стенда