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

102 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`.