arm-scan-gateway/CLAUDE.md
Andrey Epifancev ce0702ac9b chore(openspec): архив contract-identity-window и statements-v2 (#42, #46)
Ссылки README и CLAUDE.md ведут в архив.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 09:26:39 +04:00

13 KiB
Raw Blame History

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.