arm-tabular-parser/CLAUDE.md
Andrey Epifancev 69564ffcbd docs: README, CLAUDE.md и change osv-flat-form (#59)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 13:19:18 +04:00

142 lines
16 KiB
Markdown
Raw 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 — tabular-parser
Общий контекст системы — `../CLAUDE.md` и `../docs/`. Этот файл — только про этот сервис.
## Что это
Stateless-сервис разбора XLS/XLSX/XLSB: ОСВ по счетам (§1 правил) и ББ/ОФР из Excel (§12). Вход — NATS
`files.tabular_uploaded` (по умолчанию; задаётся OPENBAO__NATS__NATS_*_SUBJECT) + файл в S3 `uploads/`; выход — JSON по `../docs/contracts/recognition-result.schema.json`
в S3 `results/` + NATS `files.tabular_parsed`. **Нет доступа к БД.** Не знает про организации, периметры, план счетов —
возвращает структуру файла; нормализация и резолв — в arm-core.
## Главное правило
Структура определяется динамически по содержимому (§1.0 правил): никаких координат ячеек, имён листов, букв колонок
в коде. Словари синонимов — `rules/*.yaml`. Фикстуры `../fixtures/osv/**` — регрессия, не спецификация формата:
новый формат = новая фикстура + синоним, не новый `if`.
## Правила репозитория
- `api/arm_statements/` — копия пакета `arm-statements`, напрямую не править: обновление — `../docs/scripts/sync-statements.sh`
(`--in-place` в PR фичи), свежесть — `make statements-check` (в `check-full`); код возврата 20 при расхождении. Словари
форм — в пакете; `api/rules/statement_*.yaml` удалены планом 4 (#54).
## Тесты
- property-тесты инвариантов (§1.4) на всех файлах `../fixtures/osv/**` и `../fixtures/statements-xlsx/**`; для пакета ДКБ
`../fixtures/osv-dkb/**` — `tests/fixtures/test_osv_dkb.py`: ожидаемые `needs_check` — манифест
`tests/osv_dkb_expectations.yaml` (данные), период — по имени каталога (`2025-Q3`, `2024-10_2025-09`);
- снимки `../fixtures/golden/expected_osv_parse/*.json` — после ручной сверки «Итого»;
- синтетика только для вариантов, которых нет в фикстурах (шапка вариант 2, Вал./Кол., xlsb, xls без CODEPAGE, без outline level).
## Соглашения шаблона
Тот же шаблон команды, что у `arm-core` («Шаблон FastAPI», `RSHB/DKB_ARMKA@ba172f8`); общие соглашения —
`../docs/context/03-architecture.md`, «Соглашения шаблона команды». Репозиторий приведён к сервису **без фронта и без БД**
(change `slice-0-skeleton`). Команды backend — из `api/`, `make` — из корня репозитория.
**Оставлено от шаблона:** `api/run_server.py` (точка входа платформы, `SERVER__HOST/PORT`), `api/src/app.py` (lifespan:
подключение к MinIO и NATS, ошибка подключения логируется и не мешает старту; `GET /healthcheck`, gzip/proxy middleware),
`core/config.py` (pydantic-settings, `SERVICE_NAME` — только `service.name` конвертов (queue group — константа в `core/nats.py`), `OPENBAO__MINIO__*`
включая `MINIO_SECURE`, `OPENBAO__NATS__*`, `DEBUG`, `ENV_FILE`, `NATS_ENABLED`, `WORKER_CONCURRENCY`, `PUBLISH_RETRIES`, `DONE_CACHE_SIZE`), `core/clients.py` (S3), `core/logger.py`,
`core/exception_handlers.py` (без SQLAlchemy), `common/exceptions.py`, `pytest.ini`, `requirements*.txt`, `raisa_config.yml`,
`setup/setup.sh` + `pip.conf` (без npm-шага). Шаблонный `core/nats_callback.py` (consumer `files.tabular_parsed` — зона
ядра) удалён задачей 4.3: его место занял воркер `core/worker.py`.
**Вырезано (Срез 0):** `web/` целиком и `setup/.npmrc`; из `app.py` — mount SPA (`SPAStaticFiles`, `os.makedirs("web")`),
CORS, регистрация SMB-сессии, импорт `raisa_fastapi_protected_api`, зависимость от SQLAlchemy в обработчиках ошибок;
`db/`, `models/`, `repositories/`, `alembic/`, `alembic.ini`; из настроек — `OPENBAO.DATABASE`, `OPENBAO.SMB`; из
`requirements.txt` — `asyncpg`, `SQLAlchemy`, `alembic`, `smbprotocol`, `argon2-cffi`, `greenlet`, `Mako`,
`raisa_fastapi_protected_api`; шаблонный `README.md` заменён описанием сервиса.
**Добавлено** (в скобках — задача `openspec/changes/slice-0-skeleton/tasks.md`):
- `api/src/parser/` — интерфейс `Parser` и заглушка `NullParser`; разбор ОСВ §1 — Срез 2a (см. ниже), форм §12 —
Срез 2b;
- `api/src/contract/` — pydantic-модели контракта v2.0 (запрос, результат, конверт); `api/contracts/` — копии схем (идентичность по правилу MAJOR (S-53),
`tests/contract_identity.py`, с `../docs/contracts` проверяется в `make check`) — 3.1;
- `api/rules/` — README о назначении; сами словари `*.yaml` (в документах `rules/*.yaml` относительно `api/`)
появились в Срезе 2a — 4.1;
- `api/src/core/nats.py` и `api/src/core/worker.py` — подписка queue group `tabular-parser` на
`files.tabular_uploaded` → ack → S3 `uploads/` → парсер → S3 `results/` → `files.tabular_parsed` — 4.2–4.3;
- `api/pyproject.toml` (ruff, mypy) и `Makefile` — 2.1; `Dockerfile`, `api/.env.example`, `api/.env.test` — 2.2.
## Добавлено в Срезе 2a (парсер ОСВ; `openspec/changes/slice-2a-osv-parser/`)
**Модули `api/src/parser/`** (конвейер сверху вниз; подробности и статусы — `README.md`, раздел «Парсер ОСВ»):
- `grid.py` + `readers/` — модель листа и чтение xlsx/xls/xlsb; единственное место, где допустимы индексы и буквы колонок
(`column_letter` — только диагностика). `read_book(data, file_type) -> (листы, предупреждения)`, формат — по сигнатуре.
- `rules.py` — загрузчик словарей `api/rules/*.yaml`; `detect.py` — вид листа и заголовок таблицы; `header.py` — шапка
и `group_schema`; `hierarchy.py` — каскад `outline → indent → account_pattern → header_account → sums` (+ `trivial`); `inn_rows.py` —
строки ИНН рядом с контрагентом (OQ-163); `indicators.py` — строки-показатели (колонка «Показатели», «В валюте»); `numbers.py` — `Decimal` и ИНН (контрольная сумма); `classify.py` — строки и группировки; `checks.py` — контрольные суммы §1.4; `osv.py` —
оркестратор `OsvParser`; `deadline.py` — кооперативный таймаут; `errors.py` — коды ошибок; `cli.py` — разбор файла.
- `get_parser(settings)` создаёт `OsvParser` один раз при старте (кэша нет); `NullParser` — только для тестов воркера.
**Правила расширения:**
- Новый формат ОСВ = фикстура в `../fixtures/osv/` (или синтетика `s1x_…` в `tests/synthetic/generate.py`) + синоним в
YAML. Не новый `if` под файл, организацию, имя листа или номер строки; «нужна координата» означает, что алгоритм не по
содержимому — остановиться и обсудить.
- Нет координат: AST-линтер `tests/unit/parser/test_no_coordinates.py` проверяет `src/parser/**` (кроме `grid.py` и
`readers/`): литералы букв колонок и адресов ячеек, имена листов по умолчанию, сравнения номера строки с числом,
обращения к ячейке константами. Значения enum контракта вычисляются из `src.contract` и под линтер не попадают.
- Каждая запись словаря должна срабатывать хотя бы на одном реальном или синтетическом файле
(`tests/fixtures/test_rules_coverage.py`); мёртвый синоним удаляется, пробел в фикстурах закрывается синтетикой.
- Golden-снимки (`../fixtures/golden/expected_osv_parse/`, 131 файл: 72 реальных + 23 пакета ДКБ (`osv-dkb/**` до 1 МиБ, снимки в `osv-dkb/`) + 36 синтетических) — регрессия, а не
оракул; правятся только `make golden-update` с просмотром diff и отдельным коммитом в `fixtures`. Корректность
проверяют property-тест и ручная сверка «Итого».
- Деньги — `Decimal`, `float` в домене нет; `needs_check` — только по контрольным суммам, структуру под сходимость не
подгонять.
**Нетривиальное правило §1.4 (R-21):** при сверке «сумма детей = родитель» обороты сравниваются по колонкам, а сальдо —
по колонкам либо по нетто `дебет − кредит`, если у родителя сальдо свёрнуто (1С показывает счёт нетто, а субсчета и
контрагентов — развёрнуто). Это решение зафиксировано в design №31; не ослаблять и не расширять без обсуждения.
**Счета без сальдо по аналитике (OQ-132, #11 п. 2):** список счетов (90, 91, 99) — данные `api/rules/osv_checks.yaml`, не код;
тождество строки на них не проверяется, «сумма детей = родитель» (обороты всегда, сальдо — только корень → субсчета) и
«Итого» сохраняются. Счёт 90 добавлен решением владельца 19.10.2026 (O-1 из #58). Не расширять список «чтобы файл прошёл» — только по решению владельца.
**Настройки:** `PARSE_MAX_BYTES` (по умолчанию 50 МиБ; больше — `UNSUPPORTED_FORMAT`) и `PARSE_TIMEOUT_SECONDS` (120 с;
кооперативный, → `TIMEOUT`) — `core/config.py`. Фазу чтения таймаут не прерывает: до библиотеки чтения действуют
лимиты `readers/_limits.py` (распакованный zip ≤ 10 × `PARSE_MAX_BYTES`, площадь объединённых ячеек ≤ 200 000; R-23).
**Контракт:** `rows[].extra` (нераспознанные колонки таблицы по заголовку; появилось в v1.1); текущая версия — 2.0
(`CONTRACT_VERSION`, раздел «Формы через arm_statements»); копия схемы в `api/contracts/` идентична `../docs/contracts/` по правилу MAJOR (S-53, `tests/contract_identity.py`; проверяется в `make check`).
**Тесты:** `make check` ≈ 95 с, потому что `make test` (`-m "not integration"`) включает тесты маркера `fixtures` (golden,
property, покрытие словарей); без каталога `../fixtures` они пропускаются. Полный набор — `make check-full` (+ интеграция
с compose и сборка образа).
Проверки: `make check` (`pytest`, `ruff check . && ruff format --check .`, `mypy src`), `make check-full` перед PR.
## Формы через arm_statements (план 4, #54)
Конвейер форм ББ/ОФР (шаги 2–8, словари форм, перевод в контракт) — в пакете `api/arm_statements/` (копия
`arm-statements`, версия — `arm_statements/VERSION`). Собственного конвейера форм и словарей форм в сервисе нет: удалены
`api/src/parser/statements/{checks,columns,detect,header,lines,merge,statement}.py` и
`api/rules/statement_*.yaml`.
**Модули адаптера** (`api/src/parser/statements/`): `forms.py` (блоки форм и вид листа по словарям пакета), `cells.py`
(ячейка Excel -> `arm_statements.Cell`), `header_cells.py` (шапка по меткам словаря), `adapter.py` (`plan_sheets`,
`build_candidates`: листы -> кандидаты для пакета, продолжение присоединяется), `pipeline.py` (`StatementPipeline`:
кандидаты -> `normalize_candidate` -> `to_contract`), `number_format.py` (числовой формат ячейки, #42).
`book.py` (`BookParser`) склеивает ОСВ и формы; `get_parser(settings)` создаёт его с `default_rules()` пакета.
**Контракт 2.0:** `CONTRACT_VERSION = "2.0"`; у документов `status` и `warnings`; `checks` обязателен у BALANCE/INCOME со
строками; `line_code` может быть пуст (`line_status`). `service.statements_version` — версия копии пакета (без `VERSION`
сервис не стартует). Копия `api/contracts/` идентична `../docs/contracts/` (проверяется в `make check`).
**Коды `OSV_*`:** предупреждения листа ОСВ — уровень `info`, модуль `osv_warnings.py` (контракт 2.0); предупреждение файла
`XLS_NO_CODEPAGE` — из пакета.
**Правила расширения форм:**
- Словари форм меняются только в `arm-statements`, затем `sync-statements` (`--in-place` в PR фичи). Новое написание или
соотношение — там же, плюс фикстура или синтетика, на которой оно срабатывает (`tests/synthetic/generate_statements.py`,
`../fixtures/statements-xlsx/`). Не `if` под файл, организацию или лист.
- **Копию `api/arm_statements/` не правим напрямую** (код возврата 20 у `statements-check` при расхождении). Правка в
копии теряется при следующем `sync-statements`.
- Координат в `src/parser/**` нет (кроме `grid.py`, `readers/`): тот же AST-линтер `test_no_coordinates.py`; слов формы в
коде сервиса нет. Деньги — `Decimal`; единицы не нормализуются, первичка не мутируется.
- Модули ОСВ (`header.py`, `classify.py`, `checks.py`, `hierarchy.py`) под формы не правятся.
**Тесты на реальных данных:** манифест ожиданий — `api/tests/statements_manifest.yaml` (18 файлов). Golden — два каталога
в `../fixtures/golden/`: `expected_osv_parse` и `expected_statement_parse`; правятся только `make golden-update` с
просмотром diff и отдельным коммитом в `fixtures`.
**Значения форм:** значения ББ/ОФР из Excel — как их показывает числовой формат ячейки (#42, `statements/number_format.py`;
xlsb — как в файле + предупреждение `XLSB_NO_FORMATS`); ОСВ формат не применяет.
Книга НПЗ в манифесте — `done` (итоги расходятся в пределах округления, OQ-138).