Andrey Epifancev a15fe65d52 openspec: спека Среза 3a — горячие папки как у armka_file_worker, нормализатор OCR → statementDocument v1.2 (#6)
proposal, design (D-1…D-9, решения без обсуждения №1–12), дельты спек image-recognition-gateway
(папки, очистка под флагом, нормализация, справочники с редакциями, статусы) и scan-emulator (пара по sha256,
набор по умолчанию). tasks.md заполняется планом.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-06 18:39:52 +04:00

7.3 KiB
Raw Blame History

Spec Delta: scan-emulator

Purpose

Эмулятор ИС Сканирование для разработки и тестов: воспроизводит протокол горячих папок (раскладка FileWorker: <uuid>.<ext> → Output/<uuid>/*.json) и формат ответов Content AI, отдавая реальные ответы из ../fixtures/ocr по содержимому входящего файла, чтобы конвейер scan-gateway проверялся без доступа к банковской системе.

RENAMED Requirements

  • FROM: ### Requirement: Ответы берутся из фикстур по имени
  • TO: ### Requirement: Ответы берутся из фикстур по содержимому входящего файла

MODIFIED 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 публикует конверт с тем же кодом