Andrey Epifancev 98f21297bb openspec: архив change slice-3a-ocr-normalizer (2026-10-07), спеки image-recognition-gateway и scan-emulator
Ссылки README и CLAUDE.md переведены на архив.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-07 05:07:07 +04:00

7.6 KiB
Raw Permalink Blame History

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