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

86 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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