128 lines
14 KiB
Markdown
128 lines
14 KiB
Markdown
# 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`). Словари `api/rules/statement_*.yaml`
|
||
сервиса остаются до плана 4.
|
||
|
||
## Тесты
|
||
- 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-модели контракта v1.1 (запрос, результат, конверт); `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 → sums` (+ `trivial`); `inn_rows.py` —
|
||
строки ИНН рядом с контрагентом (OQ-163); `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/`, 127 файлов: 72 реальных + 23 пакета ДКБ (`osv-dkb/**` до 1 МиБ, снимки в `osv-dkb/`) + 32 синтетических) — регрессия, а не
|
||
оракул; правятся только `make golden-update` с просмотром diff и отдельным коммитом в `fixtures`. Корректность
|
||
проверяют property-тест и ручная сверка «Итого».
|
||
- Деньги — `Decimal`, `float` в домене нет; `needs_check` — только по контрольным суммам, структуру под сходимость не
|
||
подгонять.
|
||
|
||
**Нетривиальное правило §1.4 (R-21):** при сверке «сумма детей = родитель» обороты сравниваются по колонкам, а сальдо —
|
||
по колонкам либо по нетто `дебет − кредит`, если у родителя сальдо свёрнуто (1С показывает счёт нетто, а субсчета и
|
||
контрагентов — развёрнуто). Это решение зафиксировано в design №31; не ослаблять и не расширять без обсуждения.
|
||
|
||
**Счета без сальдо по аналитике (OQ-132, #11 п. 2):** список счетов (91, 99) — данные `api/rules/osv_checks.yaml`, не код;
|
||
тождество строки на них не проверяется, «сумма детей = родитель» (обороты всегда, сальдо — только корень → субсчета) и
|
||
«Итого» сохраняются. Счёт 90 в список не входит. Не расширять список «чтобы файл прошёл» — только по решению владельца.
|
||
|
||
**Настройки:** `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).
|
||
|
||
**Контракт v1.1:** `rows[].extra` (нераспознанные колонки таблицы по заголовку); `CONTRACT_VERSION = "1.1"`; копия схемы
|
||
в `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.
|
||
|
||
## Добавлено в Срезе 2b (парсер ББ/ОФР; `openspec/changes/slice-2b-statement-parser/`)
|
||
|
||
**Модули:** `api/src/parser/book.py` (`BookParser`: лимиты, чтение, дедлайн, вид листа, статус и карта ошибок; документы —
|
||
ОСВ в порядке листов, затем формы) и `api/src/parser/statements/` (`detect` → `columns` → `header` → `lines` → `checks` →
|
||
`merge`/`statement`; подробности — `README.md`, раздел «Парсер форм (ББ/ОФР)»). `get_parser(settings)` теперь создаёт
|
||
`BookParser` (листы ОСВ по-прежнему разбирает `osv.py`).
|
||
|
||
**Правила расширения форм:**
|
||
- Новая форма или написание = строка в `api/rules/statement_*.yaml` (+ синтетика `st0x` в
|
||
`tests/synthetic/generate_statements.py` или реальный файл в `../fixtures/statements-xlsx/`). Не `if` под файл,
|
||
организацию или лист. Соотношения §7 — данные `statement_checks.yaml`.
|
||
- Координат в `src/parser/**` нет (кроме `grid.py`, `readers/`): тот же AST-линтер `test_no_coordinates.py`; слов формы в
|
||
коде нет — только в YAML. Деньги — `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`.
|
||
|
||
**Контракт v1.2:** `CONTRACT_VERSION = "1.2"`; `documents[]` получает `BALANCE`/`INCOME`/`DISCLOSURE` (`statementDocument`:
|
||
`header`, `lines`, `checks`); копия `api/contracts/` идентична `../docs/contracts/`. Значения ББ/ОФР из Excel — как их показывает
|
||
числовой формат ячейки (#42, `statements/number_format.py`; xlsb — как в файле + предупреждение); ОСВ формат не применяет.
|
||
Книга НПЗ в манифесте — `done` (итоги расходятся в пределах округления, OQ-138).
|