Ссылки README и CLAUDE.md переведены на архив. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
86 lines
7.6 KiB
Markdown
86 lines
7.6 KiB
Markdown
# 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` работает, его лог сообщает каталоги и число найденных пар фикстур
|