Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
13 KiB
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/statements-v2/design.md.
- адаптер шага 1 —
- Статус документа и конверта определяет пакет (
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— как в старом АРМ (changehotfolder-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/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 groupscan-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.