Andrey Epifancev 7cec4596d9 chore(openspec): архив change slice-0-skeleton
Срез 0 реализован и влит (PR #3), change перенесён в openspec/changes/archive, спеки — в openspec/specs.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-04 19:44:47 +04:00

5.4 KiB
Raw Blame History

scan-emulator Specification

Purpose

Эмулятор ИС Сканирование для разработки и тестов: воспроизводит протокол горячих папок и формат ответов Content AI, отдавая реальные ответы из ../fixtures/ocr по имени файла, чтобы конвейер scan-gateway проверялся без доступа к банковской системе.

Requirements

Requirement: Эмулятор отвечает по протоколу горячих папок

Процесс python -m src.scan_emulator SHALL опрашивать входящий каталог каждую секунду; для нового файла <uuid>_<name>.<ext> через SCAN_EMULATOR_DELAY секунд (по умолчанию 2) SHALL создать каталог <output>/<uuid>/ с набором ответов (по одному файлу на документ); каталог SHALL появиться в <output> атомарно целиком: набор записывается во временный каталог эмулятора и переименовывается. После обработки входящий файл SHALL быть переименован в <uuid>_<name>.<ext>.done, чтобы не обрабатываться повторно.

Scenario: Один файл

  • WHEN во входящем каталоге появляется 3f1c…_aep_5.pdf
  • THEN через ≤ 5 с существует Output/3f1c…/3f1c….json, а входящий файл переименован в …pdf.done

Scenario: Набор появляется целиком

  • WHEN эмулятор записывает набор из нескольких файлов
  • THEN каталог Output/3f1c…/ либо не существует, либо уже содержит весь набор; частичного набора в Output нет

Requirement: Ответы берутся из фикстур по имени

Эмулятор SHALL собрать в SCAN_FIXTURES_DIR все файлы <name>.json и <name>_*.json, где <name> — имя входящего файла без префикса <uuid>_ и расширения (регистр расширения входящего файла не важен). Файл <name>.json SHALL быть записан как <uuid>.json, файл <name>_<suffix>.json — как <uuid>_<suffix>.json; в каждом file_uid = <uuid>. Фикстуры вида <name><другие символы>.json без разделителя _ (например, aep_30.json для aep_3) MUST NOT попадать в набор.

Scenario: Точное имя

  • WHEN входящий файл — …_aep_5.pdf, в фикстурах есть aep_5.json
  • THEN в Output/<uuid>/ один файл <uuid>.json, равный aep_5.json с заменённым file_uid

Scenario: Суффикс

  • WHEN входящий файл — …_aep_8.pdf, в фикстурах есть aep_8_form4.json
  • THEN в Output/<uuid>/ файл <uuid>_form4.json, равный aep_8_form4.json с заменённым file_uid

Scenario: Несколько файлов на одно задание

  • WHEN входящий файл — …_aep_3.pdf, в фикстурах есть aep_3.json и aep_3_ofr.json
  • THEN в Output/<uuid>/ два файла: <uuid>.json (из aep_3.json) и <uuid>_ofr.json (из aep_3_ofr.json), в обоих file_uid = <uuid>

Scenario: Чужой префикс

  • WHEN входящий файл — …_aep_3.pdf, в фикстурах есть также aep_30.json
  • THEN aep_30.json в набор не попадает

Requirement: Неизвестный файл — ошибка в формате Content AI

Если ни одного файла набора не найдено, эмулятор SHALL записать один ответ <uuid>.json в формате реальных ответов-ошибок ИС (aep_5.json, aep_8_form4.json): file_uid = <uuid>, error = {"code": "INVALID_DOCUMENT", "message": "<имя>: нет эталонного ответа в фикстурах"}, data_ocr — объект с пустыми полями (label, organization, inn, units, date — {"form_name": "", "confidence": 0}) и table_structure: [].

Scenario: Нет пары

  • WHEN входящий файл — …_unknown.pdf
  • THEN ответ содержит error.code = INVALID_DOCUMENT, data_ocr — объект с пустыми полями и table_structure: []; scan-gateway публикует конверт с тем же кодом

Requirement: Эмулятор запускается из того же образа

В docs/dev/docker-compose.yml сервис scan-emulator SHALL использовать образ scan-gateway с командой python -m src.scan_emulator, общий том hotfolder и ../../fixtures/ocr, смонтированный только для чтения.

Scenario: Compose

  • WHEN выполнен docker compose --profile services up -d
  • THEN контейнер scan-emulator работает, его лог сообщает каталоги и число найденных пар фикстур