Ссылки README и CLAUDE.md переведены на архив. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
12 KiB
CLAUDE.md — scan-gateway
Общий контекст системы — ../CLAUDE.md и ../docs/. Этот файл — только про этот сервис.
Что это
Адаптер внешней ИС Сканирование (Content AI) для PDF/PNG/JPG: положить файл во «Входящие» как <job_id>.<ext>
(расширение original_name в нижнем регистре; .part пишется в SCAN_TMP_DIR, по умолчанию Tmp, на той же ФС/шаре и
публикуется без замены), забрать все Исходящие/<job_id>/*.json (ИС кладёт по одному json на каждый
распознанный документ), склеить и сохранить сырой ответ в S3 raw/{job_id}.json (в БД не регистрируется), очистить
горячую папку (только при SCAN_CLEANUP=true), нормализовать в ../docs/contracts/recognition-result.schema.json
(statementDocument v1.2: по одному документу на таблицу table_structure[i]), опубликовать recognition.result
(done / needs_check / error). Таймаут → ошибка TIMEOUT по min(reply_deadline, старт + SCAN_TIMEOUT_SECONDS=300),
старт — запись файла во «Входящие». Нет доступа к БД.
Правила
- Нормализация по фактическим ответам
../fixtures/ocr/aep_*.json, а не по ТЗ (OQ-13, OQ-14), по05-domain-rules.md§11 и §11.1: правило, которого там нет, не реализуется, а поднимается вопросом. Функции очистки (src/normalizer/ocr/) перенесены из старого ядра дословно (единственное исключение дляreference-old-arm): при правке сначала разберись, что делает источник; расхождения с ним — только решением, записанным вopenspec/changes/archive/2026-10-07-slice-3a-ocr-normalizer/design.md(таблица «Decisions made without discussion»: №12, №13, №20, №22, №30 —Moneyне больше двух знаков после запятой, №31 — тексты дат длиннее 200 символов не разбираются). Функцииocr/*— чистые: без I/O, без настроек, без логов. - Справочники — данные:
api/rules/*.yaml(редакции форм66n/fsbu4, двойники,label→ тип), правила не зашиваются в код.fsbu4пока пуста — данные кладёт владелец, код при этом не меняется. - Статус:
doneтолько когда всё распознано (confidence100, даты разобраны, редакция определена по дате и заполнена, нет предупреждений) и ни один файл набора не ошибочен (№28); иначеneeds_check; ни одной таблицы →error RECOGNITION_FAILED. Предупреждения — строкиmetrics.warnings(<имя json>: table:<i>: <текст>); при изменении их текстов обнови README (раздел «Предупреждения») и golden. - Эталоны нормализатора лежат в fixtures (
../fixtures/ocr/expected_normalize/):make golden-update, diff смотреть в fixtures и объяснять; эталоны меняются отдельным PR fixtures. - Горячие папки (раскладка FileWorker): имя
<job_id>.<ext>,.partтолько вTmp, запись без замены (os.link/smbclient.rename; существующее назначение = «доставлено»)..part, ставший целью публикации, снимается самой записью независимо от флага (№26); любое другое удаление (ответ, входящий файл,.done, осиротевшие.part) — только при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для реального ответа неверна. Ошибочные файлы — вmetrics.warnings(<имя>: <код>: <сообщение>); все файлы ошибочны → код первого; есть успешный — документы из успешных файлов, статус не вышеneeds_check(№28).NOT_IMPLEMENTEDсервис больше не публикует (NullNormalizerостался только для тестов воркера). - В одном json может быть несколько таблиц разных форм (баланс, ОФР); тип документа задаёт
labelтаблицы (запасной путь — коды строк).checksв документах не передаются — их считает ядро (Срез 3b). - 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; требования и решения Среза 3a —openspec/changes/archive/2026-10-07-slice-3a-ocr-normalizer/(design.md,specs/), Среза 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,NormalizeOutcome,NullNormalizer(заглушка Среза 0, остаётся для тестов воркера) иOcrNormalizer(Срез 3a, подключён вcreate_app).api/src/normalizer/ocr/(Срез 3a) — конвейер чистых функций:text.py→dates.py→amounts.py→layout.py→rows.py→header.py→document.py;__init__.py—OcrNormalizer(обход файлов склейки, статус,metrics).api/src/normalizer/rules.py—load_rules(SCAN_RULES_DIR): pydantic-модели справочников,OcrRules(выбор редакции по дате / по кодам);api/rules/—statement_lines.yaml(редакции66n,fsbu4),ocr_lookalikes.yaml,statement_labels.yaml.api/src/contract/+api/contracts/— pydantic-модели и байт-в-байт копии схем../docs/contracts; идентичность проверяет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),api/pyproject.toml(ruff, mypy),Dockerfile+.dockerignore(один образ: шлюз и эмулятор),api/.env.example,api/.env.test.- Тесты:
api/tests/unit/(в том числеunit/normalizer/— юниты по модулямocr/и 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.