Andrey Epifancev f12ce87346 docs: соглашения шаблона в CLAUDE.md и OpenSpec change slice-0-skeleton
Срез 0 (issue #1): раздел «Соглашения шаблона» (что оставлено от шаблона команды, что
вырезается, что добавляется); change openspec/changes/slice-0-skeleton — proposal, design
с таблицей «Decisions made without discussion», спеки и tasks; в openspec/config.yaml
строки с двоеточием взяты в кавычки (YAML разбирал их как словари).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 02:43:34 +04:00

6.5 KiB
Raw Blame History

Spec Delta: recognition-job-exchange

Purpose

Единственная точка обмена ядра с сервисами распознавания — таблица recognition_job (DECISION-4): outbox публикует запросы в NATS, inbox принимает результаты идемпотентно, таймауты и DLQ отражаются в статусе задания, файл результата регистрируется в s3_object.

ADDED Requirements

Requirement: Outbox публикует задания из queued

Фоновый публикатор SHALL с периодом OUTBOX_POLL_SECONDS (по умолчанию 5) выбирать задания status = queued (с блокировкой FOR UPDATE SKIP LOCKED), формировать сообщение по recognition-request.schema.json v1.0 из recognition_job ⋈ stored_file ⋈ s3_object и публиковать в subject по pipeline с заголовком Nats-Msg-Id = <job_id>:<attempt>. После подтверждения JetStream задание SHALL получить status = published, published_at, request_msg_id, request_subject; при ошибке публикации задание SHALL остаться queued.

Scenario: Публикация запроса парсеру

  • WHEN в recognition_job вставлена строка queued с pipeline = tabular, связанная со stored_file (XLSX) и его s3_object
  • THEN в течение 10 с в recognition.tabular.request появляется сообщение, проходящее схему запроса, с job_id, file_id, s3.key исходника, reply_deadline, attempt = 1, а строка переходит в published с заполненным request_msg_id

Scenario: NATS недоступен

  • WHEN публикация завершается ошибкой соединения
  • THEN задание остаётся queued, ошибка залогирована, следующий цикл повторяет публикацию; дубликат после восстановления отбрасывается JetStream по Nats-Msg-Id

Requirement: Inbox принимает результат идемпотентно

Durable consumer arm-core-results на recognition.result SHALL валидировать конверт по recognition-result-message.schema.json, найти задание по job_id и: если result_msg_id задания уже равен Nats-Msg-Id сообщения — подтвердить без изменений; иначе зарегистрировать файл результата в s3_object (bucket_role = results, created_by = <service.name>), обновить задание (status ∈ {done, needs_check, error}, service_name, service_version, finished_at, metrics, error_code, error_message, result_schema_version, result_s3_object_id, result_msg_id), зафиксировать транзакцию и только затем подтвердить сообщение. Неизвестный job_id SHALL быть залогирован и подтверждён. Сообщение с num_delivered ≥ max_deliver SHALL перевести задание в dlq.

Scenario: Успешный результат

  • WHEN сервис публикует конверт status = done с result_s3 = {arm-ka, results/<file_id>/<job_id>/result.json} и Nats-Msg-Id = result:<job_id>:1
  • THEN задание получает status = done, result_msg_id = result:<job_id>:1, result_s3_object_id указывает на новую строку s3_object с этим ключом, finished_at заполнен

Scenario: Повторная доставка

  • WHEN то же сообщение доставляется второй раз
  • THEN строка recognition_job и s3_object не меняются, сообщение подтверждено

Scenario: Ошибка сервиса

  • WHEN конверт содержит status = error, error = {NOT_IMPLEMENTED, …} без result_s3
  • THEN задание получает status = error, error_code = NOT_IMPLEMENTED, error_message из конверта, result_s3_object_id пуст

Scenario: Отравленное сообщение

  • WHEN обработка одного и того же сообщения падает ошибкой БД пять раз подряд (max_deliver = 5)
  • THEN на пятой доставке задание переводится в dlq, сообщение подтверждено, повторов больше нет

Requirement: Таймаут ожидания результата

Фоновый вахтёр SHALL с периодом 30 с переводить задания published или processing с reply_deadline < now() в status = timeout, error_code = TIMEOUT, finished_at = now(). Replay задания (новая попытка) — вне Среза 0.

Scenario: Сервис не ответил

  • WHEN задание опубликовано с reply_deadline в прошлом и результата нет
  • THEN не позднее чем через 30 с задание имеет status = timeout

Requirement: Соединение с NATS по шаблону, stream — по флагу

Ядро SHALL подключаться к NATS с nkeys-seed и JWT, если заданы OPENBAO__NATS__NATS_SEED/NATS_JWT, и без учётных данных, если они пусты; при NATS_MANAGE_STREAM=true SHALL создать stream RECOGNITION (subjects recognition.>, окно дубликатов 2 мин) и свой consumer, если их нет. Фоновые задачи SHALL запускаться при NATS_ENABLED=true.

Scenario: Dev-окружение compose

  • WHEN ядро стартует с DEBUG=true, NATS_ENABLED=true, NATS_MANAGE_STREAM=true и пустыми учётными данными
  • THEN stream RECOGNITION и consumer arm-core-results существуют, в логах нет ошибок подключения

Scenario: NATS выключен

  • WHEN NATS_ENABLED=false
  • THEN приложение стартует, фоновые задачи не запускаются, /healthcheck отвечает 200