Срез 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
5.4 KiB
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работает, его лог сообщает каталоги и число найденных пар фикстур