Ссылки README и CLAUDE.md ведут в архив. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
102 lines
13 KiB
Markdown
102 lines
13 KiB
Markdown
# CLAUDE.md — scan-gateway
|
||
|
||
Общий контекст системы — `../CLAUDE.md` и `../docs/`. Этот файл — только про этот сервис.
|
||
|
||
## Что это
|
||
Адаптер внешней ИС Сканирование (Content AI) для PDF/PNG/JPG: положить файл во «Входящие» как `<job_id>.<ext>`
|
||
(расширение `original_name` в нижнем регистре; пишется **сразу** открытием `wb`, существующий файл перезаписывается — как в
|
||
старом АРМ), забрать **все** `Исходящие/<job_id>/*.json` (ИС кладёт по одному json на каждый
|
||
распознанный документ), склеить и сохранить сырой ответ в S3 `raw/{job_id}.json` (в БД не регистрируется), очистить
|
||
горячую папку (только при `SCAN_CLEANUP=true`), нормализовать в `../docs/contracts/recognition-result.schema.json`
|
||
(`statementDocument` v2.0 общим кодом `arm_statements`: по одному документу на таблицу `table_structure[i]`), опубликовать `files.tabular_parsed`
|
||
(по умолчанию; задаётся `OPENBAO__NATS__NATS_*_SUBJECT`) (`done` / `needs_check` / `error`). Таймаут → ошибка `TIMEOUT` по `min(reply_deadline, старт + SCAN_TIMEOUT_SECONDS=1200)`,
|
||
старт — запись файла во «Входящие». **Нет доступа к БД.**
|
||
|
||
## Правила
|
||
- `api/arm_statements/` — копия пакета `arm-statements`, напрямую не править: обновление — `../docs/scripts/sync-statements.sh`
|
||
(`--in-place` в PR фичи), свежесть — `make statements-check` (в `check-full`).
|
||
- Нормализация по фактическим ответам `../fixtures/ocr/aep_*.json`, а не по ТЗ (OQ-13, OQ-14), по `05-domain-rules.md`
|
||
§11, §11.1 и §11.9: правило, которого там нет, не реализуется, а поднимается вопросом. Шаги 2–8, словари и статусы —
|
||
в пакете `arm_statements`; в шлюзе остаются только:
|
||
- адаптер шага 1 — `api/src/normalizer/ocr/adapter.py` (таблица ИС → кандидат, `ref` — номер строки ИС);
|
||
- обход набора json — `api/src/normalizer/ocr/answer_set.py` (`normalize_candidate` на каждую таблицу,
|
||
`envelope_status`, коды `JSON_FAILED` / `TABLE_PARSE_ERROR`; Р-32/Р-33);
|
||
- граница контракта — `to_contract` / `warning_to_contract` пакета, вызываются в `api/src/normalizer/ocr/__init__.py`
|
||
(`OcrNormalizer`); своего конвертера и своего уплощения шапки нет, шлюз только валидирует словарь моделями
|
||
`src/contract`.
|
||
Переносы старого ядра (`ocr/text.py … document.py`) и справочники `api/rules/` удалены: словари меняются через
|
||
репозиторий `arm-statements`, не здесь. Решения по адаптеру (P5-n, R-n) — `openspec/changes/archive/2026-10-19-statements-v2/design.md`.
|
||
- Статус документа и конверта определяет пакет (`envelope_status`); шлюз его не пересчитывает. Все json набора
|
||
ошибочны — `error` с кодом первого ошибочного файла, а `metrics.warnings` всё равно перечисляют `JSON_FAILED` каждого
|
||
json. Успешный json без таблиц — `JSON_FAILED`. Ни одной таблицы — `error RECOGNITION_FAILED`.
|
||
- Предупреждения — структуры `{code, severity, message, locator}`, не строки; новый код предупреждения вводится только
|
||
словарём пакета (`WarningCode`, `make_warning`). При изменении правил обнови README (раздел «Предупреждения») и golden.
|
||
- Эталоны нормализатора лежат в **fixtures** (`../fixtures/ocr/expected_normalize/`): `make golden-update`, diff смотреть
|
||
в fixtures и объяснять; эталоны меняются отдельным PR fixtures.
|
||
- Горячие папки (раскладка FileWorker): имя `<job_id>.<ext>`, запись сразу в `Input` открытием `wb` с перезаписью, без
|
||
временного файла и `Tmp` — как в старом АРМ (change `hotfolder-direct-write`, ASSUMPTION(OQ-79)); `Tmp` — только у
|
||
эмулятора. Любое удаление (ответ, входящий файл, `.done`, недописанный при ошибке записи входящий файл) — только при
|
||
`SCAN_CLEANUP=true` и только после сохранения `raw/` (не при `TIMEOUT`, `FILE_READ_ERROR`, ошибке записи во входящий каталог или `raw/`). Ошибка
|
||
удаления — одна строка лога, конверт не меняется. Нет расширения у `original_name` → `RECOGNITION_FAILED`.
|
||
- Набор ответа готов по settle: есть `*.json`, а имена и размеры не менялись `SCAN_SETTLE_SECONDS` (по умолчанию 2;
|
||
отсчёт от первого опроса, увидевшего окончательный набор). Не ждать «один файл `<UUID>.json`».
|
||
- Склеенный `raw/{job_id}.json`: `{"job_id", "files": [{"name", "response", "parse_error"}, ...]}`, все `*.json`
|
||
каталога по возрастанию имени; нормализатор получает ровно эту структуру.
|
||
- Успех файла ответа — `error.code == ""` (`error` при этом не `null`!), ошибка — непустой `error.code`,
|
||
`parse_error` или корень ответа не объект (`INVALID_FORMAT`). Проверка `error is None` для реального ответа неверна.
|
||
Ошибочные файлы — предупреждения `JSON_FAILED` в `metrics.warnings` (`locator = file:<имя>`, `message = <код>: <сообщение>`); все файлы ошибочны → код первого; есть
|
||
успешный — документы из успешных файлов, статус не выше `needs_check` (№28). `NOT_IMPLEMENTED` сервис больше не публикует (`NullNormalizer` остался только
|
||
для тестов воркера).
|
||
- В одном json может быть несколько таблиц разных форм (баланс, ОФР); тип документа и редакцию определяет пакет
|
||
(`05-domain-rules.md` §11.9). `checks` считает пакет и отдаёт в документах.
|
||
- Dev-режим: эмулятор ИС Сканирование (`make emulator`, то же, что `python -m src.scan_emulator` из `api/`; в контейнере —
|
||
тот же образ с `command: python -m src.scan_emulator`): по входящему `<uuid>.<ext>` считает sha256 содержимого, находит
|
||
пару в `SCAN_FIXTURES_DIR` (PDF + `<имя>.json` + `<имя>_*.json`) и кладёт в `Output/<uuid>/` набор как `<uuid>.json` /
|
||
`<uuid>_<суффикс>.json` (атомарно, через `Tmp`). Промах → набор `SCAN_EMULATOR_DEFAULT`, иначе ответ-ошибка
|
||
`INVALID_DOCUMENT`. Имя входящего файла на выбор пары не влияет.
|
||
- Описание сервиса, протокол горячих папок, нормализация, env, запуск и коды ошибок — `README.md`; требования и решения
|
||
единой нормализации — `openspec/changes/archive/2026-10-19-statements-v2/`, Среза 3a — `openspec/changes/archive/2026-10-07-slice-3a-ocr-normalizer/`, Среза 0 —
|
||
`openspec/changes/archive/2026-10-04-slice-0-skeleton/`.
|
||
|
||
## Соглашения шаблона
|
||
Тот же шаблон команды, что у `arm-core` («Шаблон FastAPI», `RSHB/DKB_ARMKA@ba172f8`); общие соглашения —
|
||
`../docs/context/03-architecture.md`, «Соглашения шаблона команды». Репозиторий приведён к сервису **без фронта и без
|
||
БД**, но с SMB. Команды `make` — из корня репозитория (рецепты сами заходят в `api/`), `pytest` — из `api/`.
|
||
|
||
**Оставлено от шаблона:** `api/run_server.py` (точка входа платформы, `SERVER__HOST/PORT`), `api/src/app.py` (lifespan:
|
||
SMB-сессия, S3, NATS, воркер; `GET /healthcheck`; gzip/proxy middleware), `core/config.py` (pydantic-settings,
|
||
`OPENBAO__MINIO__*`, `OPENBAO__NATS__*`, `OPENBAO__SMB__*`, `DEBUG`), `core/exception_handlers.py` (подключается в
|
||
`create_app`), `api/pytest.ini`, `api/requirements*.txt` (включая `smbprotocol`), `raisa_config.yml`,
|
||
`setup/setup.sh` + `setup/pip.conf` (без npm-шага). Заглушки шаблона без кода сервиса: `src/api/` (пустой `router`,
|
||
пустые `dependencies/` и `endpoints/`), `src/schemas/` и `src/services/` (пустые пакеты), `common/exceptions.py`
|
||
(`NotFoundError`, `CouldntCreateError` нигде не используются), `core/logger.py` (`setup_logger` нигде не вызывается).
|
||
|
||
**Вырезано (Срез 0):** `web/`, `setup/.npmrc`; из `app.py` — mount SPA, CORS, `raisa_fastapi_protected_api`; `db/`,
|
||
`models/`, `repositories/`, `alembic/`; `OPENBAO.DATABASE`; из `requirements.txt` — `asyncpg`, `SQLAlchemy`, `alembic`,
|
||
`raisa_fastapi_protected_api`; шаблонные `core/nats_callback.py` и `README.md` заменены своими.
|
||
|
||
**Добавлено:**
|
||
- `api/src/gateway/` — горячие папки: протокол `HotFolder` (`hotfolder.py`: `LocalHotFolder`, `SettleTracker`, `inbox_name`;
|
||
методы `put`, `poll`, `cleanup`, `healthy`), `smb.py` (`SmbHotFolder`, импортируется лениво), фабрика `create_hotfolder`;
|
||
`merge.py` — склейка файлов ответа в `raw/`; референс поллинга — `../reference-old-arm/api/src/core/PDFWorker`.
|
||
- `api/src/core/` — `worker.py` (задание: проверка → ack → S3 → горячая папка → опрос → `raw/` → очистка (под флагом) →
|
||
нормализатор → результат; ошибки по таблице D-7), `nats.py` (core NATS без JetStream, queue group `scan-gateway`, публикация с
|
||
повторами), `clients.py` (`S3Storage`), `health.py` (`nats`/`hotfolder` в `/healthcheck`).
|
||
- `api/src/normalizer/` — протокол `Normalizer`, `NullNormalizer` (заглушка Среза 0, остаётся для тестов воркера; ошибки
|
||
файлов выдаёт `JSON_FAILED` через `make_warning` пакета), `outcome.py` (`NormalizeOutcome`), `files.py` (ошибки json
|
||
набора) и `OcrNormalizer` (подключён в `create_app`).
|
||
- `api/src/normalizer/ocr/` — `adapter.py` (адаптер шага 1), `answer_set.py` (обход набора json), `__init__.py`
|
||
(`OcrNormalizer`, граница контракта через `to_contract`). Остальной разбор — `api/arm_statements/` (копия пакета).
|
||
- `api/src/contract/` + `api/contracts/` — pydantic-модели (контракт 2.0, `CONTRACT_VERSION`) и байт-в-байт копии схем `../docs/contracts`; идентичность по правилу MAJOR (S-53,
|
||
`tests/contract_identity.py`; docs — `../docs` или `ARM_KA_DOCS`) проверяет `api/tests/unit/test_contract.py` (входит в `make check`).
|
||
- `api/src/scan_emulator/` — эмулятор ИС Сканирование (`SCAN_FIXTURES_DIR`, `SCAN_EMULATOR_DELAY`,
|
||
`SCAN_EMULATOR_POLL_SECONDS`, `SCAN_EMULATOR_DEFAULT`; пара по sha256 содержимого); в compose контейнера пока нет —
|
||
`make emulator`.
|
||
- `Makefile` (`venv`, `lint`, `test`, `test-integration`, `check`, `docker-build`, `check-full`, `up`,
|
||
`emulator`, `golden-update`, `statements-check`), `api/pyproject.toml` (ruff, mypy), `Dockerfile` + `.dockerignore` (один образ: шлюз и эмулятор),
|
||
`api/.env.example`, `api/.env.test`.
|
||
- Тесты: `api/tests/unit/` (в том числе `unit/normalizer/` — адаптер, обход набора, граница контракта, реальные ответы и golden), `api/tests/integration/`
|
||
(маркеры `integration`, `fixtures`; `test_roundtrip.py` — сквозной сценарий с эмулятором и `SCAN_CLEANUP`);
|
||
`api/tests/golden_update.py` — сбор склейки реального ответа и запись эталонов в `../fixtures/ocr/expected_normalize/`
|
||
(`make golden-update`); перед PR — `make check-full`.
|