Ссылки README и CLAUDE.md переведены на архив. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
7.6 KiB
scan-emulator Specification
Purpose
Эмулятор ИС Сканирование для разработки и тестов: воспроизводит протокол горячих папок и формат ответов Content AI,
отдавая реальные ответы из ../fixtures/ocr по имени файла, чтобы конвейер scan-gateway проверялся без доступа к
банковской системе.
Requirements
Requirement: Эмулятор отвечает по протоколу горячих папок
Процесс python -m src.scan_emulator SHALL опрашивать входящий каталог каждую секунду; файлы с суффиксами .part,
.tmp, .done и файлы, начинающиеся с ., SHALL игнорироваться. Для нового файла <uuid>.<ext> через
SCAN_EMULATOR_DELAY секунд (по умолчанию 2) эмулятор SHALL создать каталог <output>/<uuid>/ с набором ответов (по
одному файлу на документ); каталог SHALL появиться в <output> атомарно целиком: набор записывается во временный
каталог эмулятора и переименовывается. После обработки входящий файл SHALL быть переименован в <uuid>.<ext>.done,
чтобы не обрабатываться повторно.
Scenario: Один файл
- WHEN во входящем каталоге появляется
3f1c….pdfс содержимымaep_5.pdf - THEN через ≤ 5 с существует
Output/3f1c…/3f1c….json, а входящий файл переименован в3f1c….pdf.done
Scenario: Набор появляется целиком
- WHEN эмулятор записывает набор из нескольких файлов
- THEN каталог
Output/3f1c…/либо не существует, либо уже содержит весь набор; частичного набора вOutputнет
Scenario: Временные файлы шлюза игнорируются
- WHEN во входящем каталоге лежат
.3f1c….pdf.partи3f1c….pdf.done - THEN эмулятор не создаёт для них ответов
Requirement: Ответы берутся из фикстур по содержимому входящего файла
Эмулятор SHALL индексировать SCAN_FIXTURES_DIR: для каждого файла, не являющегося *.json, вычисляется sha256 →
основа имени <name> (имя без расширения; регистр расширения не важен); индекс SHALL пересчитываться, когда меняется
список файлов каталога. Для входящего файла SHALL вычисляться sha256 содержимого; при совпадении эмулятор SHALL собрать
все файлы <name>.json и <name>_*.json: <name>.json записывается как <uuid>.json, <name>_<suffix>.json — как
<uuid>_<suffix>.json, в каждом file_uid = <uuid>. Фикстуры вида <name><другие символы>.json без разделителя _
(например, aep_30.json для aep_3) MUST NOT попадать в набор. При промахе по хешу и заданном
SCAN_EMULATOR_DEFAULT=<name> с существующим набором эмулятор SHALL отдать набор <name>; лог SHALL сообщать, что
пара найдена по умолчанию.
Scenario: Точное имя
- WHEN входящий файл называется
<uuid>.pdf, а в фикстурах естьaep_5.pdfиaep_5.json - THEN имя входящего файла не используется для поиска: пара определяется только по содержимому (сценарий «Пара по содержимому»)
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и<uuid>_ofr.json, в обоихfile_uid = <uuid>
Scenario: Чужой префикс
- WHEN входящий файл равен
aep_3.pdf, в фикстурах есть такжеaep_30.json - THEN
aep_30.jsonв набор не попадает
Scenario: Промах с набором по умолчанию
- WHEN
SCAN_EMULATOR_DEFAULT=aep_6, входящий файл — произвольный PDF, которого нет в фикстурах - THEN в
Output/<uuid>/наборaep_6(<uuid>.jsonизaep_6.json), в логе отметка «по умолчанию»
Scenario: Имя входящего файла не влияет
- WHEN входящий файл называется
<uuid>.pdf, а его содержимое равноaep_6.pdf - THEN отдан набор
aep_6
Requirement: Неизвестный файл — ошибка в формате Content AI
Если пара по содержимому не найдена и SCAN_EMULATOR_DEFAULT не задан (или его набор отсутствует), эмулятор SHALL
записать один ответ <uuid>.json в формате реальных ответов-ошибок ИС (aep_5.json, aep_8_form4.json):
file_uid = <uuid>, error = {"code": "INVALID_DOCUMENT", "message": "<sha256[:12]>: нет пары по содержимому в фикстурах"}, data_ocr — объект с пустыми полями (label, organization, inn, units, date — {"form_name": "", "confidence": 0}) и table_structure: [].
Scenario: Нет пары
- WHEN входящий файл — произвольный PDF,
SCAN_EMULATOR_DEFAULTне задан - 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работает, его лог сообщает каталоги и число найденных пар фикстур